##// END OF EJS Templates
db: enforce unicode on unicode columns to prevent warnings.
marcink -
r3948:e0de097c default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

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