Merge lp:~thisfred/desktopcouch/remove-recordtype-from-dict into lp:desktopcouch
- remove-recordtype-from-dict
- Merge into trunk
Status: | Merged | ||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Approved by: | Vincenzo Di Somma | ||||||||||||||||
Approved revision: | 218 | ||||||||||||||||
Merged at revision: | 207 | ||||||||||||||||
Proposed branch: | lp:~thisfred/desktopcouch/remove-recordtype-from-dict | ||||||||||||||||
Merge into: | lp:desktopcouch | ||||||||||||||||
Diff against target: |
444 lines (+116/-69) 6 files modified
desktopcouch/contacts/tests/test_record.py (+2/-3) desktopcouch/records/doc/an_example_application.txt (+1/-1) desktopcouch/records/record.py (+93/-58) desktopcouch/records/tests/test_record.py (+17/-5) desktopcouch/records/tests/test_server.py (+2/-1) utilities/lint.sh (+1/-1) |
||||||||||||||||
To merge this branch: | bzr merge lp:~thisfred/desktopcouch/remove-recordtype-from-dict | ||||||||||||||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Vincenzo Di Somma (community) | Approve | ||
Nicola Larosa (community) | Approve | ||
Review via email: mp+40916@code.launchpad.net |
Commit message
Fixes #674487 where the record_type ended up in the dictionary of the record, where almost all client code had to filter it out again.
Fixes #669133 because the record_type version was never saved to CouchDB.
Fixes #575772 where the couchdb attachments API was defined on the wrong class.
Fixes #675787 where MergeableList.pop() could not be called without an index.
Description of the change
Fixed too much on this branch because I got a little frustrated with the number of bugs I ran into. If it's impossible to review, please tell me and I'll try and split all the fixes out into their own branches...
Eric Casteleijn (thisfred) wrote : | # |
Eric Casteleijn (thisfred) wrote : | # |
Note that the frustration is mostly aimed at myself, since this shows that I suck at reviewing and writing code, since I did at least one of those for all of the code that was buggy.
Nicola Larosa (teknico) wrote : | # |
I like this branch. A couple comments:
- since you don't need current_value anymore here:
- for index, current_value in enumerate(self):
+ for index, _ in enumerate(self):
you might as well write:
for index in range(len(self)):
Also, what's with the double blank lines before or after functions and methods? You only need double between classes. Don't get overboard with that automatic pep8 thing. ;-)
Eric Casteleijn (thisfred) wrote : | # |
> - since you don't need current_value anymore here:
>
> - for index, current_value in enumerate(self):
> + for index, _ in enumerate(self):
>
> you might as well write:
>
> for index in range(len(self)):
Yeah that's better
> Also, what's with the double blank lines before or after functions and
> methods? You only need double between classes. Don't get overboard with that
> automatic pep8 thing. ;-)
Actually I like having two lines between top level definitions no matter what they are (so functions or Classes, *not* methods.) It makes me have to think less, and improves readability. Also a top level function and a class definition are rather similar things IMO.
- 218. By Eric Casteleijn
-
microbeautification
Vincenzo Di Somma (vds) : | # |
Preview Diff
1 | === modified file 'desktopcouch/contacts/tests/test_record.py' | |||
2 | --- desktopcouch/contacts/tests/test_record.py 2010-10-31 23:20:14 +0000 | |||
3 | +++ desktopcouch/contacts/tests/test_record.py 2010-11-15 23:22:02 +0000 | |||
4 | @@ -46,6 +46,5 @@ | |||
5 | 46 | first_name = 'manuel' | 46 | first_name = 'manuel' |
6 | 47 | contact.first_name = first_name | 47 | contact.first_name = first_name |
7 | 48 | self.assertEqual(first_name, contact.first_name) | 48 | self.assertEqual(first_name, contact.first_name) |
11 | 49 | self.assertEqual(set(['record_type', 'first_name']), | 49 | self.assertEqual(set(['first_name']), set(contact.keys())) |
12 | 50 | set(contact.keys())) | 50 | |
10 | 51 | |||
13 | 52 | 51 | ||
14 | === modified file 'desktopcouch/records/doc/an_example_application.txt' | |||
15 | --- desktopcouch/records/doc/an_example_application.txt 2010-11-03 22:41:43 +0000 | |||
16 | +++ desktopcouch/records/doc/an_example_application.txt 2010-11-15 23:22:02 +0000 | |||
17 | @@ -116,7 +116,7 @@ | |||
18 | 116 | kinds of dynamic coding easier: | 116 | kinds of dynamic coding easier: |
19 | 117 | 117 | ||
20 | 118 | >>> sorted([key for key in my_recipe]) | 118 | >>> sorted([key for key in my_recipe]) |
22 | 119 | ['ingredients', 'name', 'record_type'] | 119 | ['ingredients', 'name'] |
23 | 120 | 120 | ||
24 | 121 | There are more specific types of RecordField available if you want to | 121 | There are more specific types of RecordField available if you want to |
25 | 122 | encourage that the value of one of the fields is of a certain | 122 | encourage that the value of one of the fields is of a certain |
26 | 123 | 123 | ||
27 | === modified file 'desktopcouch/records/record.py' | |||
28 | --- desktopcouch/records/record.py 2010-11-03 22:41:43 +0000 | |||
29 | +++ desktopcouch/records/record.py 2010-11-15 23:22:02 +0000 | |||
30 | @@ -27,9 +27,13 @@ | |||
31 | 27 | 27 | ||
32 | 28 | RX = re.compile('[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}') | 28 | RX = re.compile('[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}') |
33 | 29 | 29 | ||
34 | 30 | |||
35 | 30 | def is_internal(key): | 31 | def is_internal(key): |
36 | 31 | """Test whether this is an internal key.""" | 32 | """Test whether this is an internal key.""" |
38 | 32 | return key.startswith(('_', 'application_annotations')) | 33 | return ( |
39 | 34 | key == 'record_type' or | ||
40 | 35 | key.startswith(('_', 'application_annotations'))) | ||
41 | 36 | |||
42 | 33 | 37 | ||
43 | 34 | def is_uuid_dict(dictionary): | 38 | def is_uuid_dict(dictionary): |
44 | 35 | """Test whether the passed object is a dictionary like object with | 39 | """Test whether the passed object is a dictionary like object with |
45 | @@ -38,12 +42,14 @@ | |||
46 | 38 | return dictionary and not [ | 42 | return dictionary and not [ |
47 | 39 | key for key in dictionary if (not RX.match(key) and key != '_order')] | 43 | key for key in dictionary if (not RX.match(key) and key != '_order')] |
48 | 40 | 44 | ||
49 | 45 | |||
50 | 41 | def record_factory(data): | 46 | def record_factory(data): |
51 | 42 | """Create a RecordDict or RecordList from passed data.""" | 47 | """Create a RecordDict or RecordList from passed data.""" |
52 | 43 | if isinstance(data, dict): | 48 | if isinstance(data, dict): |
53 | 44 | return _build_from_dict(data) | 49 | return _build_from_dict(data) |
54 | 45 | return data | 50 | return data |
55 | 46 | 51 | ||
56 | 52 | |||
57 | 47 | def _build_from_dict(data): | 53 | def _build_from_dict(data): |
58 | 48 | """Create a RecordDict from passed data.""" | 54 | """Create a RecordDict from passed data.""" |
59 | 49 | result = RecordDict({}) | 55 | result = RecordDict({}) |
60 | @@ -54,6 +60,7 @@ | |||
61 | 54 | result[key] = record_factory(value) | 60 | result[key] = record_factory(value) |
62 | 55 | return result | 61 | return result |
63 | 56 | 62 | ||
64 | 63 | |||
65 | 57 | def validate(data): | 64 | def validate(data): |
66 | 58 | """Validate underlying data for Record objects.""" | 65 | """Validate underlying data for Record objects.""" |
67 | 59 | if isinstance(data, dict): | 66 | if isinstance(data, dict): |
68 | @@ -73,7 +80,7 @@ | |||
69 | 73 | def __init__(self, field_name): | 80 | def __init__(self, field_name): |
70 | 74 | self.field_name = field_name | 81 | self.field_name = field_name |
71 | 75 | 82 | ||
73 | 76 | def __get__(self, obj, type=None): | 83 | def __get__(self, obj, owner=None): |
74 | 77 | return obj.get(self.field_name) | 84 | return obj.get(self.field_name) |
75 | 78 | 85 | ||
76 | 79 | def __set__(self, obj, value): | 86 | def __set__(self, obj, value): |
77 | @@ -109,6 +116,7 @@ | |||
78 | 109 | """Exception when attempting to set a key we don't allow.""" | 116 | """Exception when attempting to set a key we don't allow.""" |
79 | 110 | pass | 117 | pass |
80 | 111 | 118 | ||
81 | 119 | |||
82 | 112 | class NoRecordTypeSpecified(Exception): | 120 | class NoRecordTypeSpecified(Exception): |
83 | 113 | """Exception when creating a record without specifying record type""" | 121 | """Exception when creating a record without specifying record type""" |
84 | 114 | def __str__(self): | 122 | def __str__(self): |
85 | @@ -118,6 +126,7 @@ | |||
86 | 118 | class Attachment(object): | 126 | class Attachment(object): |
87 | 119 | """This represents the value of the attachment. The name is | 127 | """This represents the value of the attachment. The name is |
88 | 120 | stored as the key that points to this name.""" | 128 | stored as the key that points to this name.""" |
89 | 129 | |||
90 | 121 | def __init__(self, content=None, content_type=None): | 130 | def __init__(self, content=None, content_type=None): |
91 | 122 | super(Attachment, self).__init__() | 131 | super(Attachment, self).__init__() |
92 | 123 | if content is not None: | 132 | if content is not None: |
93 | @@ -132,7 +141,8 @@ | |||
94 | 132 | self.content_type = content_type | 141 | self.content_type = content_type |
95 | 133 | self.needs_synch = content is not None | 142 | self.needs_synch = content is not None |
96 | 134 | 143 | ||
98 | 135 | def get_content_and_type(self): | 144 | def get_content_and_type(self): # WTF? pylint: disable=E0202 |
99 | 145 | """Get content and type of the attachment.""" | ||
100 | 136 | assert self.content is not None | 146 | assert self.content is not None |
101 | 137 | if hasattr(self.content, "read"): | 147 | if hasattr(self.content, "read"): |
102 | 138 | if hasattr(self.content, "seek"): | 148 | if hasattr(self.content, "seek"): |
103 | @@ -165,7 +175,7 @@ | |||
104 | 165 | if isinstance(item, dict): | 175 | if isinstance(item, dict): |
105 | 166 | item = record_factory(item) | 176 | item = record_factory(item) |
106 | 167 | if hasattr(item, '_data'): | 177 | if hasattr(item, '_data'): |
108 | 168 | item = item._data | 178 | item = item._data # pylint: disable=W0212 |
109 | 169 | self._data[key] = item | 179 | self._data[key] = item |
110 | 170 | 180 | ||
111 | 171 | def __delitem__(self, key): | 181 | def __delitem__(self, key): |
112 | @@ -174,44 +184,11 @@ | |||
113 | 174 | def __len__(self): | 184 | def __len__(self): |
114 | 175 | return len(self._data) | 185 | return len(self._data) |
115 | 176 | 186 | ||
116 | 177 | def attach_by_reference(self, filename, getter_function): | ||
117 | 178 | """This should only be called by the server code to refer to a BLOB | ||
118 | 179 | that is in the database, so that we do not have to download it until | ||
119 | 180 | the user wants it.""" | ||
120 | 181 | a = Attachment(None, None) | ||
121 | 182 | a.get_content_and_type = getter_function | ||
122 | 183 | self._attachments[filename] = a | ||
123 | 184 | |||
124 | 185 | def attach(self, content, filename, content_type): | ||
125 | 186 | """Attach a file-like or string-like object, with a particular name and | ||
126 | 187 | content type, to a document to be stored in the database. One can not | ||
127 | 188 | clobber names that already exist.""" | ||
128 | 189 | if filename in self._attachments: | ||
129 | 190 | raise KeyError("%r already exists" % (filename,)) | ||
130 | 191 | self._attachments[filename] = Attachment(content, content_type) | ||
131 | 192 | |||
132 | 193 | def detach(self, filename): | ||
133 | 194 | """Remove a BLOB attached to a document.""" | ||
134 | 195 | try: | ||
135 | 196 | self._attachments.pop(filename) | ||
136 | 197 | self._detached.add(filename) | ||
137 | 198 | except KeyError: | ||
138 | 199 | raise KeyError("%r is not attached to this document" % (filename,)) | ||
139 | 200 | |||
140 | 201 | def list_attachments(self): | ||
141 | 202 | return self._attachments.keys() | ||
142 | 203 | |||
143 | 204 | def attachment_data(self, filename): | ||
144 | 205 | """Retreive the attached data, the BLOB and content_type.""" | ||
145 | 206 | try: | ||
146 | 207 | a = self._attachments[filename] | ||
147 | 208 | except KeyError: | ||
148 | 209 | raise KeyError("%r is not attached to this document" % (filename,)) | ||
149 | 210 | return a.get_content_and_type() | ||
150 | 211 | 187 | ||
151 | 212 | class RecordDict(RecordData): | 188 | class RecordDict(RecordData): |
152 | 213 | """An object that represents an desktop CouchDB record fragment, | 189 | """An object that represents an desktop CouchDB record fragment, |
153 | 214 | but behaves like a dictionary. | 190 | but behaves like a dictionary. |
154 | 191 | |||
155 | 215 | """ | 192 | """ |
156 | 216 | 193 | ||
157 | 217 | def __setitem__(self, key, item): | 194 | def __setitem__(self, key, item): |
158 | @@ -229,48 +206,54 @@ | |||
159 | 229 | 206 | ||
160 | 230 | def __cmp__(self, value): | 207 | def __cmp__(self, value): |
161 | 231 | if isinstance(value, RecordDict): | 208 | if isinstance(value, RecordDict): |
163 | 232 | return cmp(self._data, value._data) | 209 | return cmp(self._data, value._data) # pylint: disable=W0212 |
164 | 233 | return -1 | 210 | return -1 |
165 | 234 | 211 | ||
166 | 235 | def get(self, key, default=None): | 212 | def get(self, key, default=None): |
168 | 236 | """Override get method.""" | 213 | """D.get(k[,d]) -> D[k] if k in D, else d. d defaults to None.""" |
169 | 237 | if not key in self._data: | 214 | if not key in self._data: |
170 | 238 | return default | 215 | return default |
171 | 239 | return self[key] | 216 | return self[key] |
172 | 240 | 217 | ||
173 | 241 | def update(self, data): | 218 | def update(self, data): |
175 | 242 | """Set items from a dict""" | 219 | """D.update(E, **F) -> None. Update D from dict/iterable E and F. |
176 | 220 | |||
177 | 221 | If E has a .keys() method, does: for k in E: D[k] = E[k] | ||
178 | 222 | If E lacks .keys() method, does: for (k, v) in E: D[k] = v | ||
179 | 223 | In either case, this is followed by: for k in F: D[k] = F[k] | ||
180 | 224 | |||
181 | 225 | """ | ||
182 | 243 | for key, val in data.items(): | 226 | for key, val in data.items(): |
183 | 244 | self[key] = val | 227 | self[key] = val |
184 | 245 | 228 | ||
185 | 246 | def items(self): | 229 | def items(self): |
187 | 247 | """Return all items.""" | 230 | """D.items() -> list of D's (key, value) pairs, as 2-tuples.""" |
188 | 248 | return [(key, self[key]) for key in self] | 231 | return [(key, self[key]) for key in self] |
189 | 249 | 232 | ||
190 | 250 | def keys(self): | 233 | def keys(self): |
192 | 251 | """Return all keys.""" | 234 | """D.keys() -> list of D's keys.""" |
193 | 252 | return self._data.keys() | 235 | return self._data.keys() |
194 | 253 | 236 | ||
195 | 254 | def setdefault(self, key, default): | 237 | def setdefault(self, key, default): |
197 | 255 | """remap setdefault""" | 238 | """D.setdefault(k[,d]) -> D.get(k,d), also set D[k]=d if k not in D.""" |
198 | 256 | if not key in self: | 239 | if not key in self: |
199 | 257 | self[key] = default | 240 | self[key] = default |
200 | 258 | return self[key] | 241 | return self[key] |
201 | 259 | 242 | ||
202 | 260 | def has_key(self, key): | 243 | def has_key(self, key): |
203 | 244 | """D.has_key(k) -> True if D has a key k, else False.""" | ||
204 | 261 | return key in self | 245 | return key in self |
205 | 262 | 246 | ||
206 | 263 | 247 | ||
207 | 264 | |||
208 | 265 | class MergeableList(RecordData): | 248 | class MergeableList(RecordData): |
209 | 266 | """An object that represents a list of complex values.""" | 249 | """An object that represents a list of complex values.""" |
210 | 267 | 250 | ||
211 | 268 | @classmethod | 251 | @classmethod |
213 | 269 | def from_list(klass, data): | 252 | def from_list(cls, data): |
214 | 270 | """Create a RecordList from passed data.""" | 253 | """Create a RecordList from passed data.""" |
215 | 271 | if not data: | 254 | if not data: |
216 | 272 | raise ValueError("Can't set empty list values.'") | 255 | raise ValueError("Can't set empty list values.'") |
218 | 273 | result = klass({}) | 256 | result = cls({}) |
219 | 274 | for value in data: | 257 | for value in data: |
220 | 275 | result.append(record_factory(value)) | 258 | result.append(record_factory(value)) |
221 | 276 | return result | 259 | return result |
222 | @@ -309,7 +292,7 @@ | |||
223 | 309 | 292 | ||
224 | 310 | def __contains__(self, value): | 293 | def __contains__(self, value): |
225 | 311 | if hasattr(value, '_data'): | 294 | if hasattr(value, '_data'): |
227 | 312 | value = value._data | 295 | value = value._data # pylint: disable=W0212 |
228 | 313 | return value in self._data.values() | 296 | return value in self._data.values() |
229 | 314 | 297 | ||
230 | 315 | def __cmp__(self, value): | 298 | def __cmp__(self, value): |
231 | @@ -321,9 +304,9 @@ | |||
232 | 321 | and hasattr(value, "__getitem__") | 304 | and hasattr(value, "__getitem__") |
233 | 322 | and hasattr(value, "__len__")): | 305 | and hasattr(value, "__len__")): |
234 | 323 | # we should have the same value in the same order | 306 | # we should have the same value in the same order |
236 | 324 | length = len(value) | 307 | length = len(value) |
237 | 325 | if len(self) == length: | 308 | if len(self) == length: |
239 | 326 | for index, current_value in enumerate(self): | 309 | for index in range(len(self)): |
240 | 327 | cmp_value = cmp(self[index], value[index]) | 310 | cmp_value = cmp(self[index], value[index]) |
241 | 328 | if cmp_value != 0: | 311 | if cmp_value != 0: |
242 | 329 | return cmp_value | 312 | return cmp_value |
243 | @@ -345,20 +328,25 @@ | |||
244 | 345 | """Return uuid key for index.""" | 328 | """Return uuid key for index.""" |
245 | 346 | return self._get_ordered_keys()[index] | 329 | return self._get_ordered_keys()[index] |
246 | 347 | 330 | ||
248 | 348 | def get_value_for_uuid(self, uuid): | 331 | def get_value_for_uuid(self, lookup_uuid): |
249 | 349 | """Allow API consumers that do know about the UUIDs to get | 332 | """Allow API consumers that do know about the UUIDs to get |
250 | 350 | values directly. | 333 | values directly. |
251 | 351 | """ | 334 | """ |
253 | 352 | return super(MergeableList, self).__getitem__(uuid) | 335 | return super(MergeableList, self).__getitem__(lookup_uuid) |
254 | 353 | 336 | ||
255 | 354 | def append(self, value): | 337 | def append(self, value): |
257 | 355 | """Append a value to end of MergeableList.""" | 338 | """L.append(object) -- append object to end.""" |
258 | 356 | new_uuid = str(uuid.uuid4()) | 339 | new_uuid = str(uuid.uuid4()) |
259 | 357 | self._data.setdefault('_order', sorted(self._data.keys())).append( | 340 | self._data.setdefault('_order', sorted(self._data.keys())).append( |
260 | 358 | new_uuid) | 341 | new_uuid) |
261 | 359 | super(MergeableList, self).__setitem__(new_uuid, value) | 342 | super(MergeableList, self).__setitem__(new_uuid, value) |
262 | 360 | 343 | ||
263 | 361 | def remove(self, value): | 344 | def remove(self, value): |
264 | 345 | """L.remove(value) -- remove first occurrence of value. | ||
265 | 346 | |||
266 | 347 | Raises ValueError if the value is not present. | ||
267 | 348 | |||
268 | 349 | """ | ||
269 | 362 | if len(self) == 1: | 350 | if len(self) == 1: |
270 | 363 | raise ValueError("MergeableList cannot be empty.") | 351 | raise ValueError("MergeableList cannot be empty.") |
271 | 364 | index = 0 | 352 | index = 0 |
272 | @@ -371,15 +359,26 @@ | |||
273 | 371 | index += 1 | 359 | index += 1 |
274 | 372 | raise ValueError("list.remove(x): x not in list") | 360 | raise ValueError("list.remove(x): x not in list") |
275 | 373 | 361 | ||
277 | 374 | def pop(self, index): | 362 | def pop(self, index=None): |
278 | 363 | """L.pop([index]) -> item -- remove and return item at | ||
279 | 364 | index. (default last.) | ||
280 | 365 | |||
281 | 366 | Raises IndexError if list is empty or index is out of range. | ||
282 | 367 | """ | ||
283 | 375 | if len(self) == 1: | 368 | if len(self) == 1: |
284 | 376 | raise ValueError("MergeableList cannot be empty.") | 369 | raise ValueError("MergeableList cannot be empty.") |
285 | 370 | if index is None: | ||
286 | 371 | index = -1 | ||
287 | 377 | value = self[index] | 372 | value = self[index] |
288 | 378 | del self[index] | 373 | del self[index] |
289 | 379 | return value | 374 | return value |
290 | 380 | 375 | ||
291 | 381 | def index(self, key): | 376 | def index(self, key): |
293 | 382 | """Get value by index.""" | 377 | """L.index(value, [start, [stop]]) -> integer -- return first |
294 | 378 | index of value. | ||
295 | 379 | |||
296 | 380 | Raises ValueError if the value is not present. | ||
297 | 381 | """ | ||
298 | 383 | return self.__getitem__(key) | 382 | return self.__getitem__(key) |
299 | 384 | 383 | ||
300 | 385 | 384 | ||
301 | @@ -397,6 +396,8 @@ | |||
302 | 397 | raise NoRecordTypeSpecified | 396 | raise NoRecordTypeSpecified |
303 | 398 | super(Record, self).__init__(data) | 397 | super(Record, self).__init__(data) |
304 | 399 | self._data['record_type'] = record_type | 398 | self._data['record_type'] = record_type |
305 | 399 | if record_type_version is not None: | ||
306 | 400 | self._data['_record_type_version'] = record_type_version | ||
307 | 400 | 401 | ||
308 | 401 | if record_id is not None: | 402 | if record_id is not None: |
309 | 402 | # Explicit setting, so try to use it. | 403 | # Explicit setting, so try to use it. |
310 | @@ -407,8 +408,6 @@ | |||
311 | 407 | # _id already set. Verify that the explicit value agrees. | 408 | # _id already set. Verify that the explicit value agrees. |
312 | 408 | if self._data['_id'] != record_id: | 409 | if self._data['_id'] != record_id: |
313 | 409 | raise ValueError("record_id doesn't match value in data.") | 410 | raise ValueError("record_id doesn't match value in data.") |
314 | 410 | if record_type_version: | ||
315 | 411 | self._record_type_version = record_type_version | ||
316 | 412 | 411 | ||
317 | 413 | def __getitem__(self, key): | 412 | def __getitem__(self, key): |
318 | 414 | if is_internal(key): | 413 | if is_internal(key): |
319 | @@ -481,4 +480,40 @@ | |||
320 | 481 | @property | 480 | @property |
321 | 482 | def record_type_version(self): | 481 | def record_type_version(self): |
322 | 483 | """Get the record type version if it's there.""" | 482 | """Get the record type version if it's there.""" |
324 | 484 | return getattr(self, '_record_type_version', None) | 483 | return self._data.get('_record_type_version', None) |
325 | 484 | |||
326 | 485 | def attach_by_reference(self, filename, getter_function): | ||
327 | 486 | """This should only be called by the server code to refer to a BLOB | ||
328 | 487 | that is in the database, so that we do not have to download it until | ||
329 | 488 | the user wants it.""" | ||
330 | 489 | a = Attachment(None, None) | ||
331 | 490 | a.get_content_and_type = getter_function | ||
332 | 491 | self._attachments[filename] = a | ||
333 | 492 | |||
334 | 493 | def attach(self, content, filename, content_type): | ||
335 | 494 | """Attach a file-like or string-like object, with a particular name and | ||
336 | 495 | content type, to a document to be stored in the database. One can not | ||
337 | 496 | clobber names that already exist.""" | ||
338 | 497 | if filename in self._attachments: | ||
339 | 498 | raise KeyError("%r already exists" % (filename,)) | ||
340 | 499 | self._attachments[filename] = Attachment(content, content_type) | ||
341 | 500 | |||
342 | 501 | def detach(self, filename): | ||
343 | 502 | """Remove a BLOB attached to a document.""" | ||
344 | 503 | try: | ||
345 | 504 | self._attachments.pop(filename) | ||
346 | 505 | self._detached.add(filename) | ||
347 | 506 | except KeyError: | ||
348 | 507 | raise KeyError("%r is not attached to this document" % (filename,)) | ||
349 | 508 | |||
350 | 509 | def list_attachments(self): | ||
351 | 510 | """List the record's attachments.""" | ||
352 | 511 | return self._attachments.keys() | ||
353 | 512 | |||
354 | 513 | def attachment_data(self, filename): | ||
355 | 514 | """Retreive the attached data, the BLOB and content_type.""" | ||
356 | 515 | try: | ||
357 | 516 | a = self._attachments[filename] | ||
358 | 517 | except KeyError: | ||
359 | 518 | raise KeyError("%r is not attached to this document" % (filename,)) | ||
360 | 519 | return a.get_content_and_type() | ||
361 | 485 | 520 | ||
362 | === modified file 'desktopcouch/records/tests/test_record.py' | |||
363 | --- desktopcouch/records/tests/test_record.py 2010-11-12 14:34:21 +0000 | |||
364 | +++ desktopcouch/records/tests/test_record.py 2010-11-15 23:22:02 +0000 | |||
365 | @@ -98,7 +98,7 @@ | |||
366 | 98 | def test_iter(self): | 98 | def test_iter(self): |
367 | 99 | """Tests it is possible to iterate over the record fields""" | 99 | """Tests it is possible to iterate over the record fields""" |
368 | 100 | self.assertEquals(sorted(list(iter(self.record))), | 100 | self.assertEquals(sorted(list(iter(self.record))), |
370 | 101 | ['a', 'b', 'record_type', 'subfield', 'subfield_uuid']) | 101 | ['a', 'b', 'subfield', 'subfield_uuid']) |
371 | 102 | 102 | ||
372 | 103 | def test_setitem_internal(self): | 103 | def test_setitem_internal(self): |
373 | 104 | """Test it is not possible to set a private field on a record""" | 104 | """Test it is not possible to set a private field on a record""" |
374 | @@ -125,7 +125,7 @@ | |||
375 | 125 | def test_keys(self): | 125 | def test_keys(self): |
376 | 126 | """Test getting the keys.""" | 126 | """Test getting the keys.""" |
377 | 127 | self.assertEqual( | 127 | self.assertEqual( |
379 | 128 | ['a', 'b', 'record_type', 'subfield', 'subfield_uuid'], | 128 | ['a', 'b', 'subfield', 'subfield_uuid'], |
380 | 129 | sorted(self.record.keys())) | 129 | sorted(self.record.keys())) |
381 | 130 | self.assertIn("a", self.record) | 130 | self.assertIn("a", self.record) |
382 | 131 | self.assertNotIn('f', self.record) | 131 | self.assertNotIn('f', self.record) |
383 | @@ -248,10 +248,18 @@ | |||
384 | 248 | def test_mergeable_list_pop_wrong_index(self): | 248 | def test_mergeable_list_pop_wrong_index(self): |
385 | 249 | """Test pop when index is out or range.""" | 249 | """Test pop when index is out or range.""" |
386 | 250 | value = [1, 2, 3, 4, 5] | 250 | value = [1, 2, 3, 4, 5] |
388 | 251 | self.record["subfield_uuid"] = value | 251 | self.record["subfield_uuid"] = MergeableList.from_list(value) |
389 | 252 | self.assertRaises(IndexError, self.record["subfield_uuid"].pop, | 252 | self.assertRaises(IndexError, self.record["subfield_uuid"].pop, |
390 | 253 | len(value) * 2) | 253 | len(value) * 2) |
391 | 254 | 254 | ||
392 | 255 | def test_mergeable_list_pop_no_index(self): | ||
393 | 256 | """Test pop without an index argument.""" | ||
394 | 257 | value = [1, 2, 3, 4, 5] | ||
395 | 258 | self.record["subfield_uuid"] = MergeableList.from_list(value) | ||
396 | 259 | popped_value = self.record["subfield_uuid"].pop() | ||
397 | 260 | self.assertEqual(value[-1], popped_value) | ||
398 | 261 | |||
399 | 262 | |||
400 | 255 | def test_mergeable_list_pop_last(self): | 263 | def test_mergeable_list_pop_last(self): |
401 | 256 | """Test that exception is raised when poping last item""" | 264 | """Test that exception is raised when poping last item""" |
402 | 257 | self.record["subfield_uuid"] = MergeableList.from_list([1]) | 265 | self.record["subfield_uuid"] = MergeableList.from_list([1]) |
403 | @@ -353,9 +361,13 @@ | |||
404 | 353 | """Test record type version support""" | 361 | """Test record type version support""" |
405 | 354 | data = {"_id": "recordid"} | 362 | data = {"_id": "recordid"} |
406 | 355 | record1 = Record(data, record_type="url") | 363 | record1 = Record(data, record_type="url") |
407 | 364 | self.assertIs(None, record1._data.get( # pylint: disable=W0212 | ||
408 | 365 | '_record_type_version')) | ||
409 | 356 | self.assertIs(None, record1.record_type_version) | 366 | self.assertIs(None, record1.record_type_version) |
412 | 357 | record2 = Record(data, record_type="url", record_type_version=1) | 367 | record2 = Record(data, record_type="url", record_type_version='1.0') |
413 | 358 | self.assertEqual(record2.record_type_version, 1) | 368 | self.assertEqual(record2._data[ # pylint: disable=W0212 |
414 | 369 | '_record_type_version'], '1.0') | ||
415 | 370 | self.assertEqual(record2.record_type_version, '1.0') | ||
416 | 359 | 371 | ||
417 | 360 | 372 | ||
418 | 361 | class TestRecordFactory(TestCase): | 373 | class TestRecordFactory(TestCase): |
419 | 362 | 374 | ||
420 | === modified file 'desktopcouch/records/tests/test_server.py' | |||
421 | --- desktopcouch/records/tests/test_server.py 2010-11-10 21:17:11 +0000 | |||
422 | +++ desktopcouch/records/tests/test_server.py 2010-11-15 23:22:02 +0000 | |||
423 | @@ -385,7 +385,8 @@ | |||
424 | 385 | results = self.database.get_all_records() | 385 | results = self.database.get_all_records() |
425 | 386 | 386 | ||
426 | 387 | self.assertTrue(8 <= len(results)) # our 8, plus callers' data | 387 | self.assertTrue(8 <= len(results)) # our 8, plus callers' data |
428 | 388 | self.assertIn("record_type", results[0]) | 388 | self.assertNotIn("record_type", results[0]) |
429 | 389 | self.assertTrue(hasattr(results[0], "record_type")) | ||
430 | 389 | 390 | ||
431 | 390 | for result in results: # index notation | 391 | for result in results: # index notation |
432 | 391 | if result.record_type == good_record_type: | 392 | if result.record_type == good_record_type: |
433 | 392 | 393 | ||
434 | === modified file 'utilities/lint.sh' | |||
435 | --- utilities/lint.sh 2010-11-12 14:34:21 +0000 | |||
436 | +++ utilities/lint.sh 2010-11-15 23:22:02 +0000 | |||
437 | @@ -37,7 +37,7 @@ | |||
438 | 37 | fi | 37 | fi |
439 | 38 | 38 | ||
440 | 39 | export PYTHONPATH="/usr/share/pycentral/pylint/site-packages:desktopcouch:$PYTHONPATH" | 39 | export PYTHONPATH="/usr/share/pycentral/pylint/site-packages:desktopcouch:$PYTHONPATH" |
442 | 40 | pylint="`which pylint` -r n -i y --variable-rgx=[a-z_][a-z0-9_]{0,30} --attr-rgx=[a-z_][a-z0-9_]{1,30} --method-rgx=[a-z_][a-z0-9_]{2,} -d I0011,R0902,R0904,R0913,W0142" | 40 | pylint="`which pylint` -r n -i y --variable-rgx=[a-z_][a-z0-9_]{0,30} --attr-rgx=[a-z_][a-z0-9_]{1,30} --method-rgx=[a-z_][a-z0-9_]{2,} -d I0011,R0902,R0903,R0904,R0913,W0142" |
443 | 41 | 41 | ||
444 | 42 | pylint_notices=`$pylint $pyfiles` | 42 | pylint_notices=`$pylint $pyfiles` |
445 | 43 | 43 |
Fixes #674487 where the record_type ended up in the dictionary of the record, where almost all client code had to filter it out again.
Fixes #669133 because the record_type version was never saved to CouchDB.
Fixes #575772 where the couchdb attachments API was defined on the wrong class.
Fixes #675787 where MergeableList.pop() could not be called without an index.