Merge lp:~thisfred/desktopcouch/remove-recordtype-from-dict into lp:desktopcouch

Proposed by Eric Casteleijn
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
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...

To post a comment you must log in.
Revision history for this message
Eric Casteleijn (thisfred) wrote :

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.

Revision history for this message
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.

Revision history for this message
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. ;-)

review: Approve
Revision history for this message
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

Revision history for this message
Vincenzo Di Somma (vds) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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 first_name = 'manuel'
6 contact.first_name = first_name
7 self.assertEqual(first_name, contact.first_name)
8- self.assertEqual(set(['record_type', 'first_name']),
9- set(contact.keys()))
10-
11+ self.assertEqual(set(['first_name']), set(contact.keys()))
12+
13
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 kinds of dynamic coding easier:
19
20 >>> sorted([key for key in my_recipe])
21- ['ingredients', 'name', 'record_type']
22+ ['ingredients', 'name']
23
24 There are more specific types of RecordField available if you want to
25 encourage that the value of one of the fields is of a certain
26
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
32 RX = re.compile('[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}')
33
34+
35 def is_internal(key):
36 """Test whether this is an internal key."""
37- return key.startswith(('_', 'application_annotations'))
38+ return (
39+ key == 'record_type' or
40+ key.startswith(('_', 'application_annotations')))
41+
42
43 def is_uuid_dict(dictionary):
44 """Test whether the passed object is a dictionary like object with
45@@ -38,12 +42,14 @@
46 return dictionary and not [
47 key for key in dictionary if (not RX.match(key) and key != '_order')]
48
49+
50 def record_factory(data):
51 """Create a RecordDict or RecordList from passed data."""
52 if isinstance(data, dict):
53 return _build_from_dict(data)
54 return data
55
56+
57 def _build_from_dict(data):
58 """Create a RecordDict from passed data."""
59 result = RecordDict({})
60@@ -54,6 +60,7 @@
61 result[key] = record_factory(value)
62 return result
63
64+
65 def validate(data):
66 """Validate underlying data for Record objects."""
67 if isinstance(data, dict):
68@@ -73,7 +80,7 @@
69 def __init__(self, field_name):
70 self.field_name = field_name
71
72- def __get__(self, obj, type=None):
73+ def __get__(self, obj, owner=None):
74 return obj.get(self.field_name)
75
76 def __set__(self, obj, value):
77@@ -109,6 +116,7 @@
78 """Exception when attempting to set a key we don't allow."""
79 pass
80
81+
82 class NoRecordTypeSpecified(Exception):
83 """Exception when creating a record without specifying record type"""
84 def __str__(self):
85@@ -118,6 +126,7 @@
86 class Attachment(object):
87 """This represents the value of the attachment. The name is
88 stored as the key that points to this name."""
89+
90 def __init__(self, content=None, content_type=None):
91 super(Attachment, self).__init__()
92 if content is not None:
93@@ -132,7 +141,8 @@
94 self.content_type = content_type
95 self.needs_synch = content is not None
96
97- def get_content_and_type(self):
98+ def get_content_and_type(self): # WTF? pylint: disable=E0202
99+ """Get content and type of the attachment."""
100 assert self.content is not None
101 if hasattr(self.content, "read"):
102 if hasattr(self.content, "seek"):
103@@ -165,7 +175,7 @@
104 if isinstance(item, dict):
105 item = record_factory(item)
106 if hasattr(item, '_data'):
107- item = item._data
108+ item = item._data # pylint: disable=W0212
109 self._data[key] = item
110
111 def __delitem__(self, key):
112@@ -174,44 +184,11 @@
113 def __len__(self):
114 return len(self._data)
115
116- def attach_by_reference(self, filename, getter_function):
117- """This should only be called by the server code to refer to a BLOB
118- that is in the database, so that we do not have to download it until
119- the user wants it."""
120- a = Attachment(None, None)
121- a.get_content_and_type = getter_function
122- self._attachments[filename] = a
123-
124- def attach(self, content, filename, content_type):
125- """Attach a file-like or string-like object, with a particular name and
126- content type, to a document to be stored in the database. One can not
127- clobber names that already exist."""
128- if filename in self._attachments:
129- raise KeyError("%r already exists" % (filename,))
130- self._attachments[filename] = Attachment(content, content_type)
131-
132- def detach(self, filename):
133- """Remove a BLOB attached to a document."""
134- try:
135- self._attachments.pop(filename)
136- self._detached.add(filename)
137- except KeyError:
138- raise KeyError("%r is not attached to this document" % (filename,))
139-
140- def list_attachments(self):
141- return self._attachments.keys()
142-
143- def attachment_data(self, filename):
144- """Retreive the attached data, the BLOB and content_type."""
145- try:
146- a = self._attachments[filename]
147- except KeyError:
148- raise KeyError("%r is not attached to this document" % (filename,))
149- return a.get_content_and_type()
150
151 class RecordDict(RecordData):
152 """An object that represents an desktop CouchDB record fragment,
153 but behaves like a dictionary.
154+
155 """
156
157 def __setitem__(self, key, item):
158@@ -229,48 +206,54 @@
159
160 def __cmp__(self, value):
161 if isinstance(value, RecordDict):
162- return cmp(self._data, value._data)
163+ return cmp(self._data, value._data) # pylint: disable=W0212
164 return -1
165
166 def get(self, key, default=None):
167- """Override get method."""
168+ """D.get(k[,d]) -> D[k] if k in D, else d. d defaults to None."""
169 if not key in self._data:
170 return default
171 return self[key]
172
173 def update(self, data):
174- """Set items from a dict"""
175+ """D.update(E, **F) -> None. Update D from dict/iterable E and F.
176+
177+ If E has a .keys() method, does: for k in E: D[k] = E[k]
178+ If E lacks .keys() method, does: for (k, v) in E: D[k] = v
179+ In either case, this is followed by: for k in F: D[k] = F[k]
180+
181+ """
182 for key, val in data.items():
183 self[key] = val
184
185 def items(self):
186- """Return all items."""
187+ """D.items() -> list of D's (key, value) pairs, as 2-tuples."""
188 return [(key, self[key]) for key in self]
189
190 def keys(self):
191- """Return all keys."""
192+ """D.keys() -> list of D's keys."""
193 return self._data.keys()
194
195 def setdefault(self, key, default):
196- """remap setdefault"""
197+ """D.setdefault(k[,d]) -> D.get(k,d), also set D[k]=d if k not in D."""
198 if not key in self:
199 self[key] = default
200 return self[key]
201
202 def has_key(self, key):
203+ """D.has_key(k) -> True if D has a key k, else False."""
204 return key in self
205
206
207-
208 class MergeableList(RecordData):
209 """An object that represents a list of complex values."""
210
211 @classmethod
212- def from_list(klass, data):
213+ def from_list(cls, data):
214 """Create a RecordList from passed data."""
215 if not data:
216 raise ValueError("Can't set empty list values.'")
217- result = klass({})
218+ result = cls({})
219 for value in data:
220 result.append(record_factory(value))
221 return result
222@@ -309,7 +292,7 @@
223
224 def __contains__(self, value):
225 if hasattr(value, '_data'):
226- value = value._data
227+ value = value._data # pylint: disable=W0212
228 return value in self._data.values()
229
230 def __cmp__(self, value):
231@@ -321,9 +304,9 @@
232 and hasattr(value, "__getitem__")
233 and hasattr(value, "__len__")):
234 # we should have the same value in the same order
235- length = len(value)
236+ length = len(value)
237 if len(self) == length:
238- for index, current_value in enumerate(self):
239+ for index in range(len(self)):
240 cmp_value = cmp(self[index], value[index])
241 if cmp_value != 0:
242 return cmp_value
243@@ -345,20 +328,25 @@
244 """Return uuid key for index."""
245 return self._get_ordered_keys()[index]
246
247- def get_value_for_uuid(self, uuid):
248+ def get_value_for_uuid(self, lookup_uuid):
249 """Allow API consumers that do know about the UUIDs to get
250 values directly.
251 """
252- return super(MergeableList, self).__getitem__(uuid)
253+ return super(MergeableList, self).__getitem__(lookup_uuid)
254
255 def append(self, value):
256- """Append a value to end of MergeableList."""
257+ """L.append(object) -- append object to end."""
258 new_uuid = str(uuid.uuid4())
259 self._data.setdefault('_order', sorted(self._data.keys())).append(
260 new_uuid)
261 super(MergeableList, self).__setitem__(new_uuid, value)
262
263 def remove(self, value):
264+ """L.remove(value) -- remove first occurrence of value.
265+
266+ Raises ValueError if the value is not present.
267+
268+ """
269 if len(self) == 1:
270 raise ValueError("MergeableList cannot be empty.")
271 index = 0
272@@ -371,15 +359,26 @@
273 index += 1
274 raise ValueError("list.remove(x): x not in list")
275
276- def pop(self, index):
277+ def pop(self, index=None):
278+ """L.pop([index]) -> item -- remove and return item at
279+ index. (default last.)
280+
281+ Raises IndexError if list is empty or index is out of range.
282+ """
283 if len(self) == 1:
284 raise ValueError("MergeableList cannot be empty.")
285+ if index is None:
286+ index = -1
287 value = self[index]
288 del self[index]
289 return value
290
291 def index(self, key):
292- """Get value by index."""
293+ """L.index(value, [start, [stop]]) -> integer -- return first
294+ index of value.
295+
296+ Raises ValueError if the value is not present.
297+ """
298 return self.__getitem__(key)
299
300
301@@ -397,6 +396,8 @@
302 raise NoRecordTypeSpecified
303 super(Record, self).__init__(data)
304 self._data['record_type'] = record_type
305+ if record_type_version is not None:
306+ self._data['_record_type_version'] = record_type_version
307
308 if record_id is not None:
309 # Explicit setting, so try to use it.
310@@ -407,8 +408,6 @@
311 # _id already set. Verify that the explicit value agrees.
312 if self._data['_id'] != record_id:
313 raise ValueError("record_id doesn't match value in data.")
314- if record_type_version:
315- self._record_type_version = record_type_version
316
317 def __getitem__(self, key):
318 if is_internal(key):
319@@ -481,4 +480,40 @@
320 @property
321 def record_type_version(self):
322 """Get the record type version if it's there."""
323- return getattr(self, '_record_type_version', None)
324+ return self._data.get('_record_type_version', None)
325+
326+ def attach_by_reference(self, filename, getter_function):
327+ """This should only be called by the server code to refer to a BLOB
328+ that is in the database, so that we do not have to download it until
329+ the user wants it."""
330+ a = Attachment(None, None)
331+ a.get_content_and_type = getter_function
332+ self._attachments[filename] = a
333+
334+ def attach(self, content, filename, content_type):
335+ """Attach a file-like or string-like object, with a particular name and
336+ content type, to a document to be stored in the database. One can not
337+ clobber names that already exist."""
338+ if filename in self._attachments:
339+ raise KeyError("%r already exists" % (filename,))
340+ self._attachments[filename] = Attachment(content, content_type)
341+
342+ def detach(self, filename):
343+ """Remove a BLOB attached to a document."""
344+ try:
345+ self._attachments.pop(filename)
346+ self._detached.add(filename)
347+ except KeyError:
348+ raise KeyError("%r is not attached to this document" % (filename,))
349+
350+ def list_attachments(self):
351+ """List the record's attachments."""
352+ return self._attachments.keys()
353+
354+ def attachment_data(self, filename):
355+ """Retreive the attached data, the BLOB and content_type."""
356+ try:
357+ a = self._attachments[filename]
358+ except KeyError:
359+ raise KeyError("%r is not attached to this document" % (filename,))
360+ return a.get_content_and_type()
361
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 def test_iter(self):
367 """Tests it is possible to iterate over the record fields"""
368 self.assertEquals(sorted(list(iter(self.record))),
369- ['a', 'b', 'record_type', 'subfield', 'subfield_uuid'])
370+ ['a', 'b', 'subfield', 'subfield_uuid'])
371
372 def test_setitem_internal(self):
373 """Test it is not possible to set a private field on a record"""
374@@ -125,7 +125,7 @@
375 def test_keys(self):
376 """Test getting the keys."""
377 self.assertEqual(
378- ['a', 'b', 'record_type', 'subfield', 'subfield_uuid'],
379+ ['a', 'b', 'subfield', 'subfield_uuid'],
380 sorted(self.record.keys()))
381 self.assertIn("a", self.record)
382 self.assertNotIn('f', self.record)
383@@ -248,10 +248,18 @@
384 def test_mergeable_list_pop_wrong_index(self):
385 """Test pop when index is out or range."""
386 value = [1, 2, 3, 4, 5]
387- self.record["subfield_uuid"] = value
388+ self.record["subfield_uuid"] = MergeableList.from_list(value)
389 self.assertRaises(IndexError, self.record["subfield_uuid"].pop,
390 len(value) * 2)
391
392+ def test_mergeable_list_pop_no_index(self):
393+ """Test pop without an index argument."""
394+ value = [1, 2, 3, 4, 5]
395+ self.record["subfield_uuid"] = MergeableList.from_list(value)
396+ popped_value = self.record["subfield_uuid"].pop()
397+ self.assertEqual(value[-1], popped_value)
398+
399+
400 def test_mergeable_list_pop_last(self):
401 """Test that exception is raised when poping last item"""
402 self.record["subfield_uuid"] = MergeableList.from_list([1])
403@@ -353,9 +361,13 @@
404 """Test record type version support"""
405 data = {"_id": "recordid"}
406 record1 = Record(data, record_type="url")
407+ self.assertIs(None, record1._data.get( # pylint: disable=W0212
408+ '_record_type_version'))
409 self.assertIs(None, record1.record_type_version)
410- record2 = Record(data, record_type="url", record_type_version=1)
411- self.assertEqual(record2.record_type_version, 1)
412+ record2 = Record(data, record_type="url", record_type_version='1.0')
413+ self.assertEqual(record2._data[ # pylint: disable=W0212
414+ '_record_type_version'], '1.0')
415+ self.assertEqual(record2.record_type_version, '1.0')
416
417
418 class TestRecordFactory(TestCase):
419
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 results = self.database.get_all_records()
425
426 self.assertTrue(8 <= len(results)) # our 8, plus callers' data
427- self.assertIn("record_type", results[0])
428+ self.assertNotIn("record_type", results[0])
429+ self.assertTrue(hasattr(results[0], "record_type"))
430
431 for result in results: # index notation
432 if result.record_type == good_record_type:
433
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 fi
439
440 export PYTHONPATH="/usr/share/pycentral/pylint/site-packages:desktopcouch:$PYTHONPATH"
441-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"
442+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
444 pylint_notices=`$pylint $pyfiles`
445

Subscribers

People subscribed via source and target branches