Merge lp:~leonardr/lazr.restful/multiversion-collection into lp:lazr.restful
| Status: | Merged |
|---|---|
| Approved by: | Francis J. Lacoste on 2010-01-12 |
| Approved revision: | no longer in the revision history of the source branch. |
| Merged at revision: | not available |
| Proposed branch: | lp:~leonardr/lazr.restful/multiversion-collection |
| Merge into: | lp:lazr.restful |
| Prerequisite: | lp:~leonardr/lazr.restful/version-specific-request-interface |
| Diff against target: |
2348 lines (+1050/-316) 20 files modified
src/lazr/restful/NEWS.txt (+5/-4) src/lazr/restful/_operation.py (+5/-5) src/lazr/restful/_resource.py (+64/-27) src/lazr/restful/declarations.py (+11/-5) src/lazr/restful/directives/__init__.py (+23/-3) src/lazr/restful/docs/multiversion.txt (+625/-144) src/lazr/restful/docs/utils.txt (+27/-5) src/lazr/restful/docs/webservice-declarations.txt (+49/-26) src/lazr/restful/docs/webservice-error.txt (+1/-0) src/lazr/restful/docs/webservice.txt (+60/-36) src/lazr/restful/example/base/root.py (+5/-4) src/lazr/restful/interfaces/_rest.py (+16/-1) src/lazr/restful/metazcml.py (+7/-1) src/lazr/restful/publisher.py (+15/-7) src/lazr/restful/simple.py (+4/-1) src/lazr/restful/tales.py (+19/-9) src/lazr/restful/testing/webservice.py (+5/-1) src/lazr/restful/tests/test_navigation.py (+50/-29) src/lazr/restful/tests/test_webservice.py (+42/-8) src/lazr/restful/utils.py (+17/-0) |
| To merge this branch: | bzr merge lp:~leonardr/lazr.restful/multiversion-collection |
| Related bugs: |
| Reviewer | Review Type | Date Requested | Status |
|---|---|---|---|
| Francis J. Lacoste (community) | code | 2010-01-06 | Approve on 2010-01-12 |
|
Review via email:
|
|||
| Leonard Richardson (leonardr) wrote : | # |
| Leonard Richardson (leonardr) wrote : | # |
I fixed all but one of the XXX sections (the remaining XXX can't be removed until we can generate multiple versions from declarations) and removed some useless code.
| Francis J. Lacoste (flacoste) wrote : | # |
On January 7, 2010, Leonard Richardson wrote:
> I fixed all but one of the XXX sections (the remaining XXX can't be removed
> until we can generate multiple versions from declarations) and removed
> some useless code.
>
Hi Leonard,
This is really taking shape! I have a few questions and suggestions, so
setting this to
review needsfixing code
> === modified file 'src/lazr/
> class EntryFieldURL(
> """An IAbsoluteURL adapter for EntryField objects."""
> component.
> @@ -1243,7 +1246,7 @@
> def __init__(self, context, request):
> """Associate this resource with a specific object and request."""
> super(EntryReso
> - self.entry = IEntry(context)
> + self.entry = getMultiAdapter
>
Not that if context is already providing IEntry, you'll get an error. I don't
know if this is a plausible case, but if it is, you'll want to only query the
adapter, if context doesn't provide IEntry.
> # We are given a model schema (IFoo). Look up the
> # corresponding entry schema (IFooEntry).
> model_schema = self.relationsh
> - return getGlobalSiteMa
> - model_schema, IEntry).schema
> + request_interface = getUtility(
> + IVersionedClien
> + name=self.
> + return getGlobalSiteMa
> + (model_schema, request_interface), IEntry).schema
In my previous review, I suggested I...Factory instead of
IVersionedClien
Sine you are using the name, why do you need a separate interface. Why not
simply ask for the IWebClientRequest with the specific name? Since all
versioned interface should extend that one, it should work.
> === modified file 'src/lazr/
> + utility = cls()
> + sm = getSiteManager()
> + sm.registerUtil
> +
> + # Create and register marker interfaces for request objects.
> + for version in set(
> + utility.
> + classname = ("IWebServiceCl
> + version.
There are probably other characters that need to be squashed '-' is likely to
be used.
> + marker_interface = InterfaceClass(
> + classname, (IWebServiceCli
> + alsoProvides(
> + marker_interface, IVersionedClien
> + sm.registerUtility(
> + marker_interface, IVersionedClien
> + name=version)
> return True
I suggest writing a unit test for that part that would cover the mangling.
> === modified file 'src/lazr/
> +URL generation
> +--------------
> +
> + >>> from zope.component import getSiteManager
> + >>> from zope.traversing
| Leonard Richardson (leonardr) wrote : | # |
Thanks for your thorough review. I'm going through it slowly. I'll probably have many comments in reply.
> > # We are given a model schema (IFoo). Look up the
> > # corresponding entry schema (IFooEntry).
> > model_schema = self.relationsh
> > - return getGlobalSiteMa
> > - model_schema, IEntry).schema
> > + request_interface = getUtility(
> > + IVersionedClien
> > + name=self.
> > + return getGlobalSiteMa
> > + (model_schema, request_interface), IEntry).schema
>
> In my previous review, I suggested I...Factory instead of
> IVersionedClien
> Sine you are using the name, why do you need a separate interface. Why not
> simply ask for the IWebClientRequest with the specific name? Since all
> versioned interface should extend that one, it should work.
I don't do that because request objects shouldn't be utilities. There's not one request object for every version, but one object for every request. I could create a dummy request object and register that as a utility, but I think that would be really confusing. What I have is also confusing, but it can be explained in an internally consistent way:
1. We need a way to look up a web service description given a version number.
2. These lookups don't operate by version number; they take a marker interface that's different for every version.
3. So we create a way to look up the marker interface given the version number.
4. We implement this by registering the marker interface objects as named utilities.
Does this make sense?
I believe in all these cases we have a specific request object whose .version we look up. If there was some way of extracting from the request object the marker interface that identified the version, we could write a property method WebServiceClien
| Francis J. Lacoste (flacoste) wrote : | # |
On January 11, 2010, Leonard Richardson wrote:
> Thanks for your thorough review. I'm going through it slowly. I'll probably
> have many comments in reply.
>
> > > # We are given a model schema (IFoo). Look up the
> > > # corresponding entry schema (IFooEntry).
> > > model_schema = self.relationsh
> > > - return getGlobalSiteMa
> > > - model_schema, IEntry).schema
> > > + request_interface = getUtility(
> > > + IVersionedClien
> > > + name=self.
> > > + return getGlobalSiteMa
> > > + (model_schema, request_interface), IEntry).schema
> >
> > In my previous review, I suggested I...Factory instead of
> > IVersionedClien
> > factory. Sine you are using the name, why do you need a separate
> > interface. Why not simply ask for the IWebClientRequest with the specific
> > name? Since all versioned interface should extend that one, it should
> > work.
>
> I don't do that because request objects shouldn't be utilities. There's not
> one request object for every version, but one object for every request. I
> could create a dummy request object and register that as a utility, but I
> think that would be really confusing. What I have is also confusing, but
> it can be explained in an internally consistent way:
>
> 1. We need a way to look up a web service description given a version
> number. 2. These lookups don't operate by version number; they take a
> marker interface that's different for every version. 3. So we create a way
> to look up the marker interface given the version number. 4. We implement
> this by registering the marker interface objects as named utilities.
>
> Does this make sense?
Yes, this does make sense. And I do think you should be registering the marker
interface themselve. Remember that a class (or an interface) is an object in
itself. It's already a pattern in zope, the ZCA registers all interfaces as
utility already.
You are also right that what I suggest doesn't work, since the
IWebServiceRequest subclass don't provide IWebServiceRequest themselves. So
what you are doing is the proper way to do. I think I just don't like the name
(it's too long, and the utility you are retrieving isn't an implementation.)
How about IWebServiceVersion ?
>
> I believe in all these cases we have a specific request object whose
> .version we look up. If there was some way of extracting from the request
> object the marker interface that identified the version, we could write a
> property method WebServiceClien
> meant? (I could write this property method now, with the utility lookup,
> and it would clean up the code a bit.)
>
--
Francis J. Lacoste
<email address hidden>
| Leonard Richardson (leonardr) wrote : | # |
> > === modified file 'src/lazr/
> > class EntryFieldURL(
> > """An IAbsoluteURL adapter for EntryField objects."""
> > component.
> > @@ -1243,7 +1246,7 @@
> > def __init__(self, context, request):
> > """Associate this resource with a specific object and request."""
> > super(EntryReso
> > - self.entry = IEntry(context)
> > + self.entry = getMultiAdapter
> >
>
> Not that if context is already providing IEntry, you'll get an error. I don't
> know if this is a plausible case, but if it is, you'll want to only query the
> adapter, if context doesn't provide IEntry.
I could go either way on this, but I kind of consider it an error to pass an Entry object directly into EntryResource. I think allowing that would encourage sloppy thinking and cause lazr.restful developers to confuse entries and data model objects. What do you think?
> > + # Create and register marker interfaces for request objects.
> > + for version in set(
> > + utility.
> > + classname = ("IWebServiceCl
> > + version.
>
> There are probably other characters that need to be squashed '-' is likely to
> be used.
I was lazy. I've added a make_identifier
> > === modified file 'src/lazr/
> > +URL generation
> > +--------------
> > +
> > + >>> from zope.component import getSiteManager
> > + >>> from zope.traversing
> > + >>> from lazr.restful.
> > + ... IRequestAwareLo
> > + >>> from lazr.restful.simple import AbsoluteURL
> > + >>> sm = getSiteManager()
> > + >>> sm.registerAdapter(
> > + ... AbsoluteURL, (IRequestAwareL
> > + ... provided=
>
> Can you add a paragraph explaining what the above does?
OK.
> > Here's the interface for the 'set' object that manages the contacts.
> >
> > + >>> from zope.interface import implements
> > >>> from lazr.restful.
> > >>> class IContactSet(
> > - ... def getAll(self):
> > + ... def getAllContacts():
> > ... "Get all contacts."
> > ...
> > - ... def get(self, name):
> > + ... def get(request, name):
> > ... "Retrieve a single contact by name."
>
> Why is request part of the interface signature?
That method doesn't need to be defined in IContactSet at all, because it's inherited from ITraverseWithGet. ITraverseWithGe
> > Here's a simple ContactSet with a predefined list of contacts.
> >
> > + >>> from zope.publisher.
| Leonard Richardson (leonardr) wrote : | # |
> You are also right that what I suggest doesn't work, since the
> IWebServiceRequest subclass don't provide IWebServiceRequest themselves. So
> what you are doing is the proper way to do. I think I just don't like the name
> (it's too long, and the utility you are retrieving isn't an implementation.)
> How about IWebServiceVersion ?
OK, I've renamed it to IWebServiceVersion.
| Leonard Richardson (leonardr) wrote : | # |
Apropos getting rid of IRequestAwareLo
>>> class ContactSetLocat
... """An ILocation implementation for ContactSet objects."""
... implements(
...
... def __init__(self, context, request):
... self.context = contact_set
... self.request = request
...
... def __parent__(self, request):
... return getUtility(
...
... def __name__(self, request):
... if request.version == 'beta':
... return 'contact_list'
... return 'contacts'
>>> from zope.traversing
>>> sm.registerAdapter(
... ZopeAbsoluteURL, (ILocation, IWebServiceLayer),
... provided=
>>> sm.registerAdapter(
... ContactSetLocation, (IContactSet, IWebServiceLayer),
... provided=ILocation)
Unfortunately this doesn't work with Zope's AbsoluteURL implementation, which contains this line:
context = ILocation(context)
I never get a chance to convert ContactSet into a ContactSetLocation.
I can modify my alternate implementation of AbsoluteURL to try a multi-adapter lookup before trying a simple cast to ILocation. I'll still need the alternate AbsoluteURL but I'd be able to get rid of the IRequestAwareLo
| Francis J. Lacoste (flacoste) wrote : | # |
On January 11, 2010, Leonard Richardson wrote:
> > > === modified file 'src/lazr/
> > > class EntryFieldURL(
> > > """An IAbsoluteURL adapter for EntryField objects."""
> > > component.
> > > @@ -1243,7 +1246,7 @@
> > > def __init__(self, context, request):
> > > """Associate this resource with a specific object and
> > > request.""" super(EntryReso
> > > self.entry = IEntry(context)
> > > + self.entry = getMultiAdapter
> >
> > Not that if context is already providing IEntry, you'll get an error. I
> > don't know if this is a plausible case, but if it is, you'll want to only
> > query the adapter, if context doesn't provide IEntry.
>
> I could go either way on this, but I kind of consider it an error to pass
> an Entry object directly into EntryResource. I think allowing that would
> encourage sloppy thinking and cause lazr.restful developers to confuse
> entries and data model objects. What do you think?
Sure.
> > > Here's a simple ContactSet with a predefined list of contacts.
> > >
> > > + >>> from zope.publisher.
> > > + >>> from lazr.restful.
> > >
> > > >>> from lazr.restful.simple import TraverseWithGet
> > >
> > > - >>> from zope.publisher.
> > >
> > > >>> class ContactSet(
> > >
> > > ... implements(
> > > - ... path = "contacts"
> > > ...
> > > ... def __init__(self):
> > > ... self.contacts = CONTACTS
> > > ...
> > > - ... def get(self, name):
> > > - ... contacts = [contact for contacts in self.contacts
> > > - ... if pair.name == name]
> > > + ... def get(self, request, name):
> > > + ... contacts = [contact for contact in self.contacts
> > > + ... if contact.name == name]
> > > ... if len(contacts) == 1:
> > > ... return contacts[0]
> > > ... return None
> >
> > From the implementation, I don't see why request is required?
>
> As before, it's inherited from ITraverseWithGet.
Ok.
--
Francis J. Lacoste
<email address hidden>
| Francis J. Lacoste (flacoste) wrote : | # |
On January 11, 2010, Leonard Richardson wrote:
> Apropos getting rid of IRequestAwareLo
some code like this:
> >>> class ContactSetLocat
>
> ... """An ILocation implementation for ContactSet objects."""
> ... implements(
> ...
> ... def __init__(self, context, request):
> ... self.context = contact_set
> ... self.request = request
> ...
> ... def __parent__(self, request):
> ... return getUtility(
> name=request.
> ... def __name__(self, request):
> ... if request.version == 'beta':
> ... return 'contact_list'
> ... return 'contacts'
>
I'd expect __parent__ and __name__ to be property. You don't need the request
parameter since you have it from the constructor.
> >>> from zope.traversing
> >>> sm.registerAdapter(
>
> ... ZopeAbsoluteURL, (ILocation, IWebServiceLayer),
> ... provided=
>
> >>> sm.registerAdapter(
>
> ... ContactSetLocation, (IContactSet, IWebServiceLayer),
> ... provided=ILocation)
>
> Unfortunately this doesn't work with Zope's AbsoluteURL implementation,
> which contains this line:
>
> context = ILocation(context)
>
> I never get a chance to convert ContactSet into a ContactSetLocation.
I see. I suggest then that you simply provide an ILocation adapter and use
get_current_
>
> I can modify my alternate implementation of AbsoluteURL to try a
> multi-adapter lookup before trying a simple cast to ILocation. I'll still
> need the alternate AbsoluteURL but I'd be able to get rid of the
> IRequestAwareLo
> something?
>
--
Francis J. Lacoste
<email address hidden>
| Leonard Richardson (leonardr) wrote : | # |
If I use get_current_
On that note, I remember what I was doing with the IRequestAwareLo
We don't really need that to get a basic system working, but it might be useful in the future. What do you think? I'll work on taking the code out for now.
| Francis J. Lacoste (flacoste) wrote : | # |
On January 12, 2010, Leonard Richardson wrote:
> If I use get_current_
> adapter at all. (Though one is still possible.) I might as well make the
> data model objects implement ILocation directly.
>
> On that note, I remember what I was doing with the IRequestAwareLo
> thing. We can always implement a standard ILocation adapter that grabs the
> request and branches based on request.version. But with
> IRequestAwareLo
> different versions. BetaContactLocation can provide the location for
> version 'beta' and DevContactLocation can provide the location for version
> 'dev'.
>
> We don't really need that to get a basic system working, but it might be
> useful in the future. What do you think? I'll work on taking the code out
> for now.
>
We shouldn't introduce other abstraction layers until they are needed :-) I
call YAGNI for now.
--
Francis J. Lacoste
<email address hidden>
| Leonard Richardson (leonardr) wrote : | # |
OK, I've made all the changes you requested. The complete diff of my changes since your review is here:
| Francis J. Lacoste (flacoste) wrote : | # |
On January 12, 2010, Leonard Richardson wrote:
> OK, I've made all the changes you requested. The complete diff of my
> changes since your review is here:
>
> http://
>
That's great, thanks. My only comment is about the NEWS.txt file:
> === modified file 'src/lazr/
> --- src/lazr/
> +++ src/lazr/
> @@ -9,10 +9,11 @@
> changes. You *must* change your configuration object to get your code
> to work in this version! See "active_versions" below.
>
> -Added the precursor of a versioning system for web services. Clients
> -can now request the "trunk" of a web service as well as one published
> -version. Apart from the URIs served, the two web services are exactly
> -the same.
> +Added a versioning system for web services. Clients can now request
> +the "trunk" of a web service as well as one published version. Apart
> +from the URIs served, the two web services are exactly the same. There
> +is no way to serve two different versions of a web service without
> +defining both versions from scratch.
You might want to make this less harsh by stating that a next version will add
annotations to make that easy to do.
review approve code
status approved
--
Francis J. Lacoste
<email address hidden>
- 95. By Leonard Richardson on 2010-01-12
-
[r=flacoste] It's now possible to define two distinct web services based on the same data model.

This branch makes it possible to serve multiple versions of a web service from the same underlying dataset. The multiversion.txt test shows how it's done. Everything else in this branch is code to make existing lazr.restful code version-aware without breaking the existing tests.
As per the 'version- specific- request- interface' branch, every incoming request is associated with some version of the web service using a marker interface. To make lazr.restful code version-aware I made it request-aware. Instead of ILocation I now use IRequestAwareLo cation. IEntry and ICollection lookups are now multi-adapter lookups: previously you could just write "IEntry( data_model_ object) ". Now you must write "getMultiAdapte r((data_ model_object, request), IEntry)" -- depending on the marker interface attached to the request object, you may get a different IEntry implementation back.
I had to add some more setup to the unit tests. Previously they got along without defining any configuration or request objects. Now that IEntry lookups require a request object, I had to define fake requests and minimal configuration objects for the unit tests.
There are a few TK places I need to look into. I also use code like this several times:
+ request_interface = getUtility( tRequestImpleme ntation, request. version) nager() .adapters. lookup(
+ IVersionedClien
+ name=self.
+ return getGlobalSiteMa
+ (model_schema, request_interface), IEntry).schema
It might be worth writing a helper function.
I'm going through the code now and I'll comment on anything else strange I find.