##// 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 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2016 RhodeCode GmbH
3 # Copyright (C) 2010-2016 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 collections
21 import collections
22
22
23 import sqlalchemy
23 import sqlalchemy
24 from sqlalchemy import UnicodeText
24 from sqlalchemy import UnicodeText
25 from sqlalchemy.ext.mutable import Mutable
25 from sqlalchemy.ext.mutable import Mutable
26
26
27 from rhodecode.lib.ext_json import json
27 from rhodecode.lib.ext_json import json
28
28
29
29
30 class JsonRaw(unicode):
30 class JsonRaw(unicode):
31 """
31 """
32 Allows interacting with a JSON types field using a raw string.
32 Allows interacting with a JSON types field using a raw string.
33
33
34 For example::
34 For example::
35 db_instance = JsonTable()
35 db_instance = JsonTable()
36 db_instance.enabled = True
36 db_instance.enabled = True
37 db_instance.json_data = JsonRaw('{"a": 4}')
37 db_instance.json_data = JsonRaw('{"a": 4}')
38
38
39 This will bypass serialization/checks, and allow storing
39 This will bypass serialization/checks, and allow storing
40 raw values
40 raw values
41 """
41 """
42 pass
42 pass
43
43
44
44
45 # Set this to the standard dict if Order is not required
45 # Set this to the standard dict if Order is not required
46 DictClass = collections.OrderedDict
46 DictClass = collections.OrderedDict
47
47
48
48
49 class JSONEncodedObj(sqlalchemy.types.TypeDecorator):
49 class JSONEncodedObj(sqlalchemy.types.TypeDecorator):
50 """
50 """
51 Represents an immutable structure as a json-encoded string.
51 Represents an immutable structure as a json-encoded string.
52
52
53 If default is, for example, a dict, then a NULL value in the
53 If default is, for example, a dict, then a NULL value in the
54 database will be exposed as an empty dict.
54 database will be exposed as an empty dict.
55 """
55 """
56
56
57 impl = UnicodeText
57 impl = UnicodeText
58 safe = True
58 safe = True
59
59
60 def __init__(self, *args, **kwargs):
60 def __init__(self, *args, **kwargs):
61 self.default = kwargs.pop('default', None)
61 self.default = kwargs.pop('default', None)
62 self.safe = kwargs.pop('safe_json', self.safe)
62 self.safe = kwargs.pop('safe_json', self.safe)
63 self.dialect_map = kwargs.pop('dialect_map', {})
63 self.dialect_map = kwargs.pop('dialect_map', {})
64 super(JSONEncodedObj, self).__init__(*args, **kwargs)
64 super(JSONEncodedObj, self).__init__(*args, **kwargs)
65
65
66 def load_dialect_impl(self, dialect):
66 def load_dialect_impl(self, dialect):
67 if dialect.name in self.dialect_map:
67 if dialect.name in self.dialect_map:
68 return dialect.type_descriptor(self.dialect_map[dialect.name])
68 return dialect.type_descriptor(self.dialect_map[dialect.name])
69 return dialect.type_descriptor(self.impl)
69 return dialect.type_descriptor(self.impl)
70
70
71 def process_bind_param(self, value, dialect):
71 def process_bind_param(self, value, dialect):
72 if isinstance(value, JsonRaw):
72 if isinstance(value, JsonRaw):
73 value = value
73 value = value
74 elif value is not None:
74 elif value is not None:
75 value = json.dumps(value)
75 value = json.dumps(value)
76 return value
76 return value
77
77
78 def process_result_value(self, value, dialect):
78 def process_result_value(self, value, dialect):
79 if self.default is not None and (not value or value == '""'):
79 if self.default is not None and (not value or value == '""'):
80 return self.default()
80 return self.default()
81
81
82 if value is not None:
82 if value is not None:
83 try:
83 try:
84 value = json.loads(value, object_pairs_hook=DictClass)
84 value = json.loads(value, object_pairs_hook=DictClass)
85 except Exception as e:
85 except Exception as e:
86 if self.safe:
86 if self.safe:
87 return self.default()
87 return self.default()
88 else:
88 else:
89 raise
89 raise
90 return value
90 return value
91
91
92
92
93 class MutationObj(Mutable):
93 class MutationObj(Mutable):
94 @classmethod
94 @classmethod
95 def coerce(cls, key, value):
95 def coerce(cls, key, value):
96 if isinstance(value, dict) and not isinstance(value, MutationDict):
96 if isinstance(value, dict) and not isinstance(value, MutationDict):
97 return MutationDict.coerce(key, value)
97 return MutationDict.coerce(key, value)
98 if isinstance(value, list) and not isinstance(value, MutationList):
98 if isinstance(value, list) and not isinstance(value, MutationList):
99 return MutationList.coerce(key, value)
99 return MutationList.coerce(key, value)
100 return value
100 return value
101
101
102 @classmethod
102 @classmethod
103 def _listen_on_attribute(cls, attribute, coerce, parent_cls):
103 def _listen_on_attribute(cls, attribute, coerce, parent_cls):
104 key = attribute.key
104 key = attribute.key
105 if parent_cls is not attribute.class_:
105 if parent_cls is not attribute.class_:
106 return
106 return
107
107
108 # rely on "propagate" here
108 # rely on "propagate" here
109 parent_cls = attribute.class_
109 parent_cls = attribute.class_
110
110
111 def load(state, *args):
111 def load(state, *args):
112 val = state.dict.get(key, None)
112 val = state.dict.get(key, None)
113 if coerce:
113 if coerce:
114 val = cls.coerce(key, val)
114 val = cls.coerce(key, val)
115 state.dict[key] = val
115 state.dict[key] = val
116 if isinstance(val, cls):
116 if isinstance(val, cls):
117 val._parents[state.obj()] = key
117 val._parents[state.obj()] = key
118
118
119 def set(target, value, oldvalue, initiator):
119 def set(target, value, oldvalue, initiator):
120 if not isinstance(value, cls):
120 if not isinstance(value, cls):
121 value = cls.coerce(key, value)
121 value = cls.coerce(key, value)
122 if isinstance(value, cls):
122 if isinstance(value, cls):
123 value._parents[target.obj()] = key
123 value._parents[target.obj()] = key
124 if isinstance(oldvalue, cls):
124 if isinstance(oldvalue, cls):
125 oldvalue._parents.pop(target.obj(), None)
125 oldvalue._parents.pop(target.obj(), None)
126 return value
126 return value
127
127
128 def pickle(state, state_dict):
128 def pickle(state, state_dict):
129 val = state.dict.get(key, None)
129 val = state.dict.get(key, None)
130 if isinstance(val, cls):
130 if isinstance(val, cls):
131 if 'ext.mutable.values' not in state_dict:
131 if 'ext.mutable.values' not in state_dict:
132 state_dict['ext.mutable.values'] = []
132 state_dict['ext.mutable.values'] = []
133 state_dict['ext.mutable.values'].append(val)
133 state_dict['ext.mutable.values'].append(val)
134
134
135 def unpickle(state, state_dict):
135 def unpickle(state, state_dict):
136 if 'ext.mutable.values' in state_dict:
136 if 'ext.mutable.values' in state_dict:
137 for val in state_dict['ext.mutable.values']:
137 for val in state_dict['ext.mutable.values']:
138 val._parents[state.obj()] = key
138 val._parents[state.obj()] = key
139
139
140 sqlalchemy.event.listen(parent_cls, 'load', load, raw=True,
140 sqlalchemy.event.listen(parent_cls, 'load', load, raw=True,
141 propagate=True)
141 propagate=True)
142 sqlalchemy.event.listen(parent_cls, 'refresh', load, raw=True,
142 sqlalchemy.event.listen(parent_cls, 'refresh', load, raw=True,
143 propagate=True)
143 propagate=True)
144 sqlalchemy.event.listen(parent_cls, 'pickle', pickle, raw=True,
144 sqlalchemy.event.listen(parent_cls, 'pickle', pickle, raw=True,
145 propagate=True)
145 propagate=True)
146 sqlalchemy.event.listen(attribute, 'set', set, raw=True, retval=True,
146 sqlalchemy.event.listen(attribute, 'set', set, raw=True, retval=True,
147 propagate=True)
147 propagate=True)
148 sqlalchemy.event.listen(parent_cls, 'unpickle', unpickle, raw=True,
148 sqlalchemy.event.listen(parent_cls, 'unpickle', unpickle, raw=True,
149 propagate=True)
149 propagate=True)
150
150
151
151
152 class MutationDict(MutationObj, DictClass):
152 class MutationDict(MutationObj, DictClass):
153 @classmethod
153 @classmethod
154 def coerce(cls, key, value):
154 def coerce(cls, key, value):
155 """Convert plain dictionary to MutationDict"""
155 """Convert plain dictionary to MutationDict"""
156 self = MutationDict(
156 self = MutationDict(
157 (k, MutationObj.coerce(key, v)) for (k, v) in value.items())
157 (k, MutationObj.coerce(key, v)) for (k, v) in value.items())
158 self._key = key
158 self._key = key
159 return self
159 return self
160
160
161 def __setitem__(self, key, value):
161 def __setitem__(self, key, value):
162 # Due to the way OrderedDict works, this is called during __init__.
162 # Due to the way OrderedDict works, this is called during __init__.
163 # At this time we don't have a key set, but what is more, the value
163 # At this time we don't have a key set, but what is more, the value
164 # being set has already been coerced. So special case this and skip.
164 # being set has already been coerced. So special case this and skip.
165 if hasattr(self, '_key'):
165 if hasattr(self, '_key'):
166 value = MutationObj.coerce(self._key, value)
166 value = MutationObj.coerce(self._key, value)
167 DictClass.__setitem__(self, key, value)
167 DictClass.__setitem__(self, key, value)
168 self.changed()
168 self.changed()
169
169
170 def __delitem__(self, key):
170 def __delitem__(self, key):
171 DictClass.__delitem__(self, key)
171 DictClass.__delitem__(self, key)
172 self.changed()
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 class MutationList(MutationObj, list):
183 class MutationList(MutationObj, list):
176 @classmethod
184 @classmethod
177 def coerce(cls, key, value):
185 def coerce(cls, key, value):
178 """Convert plain list to MutationList"""
186 """Convert plain list to MutationList"""
179 self = MutationList((MutationObj.coerce(key, v) for v in value))
187 self = MutationList((MutationObj.coerce(key, v) for v in value))
180 self._key = key
188 self._key = key
181 return self
189 return self
182
190
183 def __setitem__(self, idx, value):
191 def __setitem__(self, idx, value):
184 list.__setitem__(self, idx, MutationObj.coerce(self._key, value))
192 list.__setitem__(self, idx, MutationObj.coerce(self._key, value))
185 self.changed()
193 self.changed()
186
194
187 def __setslice__(self, start, stop, values):
195 def __setslice__(self, start, stop, values):
188 list.__setslice__(self, start, stop,
196 list.__setslice__(self, start, stop,
189 (MutationObj.coerce(self._key, v) for v in values))
197 (MutationObj.coerce(self._key, v) for v in values))
190 self.changed()
198 self.changed()
191
199
192 def __delitem__(self, idx):
200 def __delitem__(self, idx):
193 list.__delitem__(self, idx)
201 list.__delitem__(self, idx)
194 self.changed()
202 self.changed()
195
203
196 def __delslice__(self, start, stop):
204 def __delslice__(self, start, stop):
197 list.__delslice__(self, start, stop)
205 list.__delslice__(self, start, stop)
198 self.changed()
206 self.changed()
199
207
200 def append(self, value):
208 def append(self, value):
201 list.append(self, MutationObj.coerce(self._key, value))
209 list.append(self, MutationObj.coerce(self._key, value))
202 self.changed()
210 self.changed()
203
211
204 def insert(self, idx, value):
212 def insert(self, idx, value):
205 list.insert(self, idx, MutationObj.coerce(self._key, value))
213 list.insert(self, idx, MutationObj.coerce(self._key, value))
206 self.changed()
214 self.changed()
207
215
208 def extend(self, values):
216 def extend(self, values):
209 list.extend(self, (MutationObj.coerce(self._key, v) for v in values))
217 list.extend(self, (MutationObj.coerce(self._key, v) for v in values))
210 self.changed()
218 self.changed()
211
219
212 def pop(self, *args, **kw):
220 def pop(self, *args, **kw):
213 value = list.pop(self, *args, **kw)
221 value = list.pop(self, *args, **kw)
214 self.changed()
222 self.changed()
215 return value
223 return value
216
224
217 def remove(self, value):
225 def remove(self, value):
218 list.remove(self, value)
226 list.remove(self, value)
219 self.changed()
227 self.changed()
220
228
221
229
222 def JsonType(impl=None, **kwargs):
230 def JsonType(impl=None, **kwargs):
223 """
231 """
224 Helper for using a mutation obj, it allows to use .with_variant easily.
232 Helper for using a mutation obj, it allows to use .with_variant easily.
225 example::
233 example::
226
234
227 settings = Column('settings_json',
235 settings = Column('settings_json',
228 MutationObj.as_mutable(
236 MutationObj.as_mutable(
229 JsonType(dialect_map=dict(mysql=UnicodeText(16384))))
237 JsonType(dialect_map=dict(mysql=UnicodeText(16384))))
230 """
238 """
231
239
232 if impl == 'list':
240 if impl == 'list':
233 return JSONEncodedObj(default=list, **kwargs)
241 return JSONEncodedObj(default=list, **kwargs)
234 elif impl == 'dict':
242 elif impl == 'dict':
235 return JSONEncodedObj(default=DictClass, **kwargs)
243 return JSONEncodedObj(default=DictClass, **kwargs)
236 else:
244 else:
237 return JSONEncodedObj(**kwargs)
245 return JSONEncodedObj(**kwargs)
238
246
239
247
240 JSON = MutationObj.as_mutable(JsonType())
248 JSON = MutationObj.as_mutable(JsonType())
241 """
249 """
242 A type to encode/decode JSON on the fly
250 A type to encode/decode JSON on the fly
243
251
244 sqltype is the string type for the underlying DB column::
252 sqltype is the string type for the underlying DB column::
245
253
246 Column(JSON) (defaults to UnicodeText)
254 Column(JSON) (defaults to UnicodeText)
247 """
255 """
248
256
249 JSONDict = MutationObj.as_mutable(JsonType('dict'))
257 JSONDict = MutationObj.as_mutable(JsonType('dict'))
250 """
258 """
251 A type to encode/decode JSON dictionaries on the fly
259 A type to encode/decode JSON dictionaries on the fly
252 """
260 """
253
261
254 JSONList = MutationObj.as_mutable(JsonType('list'))
262 JSONList = MutationObj.as_mutable(JsonType('list'))
255 """
263 """
256 A type to encode/decode JSON lists` on the fly
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