Merge lp:~salgado/lazr.restful/extension-interfaces into lp:lazr.restful

Proposed by Guilherme Salgado
Status: Merged
Approved by: Gary Poster
Approved revision: 154
Merged at revision: 135
Proposed branch: lp:~salgado/lazr.restful/extension-interfaces
Merge into: lp:lazr.restful
Diff against target: 1498 lines (+792/-121)
16 files modified
src/lazr/restful/NEWS.txt (+10/-0)
src/lazr/restful/_resource.py (+14/-13)
src/lazr/restful/declarations.py (+70/-32)
src/lazr/restful/docs/multiversion.txt (+15/-0)
src/lazr/restful/docs/webservice-declarations.txt (+65/-59)
src/lazr/restful/docs/webservice.txt (+28/-0)
src/lazr/restful/example/base/tests/test_integration.py (+1/-0)
src/lazr/restful/example/base_extended/README.txt (+22/-0)
src/lazr/restful/example/base_extended/__init__.py (+3/-0)
src/lazr/restful/example/base_extended/comments.py (+32/-0)
src/lazr/restful/example/base_extended/site.zcml (+18/-0)
src/lazr/restful/example/base_extended/tests/test_integration.py (+38/-0)
src/lazr/restful/metazcml.py (+95/-16)
src/lazr/restful/testing/helpers.py (+45/-0)
src/lazr/restful/tests/test_declarations.py (+335/-0)
src/lazr/restful/version.txt (+1/-1)
To merge this branch: bzr merge lp:~salgado/lazr.restful/extension-interfaces
Reviewer Review Type Date Requested Status
Gary Poster Approve
Leonard Richardson (community) Needs Fixing
Review via email: mp+29388@code.launchpad.net

Description of the change

This branch makes it possible to extend webservice entries by marking certain interfaces as extensions to others. It's still a work in progress and I'm proposing the merge just to get feedback on the design and desirability of such feature.

With the current design you just decorate extension interfaces with the regular @export_as_webservice_entry() but passing in a list of interfaces it extends. (I've abused the existing decorator for simplicity, but I plan to use a separate one for extension interfaces)

That will just store the list of extended interfaces as an annotation on the interface.

That list of extended interfaces is then picked up by the code which generates the webservice interfaces/adapters, which then generates a single interface/adapter containing all the fields exported in the content interface and all of its extensions. Exported operations are handled in a similar fashion, but we have a separate adapter for every operation.

Since the extension interfaces are usually not provided directly by the content classes (they are provided through an adapter), the webservice adapters would have to adapt the content objects into the extension interface when accessing a field that actually comes from one such interface. However, when accessing fields that come from the content interface, such adaptation wouldn't be necessary (as the content object is supposed to provide the content interface). To simplify things, though, I've changed the webservice adapters to always adapt the content object into the field's originating interface.

https://lists.launchpad.net/launchpad-dev/msg03676.html has a good description of why we're doing this.

To post a comment you must log in.
Revision history for this message
Leonard Richardson (leonardr) wrote :

This branch is slow going but I have one suggestion. Can you test this assertion?

+ assert name in orig_iface, (
+ "Could not find interface where %s is defined" % name)

I don't see how it could be triggered. But maye the point is that it never is triggered?

Revision history for this message
Guilherme Salgado (salgado) wrote :

On Fri, 2010-07-16 at 15:36 +0000, Leonard Richardson wrote:
> This branch is slow going but I have one suggestion. Can you test this assertion?
>
> + assert name in orig_iface, (
> + "Could not find interface where %s is defined" % name)
>
> I don't see how it could be triggered. But maye the point is that it never is triggered?

As we discussed at the Epic, I don't think we can trigger that assertion
without introducing a bug somewhere.

--
Guilherme Salgado <https://launchpad.net/~salgado>

Revision history for this message
Gary Poster (gary) wrote :

I've done a non-exhaustive review (as a supplement to Leonard's not a replacement), and want to share my comments so far.

Requests:

- This is an important new feature for lazr.restful, and I don't see any instructions on how to use it. Could you add some documentation (doctest) to the example webservice part of the package? (As a small corollary, we'll want to mention something in the CHANGES/NEWS file.)

- I see that find_interfaces_and_contributors is responsible for raising ConflictInContributingInterfaces. I'd like to make sure that these errors are as helpful as they can be. Right now the exception just has the name and the base interface that has been modified. I'd also like to see the two or more interfaces that have created the conflict--and I'd like a test that shows that a traceback with this error gives the developer a nice error message telling what's gone wrong and where to look to address it. (A unit test of the error's __str__ should be sufficient.)

Suggestions:

- for the new argument name, "contributes_to" -> "extends"? Take it only if you like it, but it matches nicely with "extensions," which you use later, for instance. If you do this, you'd want to change "contributors" to "extenders," I suspect.

Revision history for this message
Guilherme Salgado (salgado) wrote :

On Wed, 2010-07-21 at 20:27 +0000, Gary Poster wrote:
> I've done a non-exhaustive review (as a supplement to Leonard's not a
> replacement), and want to share my comments so far.

Cool, thanks!

>
> Requests:
>
> - This is an important new feature for lazr.restful, and I don't see
> any instructions on how to use it. Could you add some documentation
> (doctest) to the example webservice part of the package? (As a small
> corollary, we'll want to mention something in the CHANGES/NEWS file.)

Indeed, and I also think it would be worth mentioning it in
docs/webservice-declarations.txt. What do you think?

>
> - I see that find_interfaces_and_contributors is responsible for
> raising ConflictInContributingInterfaces. I'd like to make sure that
> these errors are as helpful as they can be. Right now the exception
> just has the name and the base interface that has been modified. I'd
> also like to see the two or more interfaces that have created the
> conflict--and I'd like a test that shows that a traceback with this
> error gives the developer a nice error message telling what's gone
> wrong and where to look to address it. (A unit test of the error's
> __str__ should be sufficient.)

Something like http://paste.ubuntu.com/467438/ or would you like me to
show the error message in one of the existing unit tests?

>
> Suggestions:
>
> - for the new argument name, "contributes_to" -> "extends"? Take it
> only if you like it, but it matches nicely with "extensions," which
> you use later, for instance. If you do this, you'd want to change
> "contributors" to "extenders," I suspect.

Heh, extends is exactly what I used in the first round but after I
showed it to Leonard he suggested contributes_to. I guess I'll keep
this in mind and see if he has any other suggestions.

Cheers,

--
Guilherme Salgado <https://launchpad.net/~salgado>

Revision history for this message
Gary Poster (gary) wrote :

On Jul 22, 2010, at 6:28 AM, Guilherme Salgado wrote:

> On Wed, 2010-07-21 at 20:27 +0000, Gary Poster wrote:
>> I've done a non-exhaustive review (as a supplement to Leonard's not a
>> replacement), and want to share my comments so far.
>
> Cool, thanks!
>
>>
>> Requests:
>>
>> - This is an important new feature for lazr.restful, and I don't see
>> any instructions on how to use it. Could you add some documentation
>> (doctest) to the example webservice part of the package? (As a small
>> corollary, we'll want to mention something in the CHANGES/NEWS file.)
>
> Indeed, and I also think it would be worth mentioning it in
> docs/webservice-declarations.txt. What do you think?

+1

>
>>
>> - I see that find_interfaces_and_contributors is responsible for
>> raising ConflictInContributingInterfaces. I'd like to make sure that
>> these errors are as helpful as they can be. Right now the exception
>> just has the name and the base interface that has been modified. I'd
>> also like to see the two or more interfaces that have created the
>> conflict--and I'd like a test that shows that a traceback with this
>> error gives the developer a nice error message telling what's gone
>> wrong and where to look to address it. (A unit test of the error's
>> __str__ should be sufficient.)
>
> Something like http://paste.ubuntu.com/467438/ or would you like me to
> show the error message in one of the existing unit tests?

Looks perfect, thank you.

>
>>
>> Suggestions:
>>
>> - for the new argument name, "contributes_to" -> "extends"? Take it
>> only if you like it, but it matches nicely with "extensions," which
>> you use later, for instance. If you do this, you'd want to change
>> "contributors" to "extenders," I suspect.
>
> Heh, extends is exactly what I used in the first round but after I
> showed it to Leonard he suggested contributes_to. I guess I'll keep
> this in mind and see if he has any other suggestions.

Understood.

Leonard will not be back today. I'll discuss on IRC how we should proceed.

Gary

Revision history for this message
Leonard Richardson (leonardr) wrote :

I didn't like "extends" for two reasons. First, "Foo extends bar" implies that 'foo' derives from 'bar', where as in this case 'bar' derives from 'foo'. Second, "extends" already means something in object-oriented programming and I didn't want to overload the term.

This is really good, almost ready to land. Comments:

I second Gary's suggestion about ConflictInContributingInterfaces and think your patch looks good.

You still need to describe this change in the NEWS file.

I don't know how useful that TestWebServiceConfiguration refactoring is, but it's fine since oyu use it twice. The register_module refactoring is a great idea.

I would like to see this feature used in an example web service (ie. added to examples/base or in a totally new web service of your design). This is less for purposes of test coverage and more to show how the feature would work in a real situation.

Lines 802 and 803 are the same.

Some of your tests are missing explanatory comments. The stub classes like Product need to have docstrings saying things like "A stub class that publishes one string field and one read operation."

review: Needs Fixing
145. By Guilherme Salgado

Improve the ConflictInContributingInterfaces exception to state what interfaces contain the conflicting attributes

146. By Guilherme Salgado

Add new entry to NEWS.txt

Revision history for this message
Guilherme Salgado (salgado) wrote :

On Tue, 2010-07-27 at 19:06 +0000, Leonard Richardson wrote:
> Review: Needs Fixing
> I didn't like "extends" for two reasons. First, "Foo extends bar"
> implies that 'foo' derives from 'bar', where as in this case 'bar'
> derives from 'foo'. Second, "extends" already means something in
> object-oriented programming and I didn't want to overload the term.
>
> This is really good, almost ready to land. Comments:
>
> I second Gary's suggestion about ConflictInContributingInterfaces and
> think your patch looks good.

Cool.

>
> You still need to describe this change in the NEWS file.

Done; please let me know if it looks good.

>
> I don't know how useful that TestWebServiceConfiguration refactoring
> is, but it's fine since oyu use it twice. The register_module
> refactoring is a great idea.
>
> I would like to see this feature used in an example web service (ie.
> added to examples/base or in a totally new web service of your
> design). This is less for purposes of test coverage and more to show
> how the feature would work in a real situation.

I moved some things in examples/base to make use of the new feature
(http://paste.ubuntu.com/471195/), but I see no reason why people would
use it like that in a simple interface. In fact, since the need for
this feature came from us wanting to split the implementation of some
very complex classes into smaller ones, I find it hard to think of a
good yet short example to use here. Maybe you have an idea?

While doing this I've found a bug that was causing the generated
webservice interfaces to use the doc/name of extension interfaces
instead of those from the main interface.
http://paste.ubuntu.com/471192/ is the fix

>
> Lines 802 and 803 are the same.

Not really; one of them registers ProductToHasBugs and the other
ProjectToHasBugs

>
> Some of your tests are missing explanatory comments. The stub classes

Good catch; I've just added them.

> like Product need to have docstrings saying things like "A stub class
> that publishes one string field and one read operation."

I'm not sure where to add such docstrings as the stub class really is
Product but the one that publishes things is IProduct. Also, I don't
think a docstring like that adds any value, but I could add them if you
feel strong about it.

--
Guilherme Salgado <https://launchpad.net/~salgado>

147. By Guilherme Salgado

Some comments/docstrings on tests

148. By Guilherme Salgado

Fix a bug in generate_entry_interfaces that was causing the generated webservice interfaces to use the doc/name of extension interfaces instead of those from the main interface

149. By Guilherme Salgado

Document contributing interfaces in webservice-declarations.txt, rename a couple remaining variables from extensions to contributors and add a unit test to show that contributing interfaces are not exported by themselves

Revision history for this message
Guilherme Salgado (salgado) wrote :

Other than the things I'd mentioned in my previous comment, I also added a new unit test to show that contributing interfaces are not exported by themselves together with a short explanation of contributing interfaces on webservice-declarations.txt

Revision history for this message
Gary Poster (gary) wrote :

This is really a very nice branch. Thank you.

I hate to not give it a final go-ahead, but I do have one last request.

= Requests =

You said:
> Leonard said:
> > I would like to see this feature used in an example web service (ie.
> > added to examples/base or in a totally new web service of your
> > design). This is less for purposes of test coverage and more to show
> > how the feature would work in a real situation.
>
> I moved some things in examples/base to make use of the new feature
> (http://paste.ubuntu.com/471195/), but I see no reason why people would
> use it like that in a simple interface. In fact, since the need for
> this feature came from us wanting to split the implementation of some
> very complex classes into smaller ones, I find it hard to think of a
> good yet short example to use here. Maybe you have an idea?

I'd argue that the central story is reuse. That reuse might be to factor out a simpler webservice that can be reused elsewhere (which is actually what is driving your work, AIUI) or it might be to extend a simpler webservice (which you might end up doing as well). I think you could easily explain a change in this way.

If you didn't already have an example, I might try to think up some scenario, like another branch of a company that wanted to add, say, the idea of a recipe contributor, or wanted to add the idea of favorite recipes or recipe ratings. That particular idea might be undesirable for the imaginary people using the base website.

What you have already might work though. While it's hard to imagine someone not wanting search, perhaps other teams have a more complicated search method. You are sharing/reusing the base webservice, and you just want to implement a very simple search. The other team works from the same base code, but extends the cookbook with a much more complex search method that also incorporates ideas that you don't need and don't want, like contributors, favorites, and/or ratings.

Can you use your patch and then add an explanation with an imaginary scenario like the one I gave above?

= Small suggestions =

In generate_entry_adapters (src/lazr/restful/declarations.py) maybe add a comment explaining that the update ("fields.update(getFields(iface).items())") is not destructive because we have already verified that there are no overlaps in find_interfaces_and_contributors.

= Thoughts =

AFAICT, saying that an exported interface contributes to another interface effectively exports that other interface even if it were not initially marked to be exported. I don't love that, but I don't think it is a big deal, and I'm not going to worry about it.

Product docstrings: I'm fine with leaving them out as you suggest.

NEWS looks good. Thank you!

150. By Guilherme Salgado

Turn a set() into a list() as we didn't seem to need a set there.

151. By Guilherme Salgado

Raise a meaningful exception when an interface contributes to another interface which is not exported

Revision history for this message
Guilherme Salgado (salgado) wrote :
Download full text (4.9 KiB)

On Tue, 2010-08-03 at 18:11 +0000, Gary Poster wrote:
> This is really a very nice branch. Thank you.
>
> I hate to not give it a final go-ahead, but I do have one last request.
>
> = Requests =
>
> You said:
> > Leonard said:
> > > I would like to see this feature used in an example web service (ie.
> > > added to examples/base or in a totally new web service of your
> > > design). This is less for purposes of test coverage and more to show
> > > how the feature would work in a real situation.
> >
> > I moved some things in examples/base to make use of the new feature
> > (http://paste.ubuntu.com/471195/), but I see no reason why people would
> > use it like that in a simple interface. In fact, since the need for
> > this feature came from us wanting to split the implementation of some
> > very complex classes into smaller ones, I find it hard to think of a
> > good yet short example to use here. Maybe you have an idea?
>
> I'd argue that the central story is reuse. That reuse might be to
> factor out a simpler webservice that can be reused elsewhere (which is
> actually what is driving your work, AIUI) or it might be to extend a
> simpler webservice (which you might end up doing as well). I think
> you could easily explain a change in this way.

Well, what we really want is to be able to move attributes/methods out
of existing interfaces and into adapters while still publicizing a
single entry comprising what's exported by the changed interface and all
of its adapters. For the reuse story one can use mix-ins as long as
it's ok to have the exported interface inherit from a bunch of others
(as we currently do).

>
> If you didn't already have an example, I might try to think up some
> scenario, like another branch of a company that wanted to add, say,
> the idea of a recipe contributor, or wanted to add the idea of
> favorite recipes or recipe ratings. That particular idea might be
> undesirable for the imaginary people using the base website.
>
> What you have already might work though. While it's hard to imagine
> someone not wanting search, perhaps other teams have a more
> complicated search method. You are sharing/reusing the base
> webservice, and you just want to implement a very simple search. The
> other team works from the same base code, but extends the cookbook
> with a much more complex search method that also incorporates ideas
> that you don't need and don't want, like contributors, favorites,
> and/or ratings.
>

After letting this marinate on my head for a while longer, I thought of
creating an IHasComments interface that contributes a 'comments'
attribute to IRecipe, as if there was a separate web service which wants
to allow its users to comment on recipes
(http://paste.ubuntu.com/472821/). How does that sound to you?

I've also modified one of the tests for the base example to show the
comments, but that uncovered a bug in the current implementation:
EntryResource.redacted_fields will look for the 'comments' attribute on
the IRecipe object directly instead of through its IHasComments adapter,
and that causes a ForbiddenAttribute. I think I could change
redacted_fields to adapt the context into the field'...

Read more...

Revision history for this message
Guilherme Salgado (salgado) wrote :

http://paste.ubuntu.com/473132/ fixes the bug in EntryResource.redacted_fields. It basically stores, as a class variable in the generated adapter, a dict mapping every field of that adapter to the interface where that field came from. That way we can make redacted_fields use the checker for the correct interface for every field.

Revision history for this message
Gary Poster (gary) wrote :
Download full text (6.2 KiB)

On Aug 3, 2010, at 5:46 PM, Guilherme Salgado wrote:

> On Tue, 2010-08-03 at 18:11 +0000, Gary Poster wrote:
>> This is really a very nice branch. Thank you.
>>
>> I hate to not give it a final go-ahead, but I do have one last request.
>>
>> = Requests =
>>
>> You said:
>>> Leonard said:
>>>> I would like to see this feature used in an example web service (ie.
>>>> added to examples/base or in a totally new web service of your
>>>> design). This is less for purposes of test coverage and more to show
>>>> how the feature would work in a real situation.
>>>
>>> I moved some things in examples/base to make use of the new feature
>>> (http://paste.ubuntu.com/471195/), but I see no reason why people would
>>> use it like that in a simple interface. In fact, since the need for
>>> this feature came from us wanting to split the implementation of some
>>> very complex classes into smaller ones, I find it hard to think of a
>>> good yet short example to use here. Maybe you have an idea?
>>
>> I'd argue that the central story is reuse. That reuse might be to
>> factor out a simpler webservice that can be reused elsewhere (which is
>> actually what is driving your work, AIUI) or it might be to extend a
>> simpler webservice (which you might end up doing as well). I think
>> you could easily explain a change in this way.
>
> Well, what we really want is to be able to move attributes/methods out
> of existing interfaces and into adapters while still publicizing a
> single entry comprising what's exported by the changed interface and all
> of its adapters. For the reuse story one can use mix-ins as long as
> it's ok to have the exported interface inherit from a bunch of others
> (as we currently do).

Right, but the mix-in approach means a lot more work for small changes. I could be wrong, but with the mix-in class approach, you have to make your own separate registration of *everything* because you don't want one interface, you want another one. There are examples of this kind of annoyance in the webservice package. With your feature, you can reuse the entirety of a webservice, including its registrations, and then register your own things in addition. That's much easier.

Maybe I'm missing something, but that's how I see it right now.

>
>>
>> If you didn't already have an example, I might try to think up some
>> scenario, like another branch of a company that wanted to add, say,
>> the idea of a recipe contributor, or wanted to add the idea of
>> favorite recipes or recipe ratings. That particular idea might be
>> undesirable for the imaginary people using the base website.
>>
>> What you have already might work though. While it's hard to imagine
>> someone not wanting search, perhaps other teams have a more
>> complicated search method. You are sharing/reusing the base
>> webservice, and you just want to implement a very simple search. The
>> other team works from the same base code, but extends the cookbook
>> with a much more complex search method that also incorporates ideas
>> that you don't need and don't want, like contributors, favorites,
>> and/or ratings.
>>
>
> After letting this marinate on my head for a while long...

Read more...

Revision history for this message
Gary Poster (gary) wrote :

For posterity...

On IRC I mentioned to Salgado that in http://paste.ubuntu.com/473132/ it looks like we are discarding a checker in the change starting in line 23. He gave me http://paste.ubuntu.com/473195/ which was what I was looking for.

152. By Guilherme Salgado

Fix EntryResource.redacted_fields so that it doesn't assume all fields in entry.schema are directly provided by self.context

Revision history for this message
Guilherme Salgado (salgado) wrote :

On Wed, 2010-08-04 at 18:09 +0000, Gary Poster wrote:
> >
> > After letting this marinate on my head for a while longer, I thought of
> > creating an IHasComments interface that contributes a 'comments'
> > attribute to IRecipe, as if there was a separate web service which wants
> > to allow its users to comment on recipes
> > (http://paste.ubuntu.com/472821/). How does that sound to you?
>
> I like it.
>
> You *could* show what I was just talking about above, if you wanted to, by setting up a separate webservice that added comments to the original webservice. It ought to be super easy--include the configuration of the original webservice plus your extra bits. If it's not easy then that might even be construed as a problem.
>
> But that's not a requirement for this to land.
>

I've tried that by doing the same thing that the other examples do, but
it didn't work: http://paste.ubuntu.com/473253/

http://paste.ubuntu.com/473254/ has my changes, but I've also pushed
them to my branch. Any idea why it's not working?

--
Guilherme Salgado <https://launchpad.net/~salgado>

153. By Guilherme Salgado

Fix a test which I accidentally left broken

154. By Guilherme Salgado

New example webservice extending the base one using contributing interfaces

Revision history for this message
Gary Poster (gary) wrote :

Awesome! Thank you.

review: Approve
155. By Guilherme Salgado

Fix version

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'src/lazr/restful/NEWS.txt'
--- src/lazr/restful/NEWS.txt 2010-06-14 15:29:08 +0000
+++ src/lazr/restful/NEWS.txt 2010-08-05 14:04:50 +0000
@@ -2,6 +2,16 @@
2NEWS for lazr.restful2NEWS for lazr.restful
3=====================3=====================
44
50.10.0 (2010-08-05)
6===================
7
8Added the ability to mark interface A as a contributor to interface B so that
9instead of publishing A separately we will add all of A's fields and
10operations to the published version of B. Objects implementing B must be
11adaptable into A for this to work, but lazr.restful will take care of doing
12the actual adaptation before accessing fields/operations that are not directly
13provided by an object.
14
50.9.29 (2010-06-14)150.9.29 (2010-06-14)
6===================16===================
717
818
=== modified file 'src/lazr/restful/_resource.py'
--- src/lazr/restful/_resource.py 2010-06-14 15:24:20 +0000
+++ src/lazr/restful/_resource.py 2010-08-05 14:04:50 +0000
@@ -1488,11 +1488,7 @@
1488 def redacted_fields(self):1488 def redacted_fields(self):
1489 """Names the fields the current user doesn't have permission to see."""1489 """Names the fields the current user doesn't have permission to see."""
1490 failures = []1490 failures = []
1491 try:1491 orig_interfaces = self.entry._orig_interfaces
1492 checker = getChecker(self.context)
1493 except TypeError:
1494 # There's no permission checker.
1495 checker = None
1496 for name, field in getFieldsInOrder(self.entry.schema):1492 for name, field in getFieldsInOrder(self.entry.schema):
1497 try:1493 try:
1498 # Can we view the field's value? We check the1494 # Can we view the field's value? We check the
@@ -1515,15 +1511,20 @@
1515 # name. Since 'original_name' is not present, assume the1511 # name. Since 'original_name' is not present, assume the
1516 # names are the same.1512 # names are the same.
1517 original_name = name1513 original_name = name
1518 if checker is None:1514 context = orig_interfaces[name](self.context)
1519 # This is more expensive than using a Zope1515 try:
1520 # checker, but there is no checker, so either1516 checker = getChecker(context)
1521 # there is no permission control on this object,1517 except TypeError:
1522 # or permission control is implemented some other1518 # This is more expensive than using a Zope checker, but
1523 # way.1519 # there is no checker, so either there is no permission
1524 getattr(self.context, original_name)1520 # control on this object, or permission control is
1521 # implemented some other way. Also note that we use
1522 # getattr() on self.entry rather than self.context because
1523 # some of the fields in entry.schema will be provided by
1524 # adapters rather than directly by self.context.
1525 getattr(self.entry, name)
1525 else:1526 else:
1526 checker.check(self.context, original_name)1527 checker.check(context, original_name)
1527 except Unauthorized:1528 except Unauthorized:
1528 # This is an expensive operation that will make this1529 # This is an expensive operation that will make this
1529 # request more expensive still, but it happens1530 # request more expensive still, but it happens
15301531
=== modified file 'src/lazr/restful/declarations.py'
--- src/lazr/restful/declarations.py 2010-05-07 14:02:44 +0000
+++ src/lazr/restful/declarations.py 2010-08-05 14:04:50 +0000
@@ -37,6 +37,7 @@
37 ]37 ]
3838
39import copy39import copy
40import itertools
40import sys41import sys
4142
42from zope.component import getUtility, getGlobalSiteManager43from zope.component import getUtility, getGlobalSiteManager
@@ -50,6 +51,7 @@
50from zope.traversing.browser import absoluteURL51from zope.traversing.browser import absoluteURL
5152
52from lazr.delegates import Passthrough53from lazr.delegates import Passthrough
54
53from lazr.restful.fields import CollectionField, Reference55from lazr.restful.fields import CollectionField, Reference
54from lazr.restful.interface import copy_field56from lazr.restful.interface import copy_field
55from lazr.restful.interfaces import (57from lazr.restful.interfaces import (
@@ -114,8 +116,13 @@
114 return f_locals.setdefault(TAGGED_DATA, {})116 return f_locals.setdefault(TAGGED_DATA, {})
115117
116118
117def export_as_webservice_entry(singular_name=None, plural_name=None):119def export_as_webservice_entry(singular_name=None, plural_name=None,
120 contributes_to=None):
118 """Mark the content interface as exported on the web service as an entry.121 """Mark the content interface as exported on the web service as an entry.
122
123 If contributes_to is a non-empty sequence of Interfaces, this entry will
124 actually not be exported on its own but instead will contribute its
125 attributes/methods to other exported entries.
119 """126 """
120 _check_called_from_interface_def('export_as_webservice_entry()')127 _check_called_from_interface_def('export_as_webservice_entry()')
121 def mark_entry(interface):128 def mark_entry(interface):
@@ -138,7 +145,7 @@
138 interface.setTaggedValue(145 interface.setTaggedValue(
139 LAZR_WEBSERVICE_EXPORTED, dict(146 LAZR_WEBSERVICE_EXPORTED, dict(
140 type=ENTRY_TYPE, singular_name=my_singular_name,147 type=ENTRY_TYPE, singular_name=my_singular_name,
141 plural_name=my_plural_name))148 plural_name=my_plural_name, contributes_to=contributes_to))
142149
143 # Set the name of the fields that didn't specify it using the150 # Set the name of the fields that didn't specify it using the
144 # 'export_as' parameter in exported(). This must be done here,151 # 'export_as' parameter in exported(). This must be done here,
@@ -161,8 +168,6 @@
161 if tags.get('as') is None:168 if tags.get('as') is None:
162 tags['as'] = name169 tags['as'] = name
163170
164
165
166 annotate_exported_methods(interface)171 annotate_exported_methods(interface)
167 return interface172 return interface
168 addClassAdvisor(mark_entry)173 addClassAdvisor(mark_entry)
@@ -820,7 +825,6 @@
820 method, annotation_stack)825 method, annotation_stack)
821 # The mutator method must take no arguments, not counting826 # The mutator method must take no arguments, not counting
822 # arguments with values fixed by call_with().827 # arguments with values fixed by call_with().
823 signature = fromFunction(method).getSignatureInfo()
824 for version, annotations in annotation_stack.stack:828 for version, annotations in annotation_stack.stack:
825 if annotations['type'] == REMOVED_OPERATION_TYPE:829 if annotations['type'] == REMOVED_OPERATION_TYPE:
826 continue830 continue
@@ -851,7 +855,7 @@
851 "'%s' isn't exported as %s %s." % (interface.__name__, art, type))855 "'%s' isn't exported as %s %s." % (interface.__name__, art, type))
852856
853857
854def generate_entry_interfaces(interface, *versions):858def generate_entry_interfaces(interface, contributors=[], *versions):
855 """Create IEntry subinterfaces based on the tags in `interface`.859 """Create IEntry subinterfaces based on the tags in `interface`.
856860
857 :param interface: The data model interface to use as the basis861 :param interface: The data model interface to use as the basis
@@ -890,17 +894,17 @@
890 # has a set of annotations for every version. We'll make a list of894 # has a set of annotations for every version. We'll make a list of
891 # the published fields, which we'll iterate over once for each895 # the published fields, which we'll iterate over once for each
892 # version.896 # version.
893 fields = getFields(interface).items()
894 tags_for_published_fields = []897 tags_for_published_fields = []
895 for name, field in fields:898 for iface in itertools.chain([interface], contributors):
896 tag_stack = field.queryTaggedValue(LAZR_WEBSERVICE_EXPORTED)899 for name, field in getFields(iface).items():
897 if tag_stack is None:900 tag_stack = field.queryTaggedValue(LAZR_WEBSERVICE_EXPORTED)
898 # This field is not published at all.901 if tag_stack is None:
899 continue902 # This field is not published at all.
900 error_message_prefix = (903 continue
901 'Field "%s" in interface "%s": ' % (name, interface.__name__))904 error_message_prefix = (
902 _normalize_field_annotations(field, versions, error_message_prefix)905 'Field "%s" in interface "%s": ' % (name, iface.__name__))
903 tags_for_published_fields.append((name, field, tag_stack.stack))906 _normalize_field_annotations(field, versions, error_message_prefix)
907 tags_for_published_fields.append((name, field, tag_stack.stack))
904908
905 generated_interfaces = []909 generated_interfaces = []
906 for version in versions:910 for version in versions:
@@ -930,7 +934,8 @@
930 return generated_interfaces934 return generated_interfaces
931935
932936
933def generate_entry_adapters(content_interface, webservice_interfaces):937def generate_entry_adapters(
938 content_interface, contributors, webservice_interfaces):
934 """Create classes adapting from content_interface to webservice_interfaces.939 """Create classes adapting from content_interface to webservice_interfaces.
935940
936 Unlike with generate_collection_adapter and941 Unlike with generate_collection_adapter and
@@ -950,7 +955,10 @@
950 # Go through the fields and build up a picture of what this entry looks955 # Go through the fields and build up a picture of what this entry looks
951 # like for every version.956 # like for every version.
952 adapters_by_version = {}957 adapters_by_version = {}
953 for name, field in getFields(content_interface).items():958 fields = getFields(content_interface).items()
959 for version, iface in webservice_interfaces:
960 fields.extend(getFields(iface).items())
961 for name, field in fields:
954 tag_stack = field.queryTaggedValue(LAZR_WEBSERVICE_EXPORTED)962 tag_stack = field.queryTaggedValue(LAZR_WEBSERVICE_EXPORTED)
955 if tag_stack is None:963 if tag_stack is None:
956 continue964 continue
@@ -967,12 +975,34 @@
967 mutator, annotations = tags.get(975 mutator, annotations = tags.get(
968 'mutator_annotations', (None, {}))976 'mutator_annotations', (None, {}))
969977
978 # Always use the field's original_name here as we've combined
979 # fields from the content interface with fields of the webservice
980 # interfaces (where they may have different names).
981 orig_name = tags['original_name']
982
983 # Some fields may be provided by an adapter of the content class
984 # instead of being provided directly by the content class. In
985 # these cases we'd need to adapt the content class before trying
986 # to access the field, so to simplify things we always do the
987 # adaptation and rely on the fact that it will be a no-op when an
988 # we adapt an object into an interface it already provides.
989 orig_iface = content_interface
990 for contributor in contributors:
991 if orig_name in contributor:
992 orig_iface = contributor
993 assert orig_name in orig_iface, (
994 "Could not find interface where %s is defined" % orig_name)
970 if mutator is None:995 if mutator is None:
971 property = Passthrough(name, 'context')996 prop = Passthrough(orig_name, 'context', orig_iface)
972 else:997 else:
973 property = PropertyWithMutator(998 prop = PropertyWithMutator(
974 name, 'context', mutator, annotations)999 orig_name, 'context', mutator, annotations, orig_iface)
975 adapter_dict[tags['as']] = property1000 adapter_dict[tags['as']] = prop
1001
1002 # A dict mapping field names to the interfaces where they came
1003 # from.
1004 orig_interfaces = adapter_dict.setdefault('_orig_interfaces', {})
1005 orig_interfaces[name] = orig_iface
9761006
977 adapters = []1007 adapters = []
978 for version, webservice_interface in webservice_interfaces:1008 for version, webservice_interface in webservice_interfaces:
@@ -1007,8 +1037,8 @@
1007class PropertyWithMutator(Passthrough):1037class PropertyWithMutator(Passthrough):
1008 """A property with a mutator method."""1038 """A property with a mutator method."""
10091039
1010 def __init__(self, name, context, mutator, annotations):1040 def __init__(self, name, context, mutator, annotations, adaptation):
1011 super(PropertyWithMutator, self).__init__(name, context)1041 super(PropertyWithMutator, self).__init__(name, context, adaptation)
1012 self.mutator = mutator.__name__1042 self.mutator = mutator.__name__
1013 self.annotations = annotations1043 self.annotations = annotations
10141044
@@ -1016,10 +1046,13 @@
1016 """Call the mutator method to set the value."""1046 """Call the mutator method to set the value."""
1017 params = params_with_dereferenced_user(1047 params = params_with_dereferenced_user(
1018 self.annotations.get('call_with', {}))1048 self.annotations.get('call_with', {}))
1049 context = getattr(obj, self.contextvar)
1050 if self.adaptation is not None:
1051 context = self.adaptation(context)
1019 # Error checking code in mutator_for() guarantees that there1052 # Error checking code in mutator_for() guarantees that there
1020 # is one and only one non-fixed parameter for the mutator1053 # is one and only one non-fixed parameter for the mutator
1021 # method.1054 # method.
1022 getattr(obj.context, self.mutator)(new_value, **params)1055 getattr(context, self.mutator)(new_value, **params)
10231056
10241057
1025class CollectionEntrySchema:1058class CollectionEntrySchema:
@@ -1093,6 +1126,9 @@
1093class BaseResourceOperationAdapter(ResourceOperation):1126class BaseResourceOperationAdapter(ResourceOperation):
1094 """Base class for generated operation adapters."""1127 """Base class for generated operation adapters."""
10951128
1129 def _getMethod(self):
1130 return getattr(self._orig_iface(self.context), self._method_name)
1131
1096 def _getMethodParameters(self, kwargs):1132 def _getMethodParameters(self, kwargs):
1097 """Return the method parameters.1133 """Return the method parameters.
10981134
@@ -1128,7 +1164,7 @@
1128 'Cache-control', 'max-age=%i'1164 'Cache-control', 'max-age=%i'
1129 % self._export_info['cache_for'])1165 % self._export_info['cache_for'])
11301166
1131 result = getattr(self.context, self._method_name)(**params)1167 result = self._getMethod()(**params)
1132 return self.encodeResult(result)1168 return self.encodeResult(result)
11331169
11341170
@@ -1142,7 +1178,7 @@
1142 header to the URL to the created object.1178 header to the URL to the created object.
1143 """1179 """
1144 params = self._getMethodParameters(kwargs)1180 params = self._getMethodParameters(kwargs)
1145 result = getattr(self.context, self._method_name)(**params)1181 result = self._getMethod()(**params)
1146 response = self.request.response1182 response = self.request.response
1147 response.setStatus(201)1183 response.setStatus(201)
1148 response.setHeader('Location', absoluteURL(result, self.request))1184 response.setHeader('Location', absoluteURL(result, self.request))
@@ -1194,11 +1230,13 @@
1194 name = _versioned_class_name(1230 name = _versioned_class_name(
1195 '%s_%s_%s' % (prefix, method.interface.__name__, tag['as']),1231 '%s_%s_%s' % (prefix, method.interface.__name__, tag['as']),
1196 version)1232 version)
1197 class_dict = {'params' : tuple(tag['params'].values()),1233 class_dict = {
1198 'return_type' : return_type,1234 'params': tuple(tag['params'].values()),
1199 '_export_info': tag,1235 'return_type': return_type,
1200 '_method_name': method.__name__,1236 '_orig_iface': method.interface,
1201 '__doc__': method.__doc__}1237 '_export_info': tag,
1238 '_method_name': method.__name__,
1239 '__doc__': method.__doc__}
12021240
1203 if tag['type'] == 'write_operation':1241 if tag['type'] == 'write_operation':
1204 class_dict['send_modification_event'] = True1242 class_dict['send_modification_event'] = True
12051243
=== modified file 'src/lazr/restful/docs/multiversion.txt'
--- src/lazr/restful/docs/multiversion.txt 2010-06-03 13:01:01 +0000
+++ src/lazr/restful/docs/multiversion.txt 2010-08-05 14:04:50 +0000
@@ -309,6 +309,11 @@
309 ... implements(IContactEntryBeta)309 ... implements(IContactEntryBeta)
310 ... delegates(IContactEntryBeta)310 ... delegates(IContactEntryBeta)
311 ... schema = IContactEntryBeta311 ... schema = IContactEntryBeta
312 ... # This dict is normally generated by lazr.restful, but since we
313 ... # create the adapters manually here, we need to do the same for
314 ... # this dict.
315 ... _orig_interfaces = {
316 ... 'name': IContact, 'phone': IContact, 'fax': IContact}
312 ... def __init__(self, context, request):317 ... def __init__(self, context, request):
313 ... self.context = context318 ... self.context = context
314319
@@ -332,6 +337,12 @@
332 ... implements(IContactEntry10)337 ... implements(IContactEntry10)
333 ... delegates(IContactEntry10)338 ... delegates(IContactEntry10)
334 ... schema = IContactEntry10339 ... schema = IContactEntry10
340 ... # This dict is normally generated by lazr.restful, but since we
341 ... # create the adapters manually here, we need to do the same for
342 ... # this dict.
343 ... _orig_interfaces = {
344 ... 'name': IContact, 'phone_number': IContact,
345 ... 'fax_number': IContact}
335 ...346 ...
336 ... def __init__(self, context, request):347 ... def __init__(self, context, request):
337 ... self.context = context348 ... self.context = context
@@ -359,6 +370,10 @@
359 ... implements(IContactEntryDev)370 ... implements(IContactEntryDev)
360 ... delegates(IContactEntryDev)371 ... delegates(IContactEntryDev)
361 ... schema = IContactEntryDev372 ... schema = IContactEntryDev
373 ... # This dict is normally generated by lazr.restful, but since we
374 ... # create the adapters manually here, we need to do the same for
375 ... # this dict.
376 ... _orig_interfaces = {'name': IContact, 'phone_number': IContact}
362 ...377 ...
363 ... def __init__(self, context, request):378 ... def __init__(self, context, request):
364 ... self.context = context379 ... self.context = context
365380
=== modified file 'src/lazr/restful/docs/webservice-declarations.txt'
--- src/lazr/restful/docs/webservice-declarations.txt 2010-05-07 15:32:27 +0000
+++ src/lazr/restful/docs/webservice-declarations.txt 2010-08-05 14:04:50 +0000
@@ -32,7 +32,7 @@
32field, but not the inventory_number field.32field, but not the inventory_number field.
3333
34 >>> from zope.interface import Interface34 >>> from zope.interface import Interface
35 >>> from zope.schema import TextLine, Float35 >>> from zope.schema import TextLine, Float, List
36 >>> from lazr.restful.declarations import (36 >>> from lazr.restful.declarations import (
37 ... export_as_webservice_entry, exported)37 ... export_as_webservice_entry, exported)
38 >>> class IBook(Interface):38 >>> class IBook(Interface):
@@ -69,6 +69,7 @@
69 ... "%s: %s" %(key, format_value(value))69 ... "%s: %s" %(key, format_value(value))
70 ... for key, value in sorted(tag.items()))70 ... for key, value in sorted(tag.items()))
71 >>> print_export_tag(IBook)71 >>> print_export_tag(IBook)
72 contributes_to: None
72 plural_name: 'books'73 plural_name: 'books'
73 singular_name: 'book'74 singular_name: 'book'
74 type: 'entry'75 type: 'entry'
@@ -774,6 +775,37 @@
774 return_type: None775 return_type: None
775 type: 'write_operation'776 type: 'write_operation'
776777
778
779Contributing interfaces
780=======================
781
782It is possible to mix multiple interfaces into a single exported entry. This
783is specially useful when you want to export fields/methods that belong to
784adapters for your entry's class instead of to the class itself. For example,
785we can have an IDeveloper interface contributing to IUser.
786
787 >>> class IDeveloper(Interface):
788 ... export_as_webservice_entry(contributes_to=[IUser])
789 ...
790 ... programming_languages = exported(List(
791 ... title=u'Programming Languages spoken by this developer'))
792
793This will cause all the fields/methods of IDeveloper to be exported as part of
794the IBook entry instead of exporting a new entry for IDeveloper. For this to
795work you just need to ensure an object of the exported entry type can be
796adapted into the contributing interface (e.g. an IUser object can be adapted
797into IDeveloper).
798
799 >>> print_export_tag(IDeveloper)
800 contributes_to: [<InterfaceClass __builtin__.IUser>]
801 plural_name: 'developers'
802 singular_name: 'developer'
803 type: 'entry'
804
805To learn how this works, see ContributingInterfacesTestCase in
806tests/test_declarations.py.
807
808
777Generating the webservice809Generating the webservice
778=========================810=========================
779811
@@ -787,7 +819,7 @@
787819
788 >>> from lazr.restful.declarations import generate_entry_interfaces820 >>> from lazr.restful.declarations import generate_entry_interfaces
789 >>> [[version, entry_interface]] = generate_entry_interfaces(821 >>> [[version, entry_interface]] = generate_entry_interfaces(
790 ... IBook, 'beta')822 ... IBook, [], 'beta')
791823
792The created interface is named with 'Entry' appended to the original824The created interface is named with 'Entry' appended to the original
793name, and is in the same module825name, and is in the same module
@@ -836,14 +868,14 @@
836868
837 >>> class SimpleNotExported(Interface):869 >>> class SimpleNotExported(Interface):
838 ... """Interface not exported."""870 ... """Interface not exported."""
839 >>> generate_entry_interfaces(SimpleNotExported, 'beta')871 >>> generate_entry_interfaces(SimpleNotExported, [], 'beta')
840 Traceback (most recent call last):872 Traceback (most recent call last):
841 ...873 ...
842 TypeError: 'SimpleNotExported' isn't tagged for webservice export.874 TypeError: 'SimpleNotExported' isn't tagged for webservice export.
843875
844The interface must also be exported as an entry:876The interface must also be exported as an entry:
845877
846 >>> generate_entry_interfaces(IBookSet, 'beta')878 >>> generate_entry_interfaces(IBookSet, [], 'beta')
847 Traceback (most recent call last):879 Traceback (most recent call last):
848 ...880 ...
849 TypeError: 'IBookSet' isn't exported as an entry.881 TypeError: 'IBookSet' isn't exported as an entry.
@@ -854,7 +886,7 @@
854886
855 >>> from lazr.restful.declarations import generate_entry_adapters887 >>> from lazr.restful.declarations import generate_entry_adapters
856 >>> entry_adapter_factories = generate_entry_adapters(888 >>> entry_adapter_factories = generate_entry_adapters(
857 ... IBook, [('beta', entry_interface)])889 ... IBook, [], [('beta', entry_interface)])
858890
859generate_entry_adapters() generates an adapter for every version of891generate_entry_adapters() generates an adapter for every version of
860the web service (see a test for it below, in "Versioned892the web service (see a test for it below, in "Versioned
@@ -899,18 +931,14 @@
899utilities providing basic information about the web service. This one931utilities providing basic information about the web service. This one
900is just a dummy.932is just a dummy.
901933
934 >>> from lazr.restful.testing.helpers import TestWebServiceConfiguration
902 >>> from zope.component import provideUtility935 >>> from zope.component import provideUtility
903 >>> from lazr.restful.interfaces import IWebServiceConfiguration936 >>> from lazr.restful.interfaces import IWebServiceConfiguration
904 >>> class MyWebServiceConfiguration:937 >>> class MyWebServiceConfiguration(TestWebServiceConfiguration):
905 ... implements(IWebServiceConfiguration)
906 ... view_permission = "lazr.View"
907 ... active_versions = ["beta", "1.0", "2.0", "3.0"]938 ... active_versions = ["beta", "1.0", "2.0", "3.0"]
908 ... last_version_with_mutator_named_operations = "1.0"939 ... last_version_with_mutator_named_operations = "1.0"
909 ... code_revision = "1.0b"940 ... code_revision = "1.0b"
910 ... default_batch_size = 50941 ... default_batch_size = 50
911 ...
912 ... def get_request_user(self):
913 ... return 'A user'
914 >>> provideUtility(MyWebServiceConfiguration(), IWebServiceConfiguration)942 >>> provideUtility(MyWebServiceConfiguration(), IWebServiceConfiguration)
915943
916We must also set up the ability to create versioned requests. This web944We must also set up the ability to create versioned requests. This web
@@ -959,14 +987,15 @@
959It's an error to call this function on an interface not exported on the987It's an error to call this function on an interface not exported on the
960web service:988web service:
961989
962 >>> generate_entry_adapters(SimpleNotExported, ('beta', entry_interface))990 >>> generate_entry_adapters(
991 ... SimpleNotExported, [], ('beta', entry_interface))
963 Traceback (most recent call last):992 Traceback (most recent call last):
964 ...993 ...
965 TypeError: 'SimpleNotExported' isn't tagged for webservice export.994 TypeError: 'SimpleNotExported' isn't tagged for webservice export.
966995
967Or exported as a collection:996Or exported as a collection:
968997
969 >>> generate_entry_adapters(IBookSet, ('beta', entry_interface))998 >>> generate_entry_adapters(IBookSet, [], ('beta', entry_interface))
970 Traceback (most recent call last):999 Traceback (most recent call last):
971 ...1000 ...
972 TypeError: 'IBookSet' isn't exported as an entry.1001 TypeError: 'IBookSet' isn't exported as an entry.
@@ -1142,6 +1171,7 @@
1142 >>> print_params(write_method_adapter_factory.params)1171 >>> print_params(write_method_adapter_factory.params)
11431172
1144 >>> class BookOnSteroids(Book):1173 >>> class BookOnSteroids(Book):
1174 ... implements(IBookOnSteroids)
1145 ... def checkout(self, who, kind):1175 ... def checkout(self, who, kind):
1146 ... print "%s did a %s check out of '%s'." % (1176 ... print "%s did a %s check out of '%s'." % (
1147 ... who, kind, self.title)1177 ... who, kind, self.title)
@@ -1273,7 +1303,7 @@
1273 ... @export_destructor_operation()1303 ... @export_destructor_operation()
1274 ... def destroy():1304 ... def destroy():
1275 ... pass1305 ... pass
1276 >>> ignored = generate_entry_interfaces(IHasText, 'beta')1306 >>> ignored = generate_entry_interfaces(IHasText, [], 'beta')
12771307
1278A destructor method cannot take any free arguments.1308A destructor method cannot take any free arguments.
12791309
@@ -1299,7 +1329,7 @@
1299 ... @call_with(argument="fixed value")1329 ... @call_with(argument="fixed value")
1300 ... def destroy(argument):1330 ... def destroy(argument):
1301 ... pass1331 ... pass
1302 >>> ignored = generate_entry_interfaces(IHasText, 'beta')1332 >>> ignored = generate_entry_interfaces(IHasText, [], 'beta')
13031333
1304An entry cannot have more than one destructor.1334An entry cannot have more than one destructor.
13051335
@@ -1315,7 +1345,7 @@
1315 ... @export_destructor_operation()1345 ... @export_destructor_operation()
1316 ... def destroy2():1346 ... def destroy2():
1317 ... pass1347 ... pass
1318 >>> generate_entry_interfaces(IHasText, 'beta')1348 >>> generate_entry_interfaces(IHasText, [], 'beta')
1319 Traceback (most recent call last):1349 Traceback (most recent call last):
1320 ...1350 ...
1321 TypeError: An entry can only have one destructor method for1351 TypeError: An entry can only have one destructor method for
@@ -1354,10 +1384,10 @@
1354Generate the entry interface and adapter...1384Generate the entry interface and adapter...
13551385
1356 >>> hastext_entry_interfaces = generate_entry_interfaces(1386 >>> hastext_entry_interfaces = generate_entry_interfaces(
1357 ... IHasText, 'beta')1387 ... IHasText, [], 'beta')
1358 >>> [(beta, hastext_entry_interface)] = hastext_entry_interfaces1388 >>> [(beta, hastext_entry_interface)] = hastext_entry_interfaces
1359 >>> [(beta, hastext_entry_adapter_factory)] = generate_entry_adapters(1389 >>> [(beta, hastext_entry_adapter_factory)] = generate_entry_adapters(
1360 ... IHasText, hastext_entry_interfaces)1390 ... IHasText, [], hastext_entry_interfaces)
13611391
1362 >>> obj = HasText()1392 >>> obj = HasText()
1363 >>> hastext_entry_adapter = hastext_entry_adapter_factory(obj, request)1393 >>> hastext_entry_adapter = hastext_entry_adapter_factory(obj, request)
@@ -1489,7 +1519,7 @@
1489 ...1519 ...
1490 >>> class CachedBookSet(BookSet):1520 >>> class CachedBookSet(BookSet):
1491 ... """Simple ICachedBookSet implementation."""1521 ... """Simple ICachedBookSet implementation."""
1492 ... implements(IBookSet)1522 ... implements(ICachedBookSet)
1493 ...1523 ...
1494 ... def getAllBooks(self):1524 ... def getAllBooks(self):
1495 ... return self.books1525 ... return self.books
@@ -1660,7 +1690,7 @@
16601690
1661 >>> versions = ['beta', '1.0', '2.0', '3.0']1691 >>> versions = ['beta', '1.0', '2.0', '3.0']
1662 >>> versions_and_interfaces = generate_entry_interfaces(1692 >>> versions_and_interfaces = generate_entry_interfaces(
1663 ... IMultiVersionEntry, *versions)1693 ... IMultiVersionEntry, [], *versions)
16641694
1665 >>> for version, interface in versions_and_interfaces:1695 >>> for version, interface in versions_and_interfaces:
1666 ... print version1696 ... print version
@@ -1718,7 +1748,7 @@
1718classes.1748classes.
17191749
1720 >>> entry_adapters = generate_entry_adapters(1750 >>> entry_adapters = generate_entry_adapters(
1721 ... IMultiVersionEntry, versions_and_interfaces)1751 ... IMultiVersionEntry, [], versions_and_interfaces)
17221752
1723 >>> for version, adapter in entry_adapters:1753 >>> for version, adapter in entry_adapters:
1724 ... print version1754 ... print version
@@ -1827,7 +1857,7 @@
1827version, then 'bar' inherits behavior from 'foo'.1857version, then 'bar' inherits behavior from 'foo'.
18281858
1829 >>> foo, bar = generate_entry_interfaces(1859 >>> foo, bar = generate_entry_interfaces(
1830 ... IAmbiguousMultiVersion, 'foo', 'bar')1860 ... IAmbiguousMultiVersion, [], 'foo', 'bar')
18311861
1832 >>> print foo[0]1862 >>> print foo[0]
1833 foo1863 foo
@@ -1855,7 +1885,7 @@
1855 ... ('bar', dict(exported_as='bar_name')))1885 ... ('bar', dict(exported_as='bar_name')))
18561886
1857 >>> bar, foo = generate_entry_interfaces(1887 >>> bar, foo = generate_entry_interfaces(
1858 ... IAmbiguousMultiVersion, 'bar', 'foo')1888 ... IAmbiguousMultiVersion, [], 'bar', 'foo')
18591889
1860 >>> print bar[0]1890 >>> print bar[0]
1861 bar1891 bar
@@ -1888,7 +1918,7 @@
1888 ... ('1.0', dict(exported_as='bar')))1918 ... ('1.0', dict(exported_as='bar')))
18891919
1890 >>> generate_entry_interfaces(1920 >>> generate_entry_interfaces(
1891 ... INonexistentVersionEntry, 'beta', '1.0')1921 ... INonexistentVersionEntry, [], 'beta', '1.0')
1892 Traceback (most recent call last):1922 Traceback (most recent call last):
1893 ...1923 ...
1894 ValueError: Field "field" in interface "INonexistentVersionEntry":1924 ValueError: Field "field" in interface "INonexistentVersionEntry":
@@ -1904,7 +1934,7 @@
1904 ... ('1.0', dict(exported_as='bar')),1934 ... ('1.0', dict(exported_as='bar')),
1905 ... ('2.0', dict(exported_as='foo')))1935 ... ('2.0', dict(exported_as='foo')))
19061936
1907 >>> generate_entry_interfaces(IWrongOrderEntry, '1.0', '2.0')1937 >>> generate_entry_interfaces(IWrongOrderEntry, [], '1.0', '2.0')
1908 Traceback (most recent call last):1938 Traceback (most recent call last):
1909 ...1939 ...
1910 ValueError: Field "..." in interface "IWrongOrderEntry":1940 ValueError: Field "..." in interface "IWrongOrderEntry":
@@ -1920,7 +1950,7 @@
1920 ... ('beta', dict(exported_as='another_beta_name')),1950 ... ('beta', dict(exported_as='another_beta_name')),
1921 ... ('beta', dict(exported_as='beta_name')))1951 ... ('beta', dict(exported_as='beta_name')))
19221952
1923 >>> generate_entry_interfaces(IDuplicateEntry, 'beta', '1.0')1953 >>> generate_entry_interfaces(IDuplicateEntry, [], 'beta', '1.0')
1924 Traceback (most recent call last):1954 Traceback (most recent call last):
1925 ...1955 ...
1926 ValueError: Field "field" in interface "IDuplicateEntry":1956 ValueError: Field "field" in interface "IDuplicateEntry":
@@ -1937,7 +1967,7 @@
1937 ... ('beta', dict(exported_as='beta_name')),1967 ... ('beta', dict(exported_as='beta_name')),
1938 ... exported_as='earliest_name')1968 ... exported_as='earliest_name')
19391969
1940 >>> generate_entry_interfaces(IDuplicateEntry, 'beta', '1.0')1970 >>> generate_entry_interfaces(IDuplicateEntry, [], 'beta', '1.0')
1941 Traceback (most recent call last):1971 Traceback (most recent call last):
1942 ...1972 ...
1943 ValueError: Field "field" in interface "IDuplicateEntry":1973 ValueError: Field "field" in interface "IDuplicateEntry":
@@ -1977,7 +2007,7 @@
1977 ... ('beta', dict(exported_as='unchanging_name')))2007 ... ('beta', dict(exported_as='unchanging_name')))
19782008
1979 >>> [version for version, tags in2009 >>> [version for version, tags in
1980 ... generate_entry_interfaces(IUnchangingEntry, *versions)]2010 ... generate_entry_interfaces(IUnchangingEntry, [], *versions)]
1981 ['beta', '1.0', '2.0', '3.0']2011 ['beta', '1.0', '2.0', '3.0']
19822012
1983Named operations2013Named operations
@@ -2291,7 +2321,7 @@
2291 ... pass2321 ... pass
22922322
2293 >>> ignored = generate_entry_interfaces(2323 >>> ignored = generate_entry_interfaces(
2294 ... IDifferentMutators, 'beta', '1.0')2324 ... IDifferentMutators, [], 'beta', '1.0')
22952325
2296But you can't define two mutators for the same field in the same version.2326But you can't define two mutators for the same field in the same version.
22972327
@@ -2342,7 +2372,7 @@
2342 ... pass2372 ... pass
23432373
2344 >>> generate_entry_interfaces(2374 >>> generate_entry_interfaces(
2345 ... IImplicitAndExplicitMutator, 'beta', '1.0')2375 ... IImplicitAndExplicitMutator, [], 'beta', '1.0')
2346 Traceback (most recent call last):2376 Traceback (most recent call last):
2347 ...2377 ...
2348 ValueError: Field "field" in interface2378 ValueError: Field "field" in interface
@@ -2377,7 +2407,7 @@
2377 ... pass2407 ... pass
23782408
2379 >>> ignored = generate_entry_interfaces(2409 >>> ignored = generate_entry_interfaces(
2380 ... IImplicitAndExplicitMutator, 'alpha', 'beta', '1.0')2410 ... IImplicitAndExplicitMutator, [], 'alpha', 'beta', '1.0')
23812411
2382Destructor operations2412Destructor operations
2383*********************2413*********************
@@ -2402,7 +2432,7 @@
2402 ... """Another destructor method."""2432 ... """Another destructor method."""
24032433
2404 >>> ignore = generate_entry_interfaces(2434 >>> ignore = generate_entry_interfaces(
2405 ... IGoodDestructorEntry, 'beta', '1.0')2435 ... IGoodDestructorEntry, [], 'beta', '1.0')
24062436
2407In this next example, the destructor is removed in 1.0 and2437In this next example, the destructor is removed in 1.0 and
2408added back in 2.0. The 2.0 version does not inherit any values from2438added back in 2.0. The 2.0 version does not inherit any values from
@@ -2476,33 +2506,7 @@
2476(Put the interface in a module where it will be possible for the ZCML2506(Put the interface in a module where it will be possible for the ZCML
2477handler to inspect.)2507handler to inspect.)
24782508
2479 >>> import sys2509 >>> from lazr.restful.testing.helpers import register_test_module
2480 >>> from types import ModuleType
2481 >>> def create_test_module(name, *contents):
2482 ... """Defines a new module and adds it to sys.modules."""
2483 ... new_module = ModuleType(name)
2484 ... sys.modules['lazr.restful.' + name] = new_module
2485 ... for object in contents:
2486 ... setattr(new_module, object.__name__, object)
2487 ... return new_module
2488
2489 >>> from zope.configuration import xmlconfig
2490 >>> def register_test_module(name, *contents):
2491 ... new_module = create_test_module(name, *contents)
2492 ... try:
2493 ... zcmlcontext = xmlconfig.string("""
2494 ... <configure
2495 ... xmlns:webservice=
2496 ... "http://namespaces.canonical.com/webservice">
2497 ... <include package="lazr.restful" file="meta.zcml" />
2498 ... <webservice:register module="lazr.restful.%s" />
2499 ... </configure>
2500 ... """ % name)
2501 ... except Exception, e:
2502 ... del sys.modules['lazr.restful.' + name]
2503 ... raise e
2504 ... return new_module
2505
2506 >>> bookexample = register_test_module(2510 >>> bookexample = register_test_module(
2507 ... 'bookexample', IBook, IBookSet, IBookOnSteroids,2511 ... 'bookexample', IBook, IBookSet, IBookOnSteroids,
2508 ... IBookSetOnSteroids, ISimpleComment, InvalidEmail)2512 ... IBookSetOnSteroids, ISimpleComment, InvalidEmail)
@@ -2559,6 +2563,7 @@
25592563
2560(Clean-up.)2564(Clean-up.)
25612565
2566 >>> import sys
2562 >>> del bookexample2567 >>> del bookexample
2563 >>> del sys.modules['lazr.restful.bookexample']2568 >>> del sys.modules['lazr.restful.bookexample']
25642569
@@ -2878,3 +2883,4 @@
2878 <...POST_IBetaMutatorEntry3_set_value_beta...>2883 <...POST_IBetaMutatorEntry3_set_value_beta...>
2879 >>> operation_for(context, '3.0', 'set_value')2884 >>> operation_for(context, '3.0', 'set_value')
2880 <...POST_IBetaMutatorEntry3_set_value_beta...>2885 <...POST_IBetaMutatorEntry3_set_value_beta...>
2886
28812887
=== modified file 'src/lazr/restful/docs/webservice.txt'
--- src/lazr/restful/docs/webservice.txt 2010-05-20 18:11:11 +0000
+++ src/lazr/restful/docs/webservice.txt 2010-08-05 14:04:50 +0000
@@ -636,11 +636,23 @@
636 >>> from lazr.restful import Entry636 >>> from lazr.restful import Entry
637 >>> from lazr.restful.testing.webservice import FakeRequest637 >>> from lazr.restful.testing.webservice import FakeRequest
638638
639 >>> from UserDict import UserDict
640 >>> class FakeDict(UserDict):
641 ... def __init__(self, interface):
642 ... UserDict.__init__(self)
643 ... self.interface = interface
644 ... def __getitem__(self, key):
645 ... return self.interface
646
639 >>> class AuthorEntry(Entry):647 >>> class AuthorEntry(Entry):
640 ... """An author, as exposed through the web service."""648 ... """An author, as exposed through the web service."""
641 ... adapts(IAuthor)649 ... adapts(IAuthor)
642 ... delegates(IAuthorEntry)650 ... delegates(IAuthorEntry)
643 ... schema = IAuthorEntry651 ... schema = IAuthorEntry
652 ... # This dict is normally generated by lazr.restful, but since we
653 ... # create the adapters manually here, we need to do the same for
654 ... # this dict.
655 ... _orig_interfaces = FakeDict(IAuthor)
644656
645 >>> request = FakeRequest()657 >>> request = FakeRequest()
646 >>> verifyObject(IAuthorEntry, AuthorEntry(A1, request))658 >>> verifyObject(IAuthorEntry, AuthorEntry(A1, request))
@@ -675,20 +687,36 @@
675 ... """A cookbook, as exposed through the web service."""687 ... """A cookbook, as exposed through the web service."""
676 ... delegates(ICookbookEntry)688 ... delegates(ICookbookEntry)
677 ... schema = ICookbookEntry689 ... schema = ICookbookEntry
690 ... # This dict is normally generated by lazr.restful, but since we
691 ... # create the adapters manually here, we need to do the same for
692 ... # this dict.
693 ... _orig_interfaces = FakeDict(ICookbook)
678694
679 >>> class DishEntry(Entry):695 >>> class DishEntry(Entry):
680 ... """A dish, as exposed through the web service."""696 ... """A dish, as exposed through the web service."""
681 ... delegates(IDishEntry)697 ... delegates(IDishEntry)
682 ... schema = IDishEntry698 ... schema = IDishEntry
699 ... # This dict is normally generated by lazr.restful, but since we
700 ... # create the adapters manually here, we need to do the same for
701 ... # this dict.
702 ... _orig_interfaces = FakeDict(IDish)
683703
684 >>> class CommentEntry(Entry):704 >>> class CommentEntry(Entry):
685 ... """A comment, as exposed through the web service."""705 ... """A comment, as exposed through the web service."""
686 ... delegates(ICommentEntry)706 ... delegates(ICommentEntry)
687 ... schema = ICommentEntry707 ... schema = ICommentEntry
708 ... # This dict is normally generated by lazr.restful, but since we
709 ... # create the adapters manually here, we need to do the same for
710 ... # this dict.
711 ... _orig_interfaces = FakeDict(IComment)
688712
689 >>> class RecipeEntry(Entry):713 >>> class RecipeEntry(Entry):
690 ... delegates(IRecipeEntry)714 ... delegates(IRecipeEntry)
691 ... schema = IRecipeEntry715 ... schema = IRecipeEntry
716 ... # This dict is normally generated by lazr.restful, but since we
717 ... # create the adapters manually here, we need to do the same for
718 ... # this dict.
719 ... _orig_interfaces = FakeDict(IRecipe)
692720
693We need to register these entries as a multiadapter adapter from721We need to register these entries as a multiadapter adapter from
694(e.g.) ``IAuthor`` and ``IWebServiceClientRequest`` to (e.g.)722(e.g.) ``IAuthor`` and ``IWebServiceClientRequest`` to (e.g.)
695723
=== modified file 'src/lazr/restful/example/base/tests/test_integration.py'
--- src/lazr/restful/example/base/tests/test_integration.py 2009-09-01 13:12:32 +0000
+++ src/lazr/restful/example/base/tests/test_integration.py 2010-08-05 14:04:50 +0000
@@ -29,6 +29,7 @@
29 doctest.REPORT_NDIFF)29 doctest.REPORT_NDIFF)
3030
31class FunctionalLayer:31class FunctionalLayer:
32 allow_teardown = False
32 zcml = os.path.abspath(resource_filename('lazr.restful', 'ftesting.zcml'))33 zcml = os.path.abspath(resource_filename('lazr.restful', 'ftesting.zcml'))
33zcml_layer(FunctionalLayer)34zcml_layer(FunctionalLayer)
3435
3536
=== added directory 'src/lazr/restful/example/base_extended'
=== added file 'src/lazr/restful/example/base_extended/README.txt'
--- src/lazr/restful/example/base_extended/README.txt 1970-01-01 00:00:00 +0000
+++ src/lazr/restful/example/base_extended/README.txt 2010-08-05 14:04:50 +0000
@@ -0,0 +1,22 @@
1This is a very simple webservice that demonstrates how to use contributing
2interfaces to add fields to an existing webservice using a plugin-like
3pattern.
4
5Here we've just added a 'comments' field to the IRecipe entry.
6
7 >>> from lazr.restful.testing.webservice import WebServiceCaller
8 >>> webservice = WebServiceCaller(domain='cookbooks.dev')
9
10 # The comments DB for this webservice is empty so we'll add some comments
11 # to the recipe with ID=1
12 >>> from lazr.restful.example.base_extended.comments import comments_db
13 >>> comments_db[1] = ['Comment 1', 'Comment 2']
14
15And as we can see below, a recipe's representation now include its comments.
16
17 >>> print "\n".join(webservice.get('/recipes/1').jsonBody()['comments'])
18 Comment 1
19 Comment 2
20
21 >>> webservice.get('/recipes/2').jsonBody()['comments']
22 []
023
=== added file 'src/lazr/restful/example/base_extended/__init__.py'
--- src/lazr/restful/example/base_extended/__init__.py 1970-01-01 00:00:00 +0000
+++ src/lazr/restful/example/base_extended/__init__.py 2010-08-05 14:04:50 +0000
@@ -0,0 +1,3 @@
1"""A simple webservice which uses contributing interfaces to extend the 'base'
2 one.
3"""
04
=== added file 'src/lazr/restful/example/base_extended/comments.py'
--- src/lazr/restful/example/base_extended/comments.py 1970-01-01 00:00:00 +0000
+++ src/lazr/restful/example/base_extended/comments.py 2010-08-05 14:04:50 +0000
@@ -0,0 +1,32 @@
1
2from zope.component import adapts
3from zope.interface import implements, Interface
4from zope.schema import List, Text
5
6from lazr.restful.example.base.interfaces import IRecipe
7
8from lazr.restful.declarations import (
9 export_as_webservice_entry, exported)
10
11
12class IHasComments(Interface):
13 export_as_webservice_entry(contributes_to=[IRecipe])
14 comments = exported(
15 List(title=u'Comments made by users', value_type=Text()))
16
17
18class RecipeToHasCommentsAdapter:
19 implements(IHasComments)
20 adapts(IRecipe)
21
22 def __init__(self, recipe):
23 self.recipe = recipe
24
25 @property
26 def comments(self):
27 return comments_db.get(self.recipe.id, [])
28
29
30# A fake database for storing comments. Monkey-patch this to test the
31# IHasComments adapter.
32comments_db = {}
033
=== added file 'src/lazr/restful/example/base_extended/site.zcml'
--- src/lazr/restful/example/base_extended/site.zcml 1970-01-01 00:00:00 +0000
+++ src/lazr/restful/example/base_extended/site.zcml 2010-08-05 14:04:50 +0000
@@ -0,0 +1,18 @@
1<configure
2 xmlns="http://namespaces.zope.org/zope"
3 xmlns:webservice="http://namespaces.canonical.com/webservice"
4 xmlns:grok="http://namespaces.zope.org/grok">
5
6 <include package="lazr.restful" file="basic-site.zcml" />
7
8 <include package="lazr.restful.example.base" />
9
10 <webservice:register module="lazr.restful.example.base_extended.comments" />
11 <grok:grok package="lazr.restful.example.base_extended" />
12
13 <adapter
14 factory="lazr.restful.example.base_extended.comments.RecipeToHasCommentsAdapter" />
15
16 <securityPolicy
17 component="zope.security.simplepolicies.PermissiveSecurityPolicy" />
18</configure>
019
=== added directory 'src/lazr/restful/example/base_extended/tests'
=== added file 'src/lazr/restful/example/base_extended/tests/__init__.py'
=== added file 'src/lazr/restful/example/base_extended/tests/test_integration.py'
--- src/lazr/restful/example/base_extended/tests/test_integration.py 1970-01-01 00:00:00 +0000
+++ src/lazr/restful/example/base_extended/tests/test_integration.py 2010-08-05 14:04:50 +0000
@@ -0,0 +1,38 @@
1# Copyright 20010 Canonical Ltd. All rights reserved.
2
3"""Test harness for LAZR doctests."""
4
5__metaclass__ = type
6__all__ = []
7
8import os
9import doctest
10from pkg_resources import resource_filename
11
12from van.testing.layer import zcml_layer, wsgi_intercept_layer
13
14from lazr.restful.example.base.tests.test_integration import (
15 CookbookWebServiceTestPublication, DOCTEST_FLAGS)
16from lazr.restful.testing.webservice import WebServiceApplication
17
18
19class FunctionalLayer:
20 allow_teardown = False
21 zcml = os.path.abspath(resource_filename(
22 'lazr.restful.example.base_extended', 'site.zcml'))
23zcml_layer(FunctionalLayer)
24
25
26class WSGILayer(FunctionalLayer):
27 @classmethod
28 def make_application(self):
29 return WebServiceApplication({}, CookbookWebServiceTestPublication)
30wsgi_intercept_layer(WSGILayer)
31
32
33def additional_tests():
34 """See `zope.testing.testrunner`."""
35 tests = ['../README.txt']
36 suite = doctest.DocFileSuite(optionflags=DOCTEST_FLAGS, *tests)
37 suite.layer = WSGILayer
38 return suite
039
=== modified file 'src/lazr/restful/metazcml.py'
--- src/lazr/restful/metazcml.py 2010-03-03 13:24:04 +0000
+++ src/lazr/restful/metazcml.py 2010-08-05 14:04:50 +0000
@@ -7,6 +7,7 @@
77
88
9import inspect9import inspect
10import itertools
1011
11from zope.component import getUtility12from zope.component import getUtility
12from zope.component.zcml import handler13from zope.component.zcml import handler
@@ -16,14 +17,15 @@
1617
1718
18from lazr.restful.declarations import (19from lazr.restful.declarations import (
19 LAZR_WEBSERVICE_EXPORTED, OPERATION_TYPES, REMOVED_OPERATION_TYPE,20 COLLECTION_TYPE, ENTRY_TYPE, LAZR_WEBSERVICE_EXPORTED, OPERATION_TYPES,
20 generate_collection_adapter, generate_entry_adapters,21 REMOVED_OPERATION_TYPE, generate_collection_adapter,
21 generate_entry_interfaces, generate_operation_adapter)22 generate_entry_adapters, generate_entry_interfaces,
23 generate_operation_adapter)
22from lazr.restful.error import WebServiceExceptionView24from lazr.restful.error import WebServiceExceptionView
2325
24from lazr.restful.interfaces import (26from lazr.restful.interfaces import (
25 ICollection, IEntry, IResourceDELETEOperation, IResourceGETOperation,27 ICollection, IEntry, IResourceDELETEOperation, IResourceGETOperation,
26 IResourceOperation, IResourcePOSTOperation, IWebServiceClientRequest,28 IResourcePOSTOperation, IWebServiceClientRequest,
27 IWebServiceConfiguration, IWebServiceVersion)29 IWebServiceConfiguration, IWebServiceVersion)
2830
2931
@@ -34,7 +36,7 @@
34 title=u'Module which will be inspected for webservice declarations')36 title=u'Module which will be inspected for webservice declarations')
3537
3638
37def generate_and_register_entry_adapters(interface, info):39def generate_and_register_entry_adapters(interface, info, contributors):
38 """Generate an entry adapter for every version of the web service.40 """Generate an entry adapter for every version of the web service.
3941
40 This code generates an IEntry subinterface for every version, each42 This code generates an IEntry subinterface for every version, each
@@ -48,9 +50,10 @@
48 versions = list(config.active_versions)50 versions = list(config.active_versions)
4951
50 # Generate an interface and an adapter for every version.52 # Generate an interface and an adapter for every version.
51 web_interfaces = generate_entry_interfaces(interface, *versions)53 web_interfaces = generate_entry_interfaces(
52 web_factories = generate_entry_adapters(interface, web_interfaces)54 interface, contributors, *versions)
53 provides = IEntry55 web_factories = generate_entry_adapters(
56 interface, contributors, web_interfaces)
54 for i in range(0, len(web_interfaces)):57 for i in range(0, len(web_interfaces)):
55 interface_version, web_interface = web_interfaces[i]58 interface_version, web_interface = web_interfaces[i]
56 factory_version, factory = web_factories[i]59 factory_version, factory = web_factories[i]
@@ -161,10 +164,79 @@
161 tag = interface.queryTaggedValue(LAZR_WEBSERVICE_EXPORTED)164 tag = interface.queryTaggedValue(LAZR_WEBSERVICE_EXPORTED)
162 if tag is None:165 if tag is None:
163 continue166 continue
164 if tag['type'] in ['entry', 'collection']:167 if tag['type'] in [ENTRY_TYPE, COLLECTION_TYPE]:
165 yield interface168 yield interface
166169
167170
171def find_interfaces_and_contributors(module):
172 """Find the interfaces and its contributors marked for export.
173
174 Return a dictionary with interfaces as keys and their contributors as
175 values.
176 """
177 interfaces_with_contributors = {}
178 for interface in find_exported_interfaces(module):
179 if issubclass(interface, Exception):
180 # Exceptions can't have interfaces, so just store it in
181 # interfaces_with_contributors and move on.
182 interfaces_with_contributors.setdefault(interface, [])
183 continue
184
185 tag = interface.getTaggedValue(LAZR_WEBSERVICE_EXPORTED)
186 if tag.get('contributes_to'):
187 # Append this interface (which is a contributing interface) to the
188 # list of contributors of every interface it contributes to.
189 for iface in tag['contributes_to']:
190 if iface.queryTaggedValue(LAZR_WEBSERVICE_EXPORTED) is None:
191 raise AttemptToContributeToNonExportedInterface(
192 "Interface %s contributes to %s, which is not "
193 "exported." % (interface.__name__, iface.__name__))
194 raise AssertionError('foo')
195 contributors = interfaces_with_contributors.setdefault(
196 iface, [])
197 contributors.append(interface)
198 else:
199 # This is a regular interface, but one of its contributing
200 # interfaces may have been processed previously and in that case a
201 # key for it would already exist in interfaces_with_contributors;
202 # that's why we use setdefault.
203 interfaces_with_contributors.setdefault(interface, [])
204
205 # For every exported interface, check that none of its names are exported
206 # in more than one contributing interface.
207 for interface, contributors in interfaces_with_contributors.items():
208 if len(contributors) == 0:
209 continue
210 names = {}
211 for iface in itertools.chain([interface], contributors):
212 for name, f in iface.namesAndDescriptions(all=True):
213 if f.queryTaggedValue(LAZR_WEBSERVICE_EXPORTED) is not None:
214 L = names.setdefault(name, [])
215 L.append(iface)
216 for name, interfaces in names.items():
217 if len(interfaces) > 1:
218 raise ConflictInContributingInterfaces(name, interfaces)
219 return interfaces_with_contributors
220
221
222class ConflictInContributingInterfaces(Exception):
223 """More than one interface tried to contribute a given attribute/method to
224 another interface.
225 """
226
227 def __init__(self, name, interfaces):
228 self.msg = (
229 "'%s' is exported in more than one contributing interface: %s"
230 % (name, ", ".join(i.__name__ for i in interfaces)))
231
232 def __str__(self):
233 return self.msg
234
235
236class AttemptToContributeToNonExportedInterface(Exception):
237 """An interface contributes to another one which is not exported."""
238
239
168def register_webservice(context, module):240def register_webservice(context, module):
169 """Generate and register web service adapters.241 """Generate and register web service adapters.
170242
@@ -175,19 +247,21 @@
175 if not inspect.ismodule(module):247 if not inspect.ismodule(module):
176 raise TypeError("module attribute must be a module: %s, %s" %248 raise TypeError("module attribute must be a module: %s, %s" %
177 module, type(module))249 module, type(module))
178 for interface in find_exported_interfaces(module):250 interfaces_with_contributors = find_interfaces_and_contributors(module)
251
252 for interface, contributors in interfaces_with_contributors.items():
179 if issubclass(interface, Exception):253 if issubclass(interface, Exception):
180 register_exception_view(context, interface)254 register_exception_view(context, interface)
181 continue255 continue
182256
183 tag = interface.getTaggedValue(LAZR_WEBSERVICE_EXPORTED)257 tag = interface.getTaggedValue(LAZR_WEBSERVICE_EXPORTED)
184 if tag['type'] == 'entry':258 if tag['type'] == ENTRY_TYPE:
185 context.action(259 context.action(
186 discriminator=('webservice entry interface', interface),260 discriminator=('webservice entry interface', interface),
187 callable=generate_and_register_entry_adapters,261 callable=generate_and_register_entry_adapters,
188 args=(interface, context.info),262 args=(interface, context.info, contributors),
189 )263 )
190 elif tag['type'] == 'collection':264 elif tag['type'] == COLLECTION_TYPE:
191 for version in tag['collection_default_content'].keys():265 for version in tag['collection_default_content'].keys():
192 factory = generate_collection_adapter(interface, version)266 factory = generate_collection_adapter(interface, version)
193 provides = ICollection267 provides = ICollection
@@ -203,11 +277,12 @@
203 raise AssertionError('Unknown export type: %s' % tag['type'])277 raise AssertionError('Unknown export type: %s' % tag['type'])
204 context.action(278 context.action(
205 discriminator=('webservice versioned operations', interface),279 discriminator=('webservice versioned operations', interface),
206 args=(context, interface),280 args=(context, interface, contributors),
207 callable=generate_and_register_webservice_operations)281 callable=generate_and_register_webservice_operations)
208282
209283
210def generate_and_register_webservice_operations(context, interface):284def generate_and_register_webservice_operations(
285 context, interface, contributors):
211 """Create and register adapters for all exported methods.286 """Create and register adapters for all exported methods.
212287
213 Different versions of the web service may publish the same288 Different versions of the web service may publish the same
@@ -228,7 +303,11 @@
228 else:303 else:
229 block_mutator_operations_as_of_version = None304 block_mutator_operations_as_of_version = None
230305
231 for name, method in interface.namesAndDescriptions(True):306 methods = interface.namesAndDescriptions(True)
307 for iface in contributors:
308 methods.extend(iface.namesAndDescriptions(True))
309
310 for name, method in methods:
232 tag = method.queryTaggedValue(LAZR_WEBSERVICE_EXPORTED)311 tag = method.queryTaggedValue(LAZR_WEBSERVICE_EXPORTED)
233 if tag is None or tag['type'] not in OPERATION_TYPES:312 if tag is None or tag['type'] not in OPERATION_TYPES:
234 # This method is not published as a named operation.313 # This method is not published as a named operation.
235314
=== added file 'src/lazr/restful/testing/helpers.py'
--- src/lazr/restful/testing/helpers.py 1970-01-01 00:00:00 +0000
+++ src/lazr/restful/testing/helpers.py 2010-08-05 14:04:50 +0000
@@ -0,0 +1,45 @@
1import sys
2from types import ModuleType
3
4from zope.configuration import xmlconfig
5from zope.interface import implements
6
7from lazr.restful.interfaces import IWebServiceConfiguration
8
9
10def create_test_module(name, *contents):
11 """Defines a new module and adds it to sys.modules."""
12 new_module = ModuleType(name)
13 sys.modules['lazr.restful.' + name] = new_module
14 for object in contents:
15 setattr(new_module, object.__name__, object)
16 return new_module
17
18
19def register_test_module(name, *contents):
20 new_module = create_test_module(name, *contents)
21 try:
22 xmlconfig.string("""
23 <configure
24 xmlns:webservice=
25 "http://namespaces.canonical.com/webservice">
26 <include package="lazr.restful" file="meta.zcml" />
27 <webservice:register module="lazr.restful.%s" />
28 </configure>
29 """ % name)
30 except Exception, e:
31 del sys.modules['lazr.restful.' + name]
32 raise e
33 return new_module
34
35
36class TestWebServiceConfiguration:
37 implements(IWebServiceConfiguration)
38 view_permission = "lazr.View"
39 active_versions = ["beta", "1.0"]
40 last_version_with_mutator_named_operations = "1.0"
41 code_revision = "1.0b"
42 default_batch_size = 50
43
44 def get_request_user(self):
45 return 'A user'
046
=== added file 'src/lazr/restful/tests/test_declarations.py'
--- src/lazr/restful/tests/test_declarations.py 1970-01-01 00:00:00 +0000
+++ src/lazr/restful/tests/test_declarations.py 2010-08-05 14:04:50 +0000
@@ -0,0 +1,335 @@
1import unittest
2
3from zope.component import (
4 adapts, getMultiAdapter, getSiteManager, getUtility, provideUtility)
5from zope.component.interfaces import ComponentLookupError
6from zope.interface import alsoProvides, Attribute, implements, Interface
7from zope.publisher.interfaces.http import IHTTPRequest
8from zope.schema import Int, Object, TextLine
9from zope.security.checker import MultiChecker, ProxyFactory
10from zope.security.management import endInteraction, newInteraction
11
12from lazr.restful.declarations import (
13 export_as_webservice_entry, exported, export_read_operation,
14 export_write_operation, mutator_for, operation_for_version,
15 operation_parameters)
16from lazr.restful.interfaces import (
17 IEntry, IResourceGETOperation, IWebServiceConfiguration,
18 IWebServiceVersion)
19from lazr.restful.marshallers import SimpleFieldMarshaller
20from lazr.restful.metazcml import (
21 AttemptToContributeToNonExportedInterface,
22 ConflictInContributingInterfaces, find_interfaces_and_contributors)
23from lazr.restful._resource import EntryAdapterUtility, EntryResource
24from lazr.restful.testing.webservice import FakeRequest
25from lazr.restful.testing.helpers import (
26 create_test_module, TestWebServiceConfiguration, register_test_module)
27
28
29class ContributingInterfacesTestCase(unittest.TestCase):
30 """Tests for interfaces that contribute fields/operations to others."""
31
32 def setUp(self):
33 provideUtility(
34 TestWebServiceConfiguration(), IWebServiceConfiguration)
35 sm = getSiteManager()
36 sm.registerUtility(
37 ITestServiceRequestBeta, IWebServiceVersion, name='beta')
38 sm.registerUtility(
39 ITestServiceRequest10, IWebServiceVersion, name='1.0')
40 sm.registerAdapter(ProductToHasBugsAdapter)
41 sm.registerAdapter(ProjectToHasBugsAdapter)
42 sm.registerAdapter(ProductToHasBranchesAdapter)
43 sm.registerAdapter(DummyFieldMarshaller)
44 self.beta_request = FakeRequest(version='beta')
45 alsoProvides(
46 self.beta_request, getUtility(IWebServiceVersion, name='beta'))
47 self.one_zero_request = FakeRequest(version='1.0')
48 alsoProvides(
49 self.one_zero_request, getUtility(IWebServiceVersion, name='1.0'))
50 self.product = Product()
51 self.project = Project()
52
53 def test_attributes(self):
54 # The bug_count field comes from IHasBugs (which IProduct does not
55 # provide, although it can be adapted into) but that field is
56 # available in the webservice (IEntry) adapter for IProduct, and that
57 # adapter knows it needs to adapt the product into an IHasBugs to
58 # access .bug_count.
59 self.product._bug_count = 10
60 register_test_module('testmod', IProduct, IHasBugs)
61 adapter = getMultiAdapter((self.product, self.beta_request), IEntry)
62 self.assertEqual(adapter.bug_count, 10)
63
64 def test_operations(self):
65 # Although getBugsCount() is not provided by IProduct, it is available
66 # on the webservice adapter as IHasBugs contributes it to IProduct.
67 self.product._bug_count = 10
68 register_test_module('testmod', IProduct, IHasBugs)
69 adapter = getMultiAdapter(
70 (self.product, self.beta_request),
71 IResourceGETOperation, name='getBugsCount')
72 self.assertEqual(adapter(), '10')
73
74 def test_contributing_interface_with_differences_between_versions(self):
75 # In the 'beta' version, IHasBranches.development_branches is exported
76 # with its original name whereas for the '1.0' version it's exported
77 # as 'development_branch_10'.
78 self.product._dev_branch = Branch('A product branch')
79 register_test_module('testmod', IProduct, IHasBranches)
80 adapter = getMultiAdapter((self.product, self.beta_request), IEntry)
81 self.assertEqual(adapter.development_branch, self.product._dev_branch)
82
83 adapter = getMultiAdapter(
84 (self.product, self.one_zero_request), IEntry)
85 self.assertEqual(
86 adapter.development_branch_10, self.product._dev_branch)
87
88 def test_mutator_for_just_one_version(self):
89 # On the 'beta' version, IHasBranches contributes a read only
90 # development_branch field, but on version '1.0' that field can be
91 # modified as we define a mutator for it.
92 self.product._dev_branch = Branch('A product branch')
93 register_test_module('testmod', IProduct, IHasBranches)
94 adapter = getMultiAdapter((self.product, self.beta_request), IEntry)
95 try:
96 adapter.development_branch = None
97 except AttributeError:
98 pass
99 else:
100 self.fail('IHasBranches.development_branch should be read-only '
101 'on the beta version')
102
103 adapter = getMultiAdapter(
104 (self.product, self.one_zero_request), IEntry)
105 self.assertEqual(
106 adapter.development_branch_10, self.product._dev_branch)
107 adapter.development_branch_10 = None
108 self.assertEqual(
109 adapter.development_branch_10, None)
110
111 def test_contributing_to_multiple_interfaces(self):
112 # Check that the webservice adapter for both IProduct and IProject
113 # have the IHasBugs attributes, as that interface contributes to them.
114 self.product._bug_count = 10
115 self.project._bug_count = 100
116 register_test_module('testmod', IProduct, IProject, IHasBugs)
117 adapter = getMultiAdapter((self.product, self.beta_request), IEntry)
118 self.assertEqual(adapter.bug_count, 10)
119
120 adapter = getMultiAdapter((self.project, self.beta_request), IEntry)
121 self.assertEqual(adapter.bug_count, 100)
122
123 def test_multiple_contributing_interfaces(self):
124 # Check that the webservice adapter for IProduct has the attributes
125 # from both IHasBugs and IHasBranches.
126 self.product._bug_count = 10
127 self.product._dev_branch = Branch('A product branch')
128 register_test_module('testmod', IProduct, IHasBugs, IHasBranches)
129 adapter = getMultiAdapter((self.product, self.beta_request), IEntry)
130 self.assertEqual(adapter.bug_count, 10)
131 self.assertEqual(adapter.development_branch, self.product._dev_branch)
132
133 def test_redacted_fields_with_no_permission_checker(self):
134 # When looking up an entry's redacted_fields, we take into account the
135 # interface where the field is defined and adapt the context to that
136 # interface before accessing that field.
137 register_test_module('testmod', IProduct, IHasBugs, IHasBranches)
138 entry_resource = EntryResource(self.product, self.beta_request)
139 self.assertEquals([], entry_resource.redacted_fields)
140
141 def test_redacted_fields_with_permission_checker(self):
142 # When looking up an entry's redacted_fields for an object which is
143 # security proxied, we use the security checker for the interface
144 # where the field is defined.
145 register_test_module('testmod', IProduct, IHasBugs, IHasBranches)
146 newInteraction()
147 try:
148 secure_product = ProxyFactory(
149 self.product,
150 checker=MultiChecker([(IProduct, 'zope.Public')]))
151 entry_resource = EntryResource(secure_product, self.beta_request)
152 self.assertEquals([], entry_resource.redacted_fields)
153 finally:
154 endInteraction()
155
156 def test_duplicate_contributed_attributes(self):
157 # We do not allow a given attribute to be contributed to a given
158 # interface by more than one contributing interface.
159 testmod = create_test_module('testmod', IProduct, IHasBugs, IHasBugs2)
160 self.assertRaises(
161 ConflictInContributingInterfaces,
162 find_interfaces_and_contributors, testmod)
163
164 def test_contributing_interface_not_exported(self):
165 # Contributing interfaces are not exported by themselves -- they only
166 # contribute their exported fields/operations to other entries.
167 class DummyHasBranches:
168 implements(IHasBranches)
169 dummy = DummyHasBranches()
170 register_test_module('testmod', IProduct, IHasBranches)
171 self.assertRaises(
172 ComponentLookupError,
173 getMultiAdapter, (dummy, self.beta_request), IEntry)
174
175 def test_cannot_contribute_to_non_exported_interface(self):
176 # A contributing interface can only contribute to exported interfaces.
177 class INotExported(Interface):
178 pass
179 class IContributor(Interface):
180 export_as_webservice_entry(contributes_to=[INotExported])
181 title = exported(TextLine(title=u'The project title'))
182 testmod = create_test_module('testmod', IContributor, INotExported)
183 self.assertRaises(
184 AttemptToContributeToNonExportedInterface,
185 find_interfaces_and_contributors, testmod)
186
187 def test_duplicate_contributed_methods(self):
188 # We do not allow a given method to be contributed to a given
189 # interface by more than one contributing interface.
190 testmod = create_test_module('testmod', IProduct, IHasBugs, IHasBugs3)
191 self.assertRaises(
192 ConflictInContributingInterfaces,
193 find_interfaces_and_contributors, testmod)
194
195 def test_ConflictInContributingInterfaces(self):
196 # The ConflictInContributingInterfaces exception states what are the
197 # contributing interfaces that caused the conflict.
198 e = ConflictInContributingInterfaces('foo', [IHasBugs, IHasBugs2])
199 expected_msg = ("'foo' is exported in more than one contributing "
200 "interface: IHasBugs, IHasBugs2")
201 self.assertEquals(str(e), expected_msg)
202
203 def test_type_name(self):
204 # Even though the generated adapters will contain stuff from various
205 # different adapters, its type name is that of the main interface and
206 # not one of its contributors.
207 register_test_module('testmod', IProduct, IHasBugs)
208 adapter = getMultiAdapter((self.product, self.beta_request), IEntry)
209 self.assertEqual(
210 'product', EntryAdapterUtility(adapter.__class__).singular_type)
211
212
213class IProduct(Interface):
214 export_as_webservice_entry()
215 title = exported(TextLine(title=u'The product title'))
216 # Need to define the two attributes below because we have a test which
217 # wraps a Product object with a security proxy and later uses adapters
218 # that access _dev_branch and _bug_count.
219 _dev_branch = Attribute('dev branch')
220 _bug_count = Attribute('bug count')
221
222
223class Product(object):
224 implements(IProduct)
225 title = 'A product'
226 _bug_count = 0
227 _dev_branch = None
228
229
230class IProject(Interface):
231 export_as_webservice_entry()
232 title = exported(TextLine(title=u'The project title'))
233
234
235class Project(object):
236 implements(IProject)
237 title = 'A project'
238 _bug_count = 0
239
240
241class IHasBugs(Interface):
242 export_as_webservice_entry(contributes_to=[IProduct, IProject])
243 bug_count = exported(Int(title=u'Number of bugs'))
244 not_exported = TextLine(title=u'Not exported')
245
246 @export_read_operation()
247 def getBugsCount():
248 pass
249
250
251class IHasBugs2(Interface):
252 export_as_webservice_entry(contributes_to=[IProduct])
253 bug_count = exported(Int(title=u'Number of bugs'))
254 not_exported = TextLine(title=u'Not exported')
255
256
257class IHasBugs3(Interface):
258 export_as_webservice_entry(contributes_to=[IProduct])
259 not_exported = TextLine(title=u'Not exported')
260
261 @export_read_operation()
262 def getBugsCount():
263 pass
264
265
266class ProductToHasBugsAdapter(object):
267 adapts(IProduct)
268 implements(IHasBugs)
269
270 def __init__(self, context):
271 self.context = context
272 self.bug_count = context._bug_count
273
274 def getBugsCount(self):
275 return self.bug_count
276
277
278class ProjectToHasBugsAdapter(ProductToHasBugsAdapter):
279 adapts(IProject)
280
281
282class IBranch(Interface):
283 name = TextLine(title=u'The branch name')
284
285
286class Branch(object):
287 implements(IBranch)
288
289 def __init__(self, name):
290 self.name = name
291
292
293class IHasBranches(Interface):
294 export_as_webservice_entry(contributes_to=[IProduct])
295 not_exported = TextLine(title=u'Not exported')
296 development_branch = exported(
297 Object(schema=IBranch, readonly=True),
298 ('1.0', dict(exported_as='development_branch_10')),
299 ('beta', dict(exported_as='development_branch')))
300
301 @mutator_for(development_branch)
302 @export_write_operation()
303 @operation_parameters(value=TextLine())
304 @operation_for_version('1.0')
305 def set_dev_branch(value):
306 pass
307
308
309class ProductToHasBranchesAdapter(object):
310 adapts(IProduct)
311 implements(IHasBranches)
312
313 def __init__(self, context):
314 self.context = context
315
316 @property
317 def development_branch(self):
318 return self.context._dev_branch
319
320 def set_dev_branch(self, value):
321 self.context._dev_branch = value
322
323
324# One of our tests will try to unmarshall some entries, but even though we
325# don't care about the unmarshalling itself, we need to register a generic
326# marshaller so that the adapter lookup doesn't fail and cause an error on the
327# test.
328class DummyFieldMarshaller(SimpleFieldMarshaller):
329 adapts(Interface, IHTTPRequest)
330
331
332class ITestServiceRequestBeta(IWebServiceVersion):
333 pass
334class ITestServiceRequest10(IWebServiceVersion):
335 pass
0336
=== modified file 'src/lazr/restful/version.txt'
--- src/lazr/restful/version.txt 2010-06-14 15:29:08 +0000
+++ src/lazr/restful/version.txt 2010-08-05 14:04:50 +0000
@@ -1,1 +1,1 @@
10.9.2910.10.0

Subscribers

People subscribed via source and target branches