##// END OF EJS Templates
jsonalchemy: fix a case when default is not set and parsinge exception happens....
marcink -
r2396:6206bdd3 default
parent child Browse files
Show More
@@ -1,265 +1,265 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 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 and self.default is not None:
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):
174 def __setstate__(self, state):
175 self.__dict__ = state
175 self.__dict__ = state
176
176
177 def __reduce_ex__(self, proto):
177 def __reduce_ex__(self, proto):
178 # support pickling of MutationDicts
178 # support pickling of MutationDicts
179 d = dict(self)
179 d = dict(self)
180 return (self.__class__, (d, ))
180 return (self.__class__, (d, ))
181
181
182
182
183 class MutationList(MutationObj, list):
183 class MutationList(MutationObj, list):
184 @classmethod
184 @classmethod
185 def coerce(cls, key, value):
185 def coerce(cls, key, value):
186 """Convert plain list to MutationList"""
186 """Convert plain list to MutationList"""
187 self = MutationList((MutationObj.coerce(key, v) for v in value))
187 self = MutationList((MutationObj.coerce(key, v) for v in value))
188 self._key = key
188 self._key = key
189 return self
189 return self
190
190
191 def __setitem__(self, idx, value):
191 def __setitem__(self, idx, value):
192 list.__setitem__(self, idx, MutationObj.coerce(self._key, value))
192 list.__setitem__(self, idx, MutationObj.coerce(self._key, value))
193 self.changed()
193 self.changed()
194
194
195 def __setslice__(self, start, stop, values):
195 def __setslice__(self, start, stop, values):
196 list.__setslice__(self, start, stop,
196 list.__setslice__(self, start, stop,
197 (MutationObj.coerce(self._key, v) for v in values))
197 (MutationObj.coerce(self._key, v) for v in values))
198 self.changed()
198 self.changed()
199
199
200 def __delitem__(self, idx):
200 def __delitem__(self, idx):
201 list.__delitem__(self, idx)
201 list.__delitem__(self, idx)
202 self.changed()
202 self.changed()
203
203
204 def __delslice__(self, start, stop):
204 def __delslice__(self, start, stop):
205 list.__delslice__(self, start, stop)
205 list.__delslice__(self, start, stop)
206 self.changed()
206 self.changed()
207
207
208 def append(self, value):
208 def append(self, value):
209 list.append(self, MutationObj.coerce(self._key, value))
209 list.append(self, MutationObj.coerce(self._key, value))
210 self.changed()
210 self.changed()
211
211
212 def insert(self, idx, value):
212 def insert(self, idx, value):
213 list.insert(self, idx, MutationObj.coerce(self._key, value))
213 list.insert(self, idx, MutationObj.coerce(self._key, value))
214 self.changed()
214 self.changed()
215
215
216 def extend(self, values):
216 def extend(self, values):
217 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))
218 self.changed()
218 self.changed()
219
219
220 def pop(self, *args, **kw):
220 def pop(self, *args, **kw):
221 value = list.pop(self, *args, **kw)
221 value = list.pop(self, *args, **kw)
222 self.changed()
222 self.changed()
223 return value
223 return value
224
224
225 def remove(self, value):
225 def remove(self, value):
226 list.remove(self, value)
226 list.remove(self, value)
227 self.changed()
227 self.changed()
228
228
229
229
230 def JsonType(impl=None, **kwargs):
230 def JsonType(impl=None, **kwargs):
231 """
231 """
232 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.
233 example::
233 example::
234
234
235 settings = Column('settings_json',
235 settings = Column('settings_json',
236 MutationObj.as_mutable(
236 MutationObj.as_mutable(
237 JsonType(dialect_map=dict(mysql=UnicodeText(16384))))
237 JsonType(dialect_map=dict(mysql=UnicodeText(16384))))
238 """
238 """
239
239
240 if impl == 'list':
240 if impl == 'list':
241 return JSONEncodedObj(default=list, **kwargs)
241 return JSONEncodedObj(default=list, **kwargs)
242 elif impl == 'dict':
242 elif impl == 'dict':
243 return JSONEncodedObj(default=DictClass, **kwargs)
243 return JSONEncodedObj(default=DictClass, **kwargs)
244 else:
244 else:
245 return JSONEncodedObj(**kwargs)
245 return JSONEncodedObj(**kwargs)
246
246
247
247
248 JSON = MutationObj.as_mutable(JsonType())
248 JSON = MutationObj.as_mutable(JsonType())
249 """
249 """
250 A type to encode/decode JSON on the fly
250 A type to encode/decode JSON on the fly
251
251
252 sqltype is the string type for the underlying DB column::
252 sqltype is the string type for the underlying DB column::
253
253
254 Column(JSON) (defaults to UnicodeText)
254 Column(JSON) (defaults to UnicodeText)
255 """
255 """
256
256
257 JSONDict = MutationObj.as_mutable(JsonType('dict'))
257 JSONDict = MutationObj.as_mutable(JsonType('dict'))
258 """
258 """
259 A type to encode/decode JSON dictionaries on the fly
259 A type to encode/decode JSON dictionaries on the fly
260 """
260 """
261
261
262 JSONList = MutationObj.as_mutable(JsonType('list'))
262 JSONList = MutationObj.as_mutable(JsonType('list'))
263 """
263 """
264 A type to encode/decode JSON lists` on the fly
264 A type to encode/decode JSON lists` on the fly
265 """
265 """
General Comments 0
You need to be logged in to leave comments. Login now