Merge lp:~leonardr/lazr.restfulclient/server-takes-precedence into lp:lazr.restfulclient

Proposed by Leonard Richardson
Status: Merged
Approved by: Aaron Bentley
Approved revision: 104
Merged at revision: 101
Proposed branch: lp:~leonardr/lazr.restfulclient/server-takes-precedence
Merge into: lp:lazr.restfulclient
Diff against target: 153 lines (+106/-6)
4 files modified
src/lazr/restfulclient/NEWS.txt (+13/-4)
src/lazr/restfulclient/docs/toplevel.txt (+80/-0)
src/lazr/restfulclient/resource.py (+12/-1)
src/lazr/restfulclient/version.txt (+1/-1)
To merge this branch: bzr merge lp:~leonardr/lazr.restfulclient/server-takes-precedence
Reviewer Review Type Date Requested Status
Aaron Bentley (community) Approve
Gary Poster Pending
Review via email: mp+28059@code.launchpad.net

Description of the change

Let's say you have a shim object of resource type 'foo'. Because it's a shim, you don't have any real data for it, but you can get some data by accessing one of the many properties of a 'foo'-type object. When you do this, lazr.restful makes a request for the corresponding resource.

But, uh-oh! When lazr.restful makes the request, the representation of this resource says it's not a 'foo' object at all! It's actually a 'bar' object! What to do?

Currently, lazr.restful stubbornly insists that the object is a 'foo' object, and you can only access the features that are common to 'foo' and 'bar'. This branch changes things so that once the representation is received, the resource silently changes into a 'bar' object. You'll only get an error if you tried to access a feature of 'foo' that wasn't in 'bar'.

This is important for my upcoming launchpadlib branch, where 'foo' == 'team' and 'bar' == 'person'.

To post a comment you must log in.
Revision history for this message
Aaron Bentley (abentley) wrote :

I have reservations about letting objects change type, but leonardr has convinced me that this is not a practical problem. The discrepancy would be hard to provoke, because accessing members will cause the object to be loaded.

I would like to see mention in the documentation that WADL doesn't allow inheritance relationships to be explored. Otherwise, this is fine.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/lazr/restfulclient/NEWS.txt'
2--- src/lazr/restfulclient/NEWS.txt 2010-06-16 16:49:44 +0000
3+++ src/lazr/restfulclient/NEWS.txt 2010-06-21 14:30:49 +0000
4@@ -2,13 +2,22 @@
5 NEWS for lazr.restfulclient
6 ===========================
7
8+0.9.19 (2010-06-21)
9+===================
10+
11+ - When the representation of a resource, as retrieved from the
12+ server, is of a different type than expected, the server value now
13+ takes precedence. This means that, in rare situations, a resource
14+ may start out presumed to be of one type, and change its
15+ capabilities once its representation is fetched from the server.
16+
17 0.9.18 (2010-06-16)
18 ===================
19
20- - Made it possible to avoid fetching a representation of every
21- single object looked up from a CollectionWithKeyBasedLookup (by
22- defining .contains_resource_type on the class), potentially
23- improving script performance.
24+ - Made it possible to avoid fetching a representation of every single
25+ object looked up from a CollectionWithKeyBasedLookup (by defining
26+ .collection_of on the class), potentially improving script
27+ performance.
28
29 0.9.17 (2010-05-10)
30 ===================
31
32=== modified file 'src/lazr/restfulclient/docs/toplevel.txt'
33--- src/lazr/restfulclient/docs/toplevel.txt 2010-02-11 15:59:16 +0000
34+++ src/lazr/restfulclient/docs/toplevel.txt 2010-06-21 14:30:49 +0000
35@@ -41,6 +41,86 @@
36 >>> print service.recipes[1].dish.name
37 Roast chicken
38
39+When a collection supports key-based lookups, simply looking up an
40+object will not trigger an HTTP request. The HTTP request happens when
41+you try to access one of the object's properties.
42+
43+ >>> import httplib2
44+ >>> httplib2.debuglevel = 1
45+ >>> debug_service = CookbookWebServiceClient()
46+ send: 'GET ...'
47+ ...
48+ >>> httplib2.debuglevel = 0
49+
50+ >>> recipe = debug_service.recipes[4]
51+
52+ >>> sorted(recipe.lp_attributes)
53+ ['http_etag', 'id', 'instructions', ...]
54+
55+ >>> print recipe.instructions
56+ send: 'GET /1.0/recipes/4 ...'
57+ ...
58+ Preheat oven to...
59+
60+Now, let's imagine that a top-level collection is misconfigured. We
61+know that the top-level collection of recipes contains objects whose
62+resource type is 'recipe'. But let's tell lazr.restfulclient that that
63+collection contains objects of type 'cookbook'.
64+
65+ >>> from lazr.restfulclient.tests.example import RecipeSet
66+ >>> print RecipeSet.collection_of
67+ recipe
68+ >>> RecipeSet.collection_of = 'cookbook'
69+
70+Looking up an object will give you something that presents the
71+interface of a cookbook.
72+
73+ >>> not_really_a_cookbook = debug_service.recipes[2]
74+ >>> sorted(not_really_a_cookbook.lp_attributes)
75+ ['confirmed', 'copyright_date', 'cuisine'...]
76+
77+But once you try to access one of the object's properties, and the
78+HTTP request is made...
79+
80+ >>> print not_really_a_cookbook.resource_type_link
81+ send: 'GET /1.0/recipes/2 ...'
82+ ...
83+ http://cookbooks.dev/1.0/#recipe
84+
85+...the server serves a recipe, and so the client-side object takes on
86+the properties of a recipe. You can only fool lazr.restfulclient up to
87+the point where it has real data to look at.
88+
89+ >>> sorted(not_really_a_cookbook.lp_attributes)
90+ ['http_etag', 'id', 'instructions', ...]
91+
92+ >>> print not_really_a_cookbook.instructions
93+ Draw, singe, stuff, and truss...
94+
95+This isn't just a defense mechanism: it's a useful feature when a
96+top-level collection contains mixed subclasses of some superclass. For
97+instance, the launchpadlib library defines the 'people' collection as
98+containing 'team' objects, even though it also contains 'person'
99+objects, which expose a subset of a team's functionality. All objects
100+looked up in that collection start out as team objects, but once an
101+object's data is fetched, if it turns out to actually be a person, it
102+switches from the "team" interface to the "people" interface.
103+
104+If you try to access a property based on a resource type the object
105+doesn't really implement, you'll get an error.
106+
107+ >>> not_really_a_cookbook = debug_service.recipes[3]
108+ >>> sorted(not_really_a_cookbook.lp_attributes)
109+ ['confirmed', 'copyright_date', 'cuisine'...]
110+ >>> not_really_a_cookbook.cuisine
111+ Traceback (most recent call last):
112+ ...
113+ AttributeError: 'Entry' object has no attribute 'cuisine'
114+
115+Cleanup.
116+
117+ >>> RecipeSet.collection_of = 'recipe'
118+
119 Versioning
120 ==========
121
122
123=== modified file 'src/lazr/restfulclient/resource.py'
124--- src/lazr/restfulclient/resource.py 2010-06-16 17:40:18 +0000
125+++ src/lazr/restfulclient/resource.py 2010-06-21 14:30:49 +0000
126@@ -329,8 +329,19 @@
127 if self._wadl_resource.representation is None:
128 # Get a representation of the linked resource.
129 representation = self._root._browser.get(self._wadl_resource)
130+ representation = simplejson.loads(representation)
131+ type_link = representation['resource_type_link']
132+ if (type_link is not None
133+ and type_link != self._wadl_resource.type_url):
134+ # In rare cases, the resource type served by the
135+ # server conflicts with the type the client thought
136+ # this resource had. When this happens, the server
137+ # value takes precedence.
138+ resource_type = self._root._wadl.get_resource_type(type_link)
139+ self._wadl_resource.tag = resource_type.tag
140 self.__dict__['_wadl_resource'] = self._wadl_resource.bind(
141- representation, self.JSON_MEDIA_TYPE)
142+ representation, self.JSON_MEDIA_TYPE,
143+ representation_needs_processing=False)
144
145 def __ne__(self, other):
146 """Inequality operator."""
147
148=== modified file 'src/lazr/restfulclient/version.txt'
149--- src/lazr/restfulclient/version.txt 2010-06-16 16:49:44 +0000
150+++ src/lazr/restfulclient/version.txt 2010-06-21 14:30:49 +0000
151@@ -1,1 +1,1 @@
152-0.9.18
153+0.9.19

Subscribers

People subscribed via source and target branches