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
1=== modified file 'src/lazr/restful/NEWS.txt'
2--- src/lazr/restful/NEWS.txt 2010-06-14 15:29:08 +0000
3+++ src/lazr/restful/NEWS.txt 2010-08-05 14:04:50 +0000
4@@ -2,6 +2,16 @@
5 NEWS for lazr.restful
6 =====================
7
8+0.10.0 (2010-08-05)
9+===================
10+
11+Added the ability to mark interface A as a contributor to interface B so that
12+instead of publishing A separately we will add all of A's fields and
13+operations to the published version of B. Objects implementing B must be
14+adaptable into A for this to work, but lazr.restful will take care of doing
15+the actual adaptation before accessing fields/operations that are not directly
16+provided by an object.
17+
18 0.9.29 (2010-06-14)
19 ===================
20
21
22=== modified file 'src/lazr/restful/_resource.py'
23--- src/lazr/restful/_resource.py 2010-06-14 15:24:20 +0000
24+++ src/lazr/restful/_resource.py 2010-08-05 14:04:50 +0000
25@@ -1488,11 +1488,7 @@
26 def redacted_fields(self):
27 """Names the fields the current user doesn't have permission to see."""
28 failures = []
29- try:
30- checker = getChecker(self.context)
31- except TypeError:
32- # There's no permission checker.
33- checker = None
34+ orig_interfaces = self.entry._orig_interfaces
35 for name, field in getFieldsInOrder(self.entry.schema):
36 try:
37 # Can we view the field's value? We check the
38@@ -1515,15 +1511,20 @@
39 # name. Since 'original_name' is not present, assume the
40 # names are the same.
41 original_name = name
42- if checker is None:
43- # This is more expensive than using a Zope
44- # checker, but there is no checker, so either
45- # there is no permission control on this object,
46- # or permission control is implemented some other
47- # way.
48- getattr(self.context, original_name)
49+ context = orig_interfaces[name](self.context)
50+ try:
51+ checker = getChecker(context)
52+ except TypeError:
53+ # This is more expensive than using a Zope checker, but
54+ # there is no checker, so either there is no permission
55+ # control on this object, or permission control is
56+ # implemented some other way. Also note that we use
57+ # getattr() on self.entry rather than self.context because
58+ # some of the fields in entry.schema will be provided by
59+ # adapters rather than directly by self.context.
60+ getattr(self.entry, name)
61 else:
62- checker.check(self.context, original_name)
63+ checker.check(context, original_name)
64 except Unauthorized:
65 # This is an expensive operation that will make this
66 # request more expensive still, but it happens
67
68=== modified file 'src/lazr/restful/declarations.py'
69--- src/lazr/restful/declarations.py 2010-05-07 14:02:44 +0000
70+++ src/lazr/restful/declarations.py 2010-08-05 14:04:50 +0000
71@@ -37,6 +37,7 @@
72 ]
73
74 import copy
75+import itertools
76 import sys
77
78 from zope.component import getUtility, getGlobalSiteManager
79@@ -50,6 +51,7 @@
80 from zope.traversing.browser import absoluteURL
81
82 from lazr.delegates import Passthrough
83+
84 from lazr.restful.fields import CollectionField, Reference
85 from lazr.restful.interface import copy_field
86 from lazr.restful.interfaces import (
87@@ -114,8 +116,13 @@
88 return f_locals.setdefault(TAGGED_DATA, {})
89
90
91-def export_as_webservice_entry(singular_name=None, plural_name=None):
92+def export_as_webservice_entry(singular_name=None, plural_name=None,
93+ contributes_to=None):
94 """Mark the content interface as exported on the web service as an entry.
95+
96+ If contributes_to is a non-empty sequence of Interfaces, this entry will
97+ actually not be exported on its own but instead will contribute its
98+ attributes/methods to other exported entries.
99 """
100 _check_called_from_interface_def('export_as_webservice_entry()')
101 def mark_entry(interface):
102@@ -138,7 +145,7 @@
103 interface.setTaggedValue(
104 LAZR_WEBSERVICE_EXPORTED, dict(
105 type=ENTRY_TYPE, singular_name=my_singular_name,
106- plural_name=my_plural_name))
107+ plural_name=my_plural_name, contributes_to=contributes_to))
108
109 # Set the name of the fields that didn't specify it using the
110 # 'export_as' parameter in exported(). This must be done here,
111@@ -161,8 +168,6 @@
112 if tags.get('as') is None:
113 tags['as'] = name
114
115-
116-
117 annotate_exported_methods(interface)
118 return interface
119 addClassAdvisor(mark_entry)
120@@ -820,7 +825,6 @@
121 method, annotation_stack)
122 # The mutator method must take no arguments, not counting
123 # arguments with values fixed by call_with().
124- signature = fromFunction(method).getSignatureInfo()
125 for version, annotations in annotation_stack.stack:
126 if annotations['type'] == REMOVED_OPERATION_TYPE:
127 continue
128@@ -851,7 +855,7 @@
129 "'%s' isn't exported as %s %s." % (interface.__name__, art, type))
130
131
132-def generate_entry_interfaces(interface, *versions):
133+def generate_entry_interfaces(interface, contributors=[], *versions):
134 """Create IEntry subinterfaces based on the tags in `interface`.
135
136 :param interface: The data model interface to use as the basis
137@@ -890,17 +894,17 @@
138 # has a set of annotations for every version. We'll make a list of
139 # the published fields, which we'll iterate over once for each
140 # version.
141- fields = getFields(interface).items()
142 tags_for_published_fields = []
143- for name, field in fields:
144- tag_stack = field.queryTaggedValue(LAZR_WEBSERVICE_EXPORTED)
145- if tag_stack is None:
146- # This field is not published at all.
147- continue
148- error_message_prefix = (
149- 'Field "%s" in interface "%s": ' % (name, interface.__name__))
150- _normalize_field_annotations(field, versions, error_message_prefix)
151- tags_for_published_fields.append((name, field, tag_stack.stack))
152+ for iface in itertools.chain([interface], contributors):
153+ for name, field in getFields(iface).items():
154+ tag_stack = field.queryTaggedValue(LAZR_WEBSERVICE_EXPORTED)
155+ if tag_stack is None:
156+ # This field is not published at all.
157+ continue
158+ error_message_prefix = (
159+ 'Field "%s" in interface "%s": ' % (name, iface.__name__))
160+ _normalize_field_annotations(field, versions, error_message_prefix)
161+ tags_for_published_fields.append((name, field, tag_stack.stack))
162
163 generated_interfaces = []
164 for version in versions:
165@@ -930,7 +934,8 @@
166 return generated_interfaces
167
168
169-def generate_entry_adapters(content_interface, webservice_interfaces):
170+def generate_entry_adapters(
171+ content_interface, contributors, webservice_interfaces):
172 """Create classes adapting from content_interface to webservice_interfaces.
173
174 Unlike with generate_collection_adapter and
175@@ -950,7 +955,10 @@
176 # Go through the fields and build up a picture of what this entry looks
177 # like for every version.
178 adapters_by_version = {}
179- for name, field in getFields(content_interface).items():
180+ fields = getFields(content_interface).items()
181+ for version, iface in webservice_interfaces:
182+ fields.extend(getFields(iface).items())
183+ for name, field in fields:
184 tag_stack = field.queryTaggedValue(LAZR_WEBSERVICE_EXPORTED)
185 if tag_stack is None:
186 continue
187@@ -967,12 +975,34 @@
188 mutator, annotations = tags.get(
189 'mutator_annotations', (None, {}))
190
191+ # Always use the field's original_name here as we've combined
192+ # fields from the content interface with fields of the webservice
193+ # interfaces (where they may have different names).
194+ orig_name = tags['original_name']
195+
196+ # Some fields may be provided by an adapter of the content class
197+ # instead of being provided directly by the content class. In
198+ # these cases we'd need to adapt the content class before trying
199+ # to access the field, so to simplify things we always do the
200+ # adaptation and rely on the fact that it will be a no-op when an
201+ # we adapt an object into an interface it already provides.
202+ orig_iface = content_interface
203+ for contributor in contributors:
204+ if orig_name in contributor:
205+ orig_iface = contributor
206+ assert orig_name in orig_iface, (
207+ "Could not find interface where %s is defined" % orig_name)
208 if mutator is None:
209- property = Passthrough(name, 'context')
210+ prop = Passthrough(orig_name, 'context', orig_iface)
211 else:
212- property = PropertyWithMutator(
213- name, 'context', mutator, annotations)
214- adapter_dict[tags['as']] = property
215+ prop = PropertyWithMutator(
216+ orig_name, 'context', mutator, annotations, orig_iface)
217+ adapter_dict[tags['as']] = prop
218+
219+ # A dict mapping field names to the interfaces where they came
220+ # from.
221+ orig_interfaces = adapter_dict.setdefault('_orig_interfaces', {})
222+ orig_interfaces[name] = orig_iface
223
224 adapters = []
225 for version, webservice_interface in webservice_interfaces:
226@@ -1007,8 +1037,8 @@
227 class PropertyWithMutator(Passthrough):
228 """A property with a mutator method."""
229
230- def __init__(self, name, context, mutator, annotations):
231- super(PropertyWithMutator, self).__init__(name, context)
232+ def __init__(self, name, context, mutator, annotations, adaptation):
233+ super(PropertyWithMutator, self).__init__(name, context, adaptation)
234 self.mutator = mutator.__name__
235 self.annotations = annotations
236
237@@ -1016,10 +1046,13 @@
238 """Call the mutator method to set the value."""
239 params = params_with_dereferenced_user(
240 self.annotations.get('call_with', {}))
241+ context = getattr(obj, self.contextvar)
242+ if self.adaptation is not None:
243+ context = self.adaptation(context)
244 # Error checking code in mutator_for() guarantees that there
245 # is one and only one non-fixed parameter for the mutator
246 # method.
247- getattr(obj.context, self.mutator)(new_value, **params)
248+ getattr(context, self.mutator)(new_value, **params)
249
250
251 class CollectionEntrySchema:
252@@ -1093,6 +1126,9 @@
253 class BaseResourceOperationAdapter(ResourceOperation):
254 """Base class for generated operation adapters."""
255
256+ def _getMethod(self):
257+ return getattr(self._orig_iface(self.context), self._method_name)
258+
259 def _getMethodParameters(self, kwargs):
260 """Return the method parameters.
261
262@@ -1128,7 +1164,7 @@
263 'Cache-control', 'max-age=%i'
264 % self._export_info['cache_for'])
265
266- result = getattr(self.context, self._method_name)(**params)
267+ result = self._getMethod()(**params)
268 return self.encodeResult(result)
269
270
271@@ -1142,7 +1178,7 @@
272 header to the URL to the created object.
273 """
274 params = self._getMethodParameters(kwargs)
275- result = getattr(self.context, self._method_name)(**params)
276+ result = self._getMethod()(**params)
277 response = self.request.response
278 response.setStatus(201)
279 response.setHeader('Location', absoluteURL(result, self.request))
280@@ -1194,11 +1230,13 @@
281 name = _versioned_class_name(
282 '%s_%s_%s' % (prefix, method.interface.__name__, tag['as']),
283 version)
284- class_dict = {'params' : tuple(tag['params'].values()),
285- 'return_type' : return_type,
286- '_export_info': tag,
287- '_method_name': method.__name__,
288- '__doc__': method.__doc__}
289+ class_dict = {
290+ 'params': tuple(tag['params'].values()),
291+ 'return_type': return_type,
292+ '_orig_iface': method.interface,
293+ '_export_info': tag,
294+ '_method_name': method.__name__,
295+ '__doc__': method.__doc__}
296
297 if tag['type'] == 'write_operation':
298 class_dict['send_modification_event'] = True
299
300=== modified file 'src/lazr/restful/docs/multiversion.txt'
301--- src/lazr/restful/docs/multiversion.txt 2010-06-03 13:01:01 +0000
302+++ src/lazr/restful/docs/multiversion.txt 2010-08-05 14:04:50 +0000
303@@ -309,6 +309,11 @@
304 ... implements(IContactEntryBeta)
305 ... delegates(IContactEntryBeta)
306 ... schema = IContactEntryBeta
307+ ... # This dict is normally generated by lazr.restful, but since we
308+ ... # create the adapters manually here, we need to do the same for
309+ ... # this dict.
310+ ... _orig_interfaces = {
311+ ... 'name': IContact, 'phone': IContact, 'fax': IContact}
312 ... def __init__(self, context, request):
313 ... self.context = context
314
315@@ -332,6 +337,12 @@
316 ... implements(IContactEntry10)
317 ... delegates(IContactEntry10)
318 ... schema = IContactEntry10
319+ ... # This dict is normally generated by lazr.restful, but since we
320+ ... # create the adapters manually here, we need to do the same for
321+ ... # this dict.
322+ ... _orig_interfaces = {
323+ ... 'name': IContact, 'phone_number': IContact,
324+ ... 'fax_number': IContact}
325 ...
326 ... def __init__(self, context, request):
327 ... self.context = context
328@@ -359,6 +370,10 @@
329 ... implements(IContactEntryDev)
330 ... delegates(IContactEntryDev)
331 ... schema = IContactEntryDev
332+ ... # This dict is normally generated by lazr.restful, but since we
333+ ... # create the adapters manually here, we need to do the same for
334+ ... # this dict.
335+ ... _orig_interfaces = {'name': IContact, 'phone_number': IContact}
336 ...
337 ... def __init__(self, context, request):
338 ... self.context = context
339
340=== modified file 'src/lazr/restful/docs/webservice-declarations.txt'
341--- src/lazr/restful/docs/webservice-declarations.txt 2010-05-07 15:32:27 +0000
342+++ src/lazr/restful/docs/webservice-declarations.txt 2010-08-05 14:04:50 +0000
343@@ -32,7 +32,7 @@
344 field, but not the inventory_number field.
345
346 >>> from zope.interface import Interface
347- >>> from zope.schema import TextLine, Float
348+ >>> from zope.schema import TextLine, Float, List
349 >>> from lazr.restful.declarations import (
350 ... export_as_webservice_entry, exported)
351 >>> class IBook(Interface):
352@@ -69,6 +69,7 @@
353 ... "%s: %s" %(key, format_value(value))
354 ... for key, value in sorted(tag.items()))
355 >>> print_export_tag(IBook)
356+ contributes_to: None
357 plural_name: 'books'
358 singular_name: 'book'
359 type: 'entry'
360@@ -774,6 +775,37 @@
361 return_type: None
362 type: 'write_operation'
363
364+
365+Contributing interfaces
366+=======================
367+
368+It is possible to mix multiple interfaces into a single exported entry. This
369+is specially useful when you want to export fields/methods that belong to
370+adapters for your entry's class instead of to the class itself. For example,
371+we can have an IDeveloper interface contributing to IUser.
372+
373+ >>> class IDeveloper(Interface):
374+ ... export_as_webservice_entry(contributes_to=[IUser])
375+ ...
376+ ... programming_languages = exported(List(
377+ ... title=u'Programming Languages spoken by this developer'))
378+
379+This will cause all the fields/methods of IDeveloper to be exported as part of
380+the IBook entry instead of exporting a new entry for IDeveloper. For this to
381+work you just need to ensure an object of the exported entry type can be
382+adapted into the contributing interface (e.g. an IUser object can be adapted
383+into IDeveloper).
384+
385+ >>> print_export_tag(IDeveloper)
386+ contributes_to: [<InterfaceClass __builtin__.IUser>]
387+ plural_name: 'developers'
388+ singular_name: 'developer'
389+ type: 'entry'
390+
391+To learn how this works, see ContributingInterfacesTestCase in
392+tests/test_declarations.py.
393+
394+
395 Generating the webservice
396 =========================
397
398@@ -787,7 +819,7 @@
399
400 >>> from lazr.restful.declarations import generate_entry_interfaces
401 >>> [[version, entry_interface]] = generate_entry_interfaces(
402- ... IBook, 'beta')
403+ ... IBook, [], 'beta')
404
405 The created interface is named with 'Entry' appended to the original
406 name, and is in the same module
407@@ -836,14 +868,14 @@
408
409 >>> class SimpleNotExported(Interface):
410 ... """Interface not exported."""
411- >>> generate_entry_interfaces(SimpleNotExported, 'beta')
412+ >>> generate_entry_interfaces(SimpleNotExported, [], 'beta')
413 Traceback (most recent call last):
414 ...
415 TypeError: 'SimpleNotExported' isn't tagged for webservice export.
416
417 The interface must also be exported as an entry:
418
419- >>> generate_entry_interfaces(IBookSet, 'beta')
420+ >>> generate_entry_interfaces(IBookSet, [], 'beta')
421 Traceback (most recent call last):
422 ...
423 TypeError: 'IBookSet' isn't exported as an entry.
424@@ -854,7 +886,7 @@
425
426 >>> from lazr.restful.declarations import generate_entry_adapters
427 >>> entry_adapter_factories = generate_entry_adapters(
428- ... IBook, [('beta', entry_interface)])
429+ ... IBook, [], [('beta', entry_interface)])
430
431 generate_entry_adapters() generates an adapter for every version of
432 the web service (see a test for it below, in "Versioned
433@@ -899,18 +931,14 @@
434 utilities providing basic information about the web service. This one
435 is just a dummy.
436
437+ >>> from lazr.restful.testing.helpers import TestWebServiceConfiguration
438 >>> from zope.component import provideUtility
439 >>> from lazr.restful.interfaces import IWebServiceConfiguration
440- >>> class MyWebServiceConfiguration:
441- ... implements(IWebServiceConfiguration)
442- ... view_permission = "lazr.View"
443+ >>> class MyWebServiceConfiguration(TestWebServiceConfiguration):
444 ... active_versions = ["beta", "1.0", "2.0", "3.0"]
445 ... last_version_with_mutator_named_operations = "1.0"
446 ... code_revision = "1.0b"
447 ... default_batch_size = 50
448- ...
449- ... def get_request_user(self):
450- ... return 'A user'
451 >>> provideUtility(MyWebServiceConfiguration(), IWebServiceConfiguration)
452
453 We must also set up the ability to create versioned requests. This web
454@@ -959,14 +987,15 @@
455 It's an error to call this function on an interface not exported on the
456 web service:
457
458- >>> generate_entry_adapters(SimpleNotExported, ('beta', entry_interface))
459+ >>> generate_entry_adapters(
460+ ... SimpleNotExported, [], ('beta', entry_interface))
461 Traceback (most recent call last):
462 ...
463 TypeError: 'SimpleNotExported' isn't tagged for webservice export.
464
465 Or exported as a collection:
466
467- >>> generate_entry_adapters(IBookSet, ('beta', entry_interface))
468+ >>> generate_entry_adapters(IBookSet, [], ('beta', entry_interface))
469 Traceback (most recent call last):
470 ...
471 TypeError: 'IBookSet' isn't exported as an entry.
472@@ -1142,6 +1171,7 @@
473 >>> print_params(write_method_adapter_factory.params)
474
475 >>> class BookOnSteroids(Book):
476+ ... implements(IBookOnSteroids)
477 ... def checkout(self, who, kind):
478 ... print "%s did a %s check out of '%s'." % (
479 ... who, kind, self.title)
480@@ -1273,7 +1303,7 @@
481 ... @export_destructor_operation()
482 ... def destroy():
483 ... pass
484- >>> ignored = generate_entry_interfaces(IHasText, 'beta')
485+ >>> ignored = generate_entry_interfaces(IHasText, [], 'beta')
486
487 A destructor method cannot take any free arguments.
488
489@@ -1299,7 +1329,7 @@
490 ... @call_with(argument="fixed value")
491 ... def destroy(argument):
492 ... pass
493- >>> ignored = generate_entry_interfaces(IHasText, 'beta')
494+ >>> ignored = generate_entry_interfaces(IHasText, [], 'beta')
495
496 An entry cannot have more than one destructor.
497
498@@ -1315,7 +1345,7 @@
499 ... @export_destructor_operation()
500 ... def destroy2():
501 ... pass
502- >>> generate_entry_interfaces(IHasText, 'beta')
503+ >>> generate_entry_interfaces(IHasText, [], 'beta')
504 Traceback (most recent call last):
505 ...
506 TypeError: An entry can only have one destructor method for
507@@ -1354,10 +1384,10 @@
508 Generate the entry interface and adapter...
509
510 >>> hastext_entry_interfaces = generate_entry_interfaces(
511- ... IHasText, 'beta')
512+ ... IHasText, [], 'beta')
513 >>> [(beta, hastext_entry_interface)] = hastext_entry_interfaces
514 >>> [(beta, hastext_entry_adapter_factory)] = generate_entry_adapters(
515- ... IHasText, hastext_entry_interfaces)
516+ ... IHasText, [], hastext_entry_interfaces)
517
518 >>> obj = HasText()
519 >>> hastext_entry_adapter = hastext_entry_adapter_factory(obj, request)
520@@ -1489,7 +1519,7 @@
521 ...
522 >>> class CachedBookSet(BookSet):
523 ... """Simple ICachedBookSet implementation."""
524- ... implements(IBookSet)
525+ ... implements(ICachedBookSet)
526 ...
527 ... def getAllBooks(self):
528 ... return self.books
529@@ -1660,7 +1690,7 @@
530
531 >>> versions = ['beta', '1.0', '2.0', '3.0']
532 >>> versions_and_interfaces = generate_entry_interfaces(
533- ... IMultiVersionEntry, *versions)
534+ ... IMultiVersionEntry, [], *versions)
535
536 >>> for version, interface in versions_and_interfaces:
537 ... print version
538@@ -1718,7 +1748,7 @@
539 classes.
540
541 >>> entry_adapters = generate_entry_adapters(
542- ... IMultiVersionEntry, versions_and_interfaces)
543+ ... IMultiVersionEntry, [], versions_and_interfaces)
544
545 >>> for version, adapter in entry_adapters:
546 ... print version
547@@ -1827,7 +1857,7 @@
548 version, then 'bar' inherits behavior from 'foo'.
549
550 >>> foo, bar = generate_entry_interfaces(
551- ... IAmbiguousMultiVersion, 'foo', 'bar')
552+ ... IAmbiguousMultiVersion, [], 'foo', 'bar')
553
554 >>> print foo[0]
555 foo
556@@ -1855,7 +1885,7 @@
557 ... ('bar', dict(exported_as='bar_name')))
558
559 >>> bar, foo = generate_entry_interfaces(
560- ... IAmbiguousMultiVersion, 'bar', 'foo')
561+ ... IAmbiguousMultiVersion, [], 'bar', 'foo')
562
563 >>> print bar[0]
564 bar
565@@ -1888,7 +1918,7 @@
566 ... ('1.0', dict(exported_as='bar')))
567
568 >>> generate_entry_interfaces(
569- ... INonexistentVersionEntry, 'beta', '1.0')
570+ ... INonexistentVersionEntry, [], 'beta', '1.0')
571 Traceback (most recent call last):
572 ...
573 ValueError: Field "field" in interface "INonexistentVersionEntry":
574@@ -1904,7 +1934,7 @@
575 ... ('1.0', dict(exported_as='bar')),
576 ... ('2.0', dict(exported_as='foo')))
577
578- >>> generate_entry_interfaces(IWrongOrderEntry, '1.0', '2.0')
579+ >>> generate_entry_interfaces(IWrongOrderEntry, [], '1.0', '2.0')
580 Traceback (most recent call last):
581 ...
582 ValueError: Field "..." in interface "IWrongOrderEntry":
583@@ -1920,7 +1950,7 @@
584 ... ('beta', dict(exported_as='another_beta_name')),
585 ... ('beta', dict(exported_as='beta_name')))
586
587- >>> generate_entry_interfaces(IDuplicateEntry, 'beta', '1.0')
588+ >>> generate_entry_interfaces(IDuplicateEntry, [], 'beta', '1.0')
589 Traceback (most recent call last):
590 ...
591 ValueError: Field "field" in interface "IDuplicateEntry":
592@@ -1937,7 +1967,7 @@
593 ... ('beta', dict(exported_as='beta_name')),
594 ... exported_as='earliest_name')
595
596- >>> generate_entry_interfaces(IDuplicateEntry, 'beta', '1.0')
597+ >>> generate_entry_interfaces(IDuplicateEntry, [], 'beta', '1.0')
598 Traceback (most recent call last):
599 ...
600 ValueError: Field "field" in interface "IDuplicateEntry":
601@@ -1977,7 +2007,7 @@
602 ... ('beta', dict(exported_as='unchanging_name')))
603
604 >>> [version for version, tags in
605- ... generate_entry_interfaces(IUnchangingEntry, *versions)]
606+ ... generate_entry_interfaces(IUnchangingEntry, [], *versions)]
607 ['beta', '1.0', '2.0', '3.0']
608
609 Named operations
610@@ -2291,7 +2321,7 @@
611 ... pass
612
613 >>> ignored = generate_entry_interfaces(
614- ... IDifferentMutators, 'beta', '1.0')
615+ ... IDifferentMutators, [], 'beta', '1.0')
616
617 But you can't define two mutators for the same field in the same version.
618
619@@ -2342,7 +2372,7 @@
620 ... pass
621
622 >>> generate_entry_interfaces(
623- ... IImplicitAndExplicitMutator, 'beta', '1.0')
624+ ... IImplicitAndExplicitMutator, [], 'beta', '1.0')
625 Traceback (most recent call last):
626 ...
627 ValueError: Field "field" in interface
628@@ -2377,7 +2407,7 @@
629 ... pass
630
631 >>> ignored = generate_entry_interfaces(
632- ... IImplicitAndExplicitMutator, 'alpha', 'beta', '1.0')
633+ ... IImplicitAndExplicitMutator, [], 'alpha', 'beta', '1.0')
634
635 Destructor operations
636 *********************
637@@ -2402,7 +2432,7 @@
638 ... """Another destructor method."""
639
640 >>> ignore = generate_entry_interfaces(
641- ... IGoodDestructorEntry, 'beta', '1.0')
642+ ... IGoodDestructorEntry, [], 'beta', '1.0')
643
644 In this next example, the destructor is removed in 1.0 and
645 added back in 2.0. The 2.0 version does not inherit any values from
646@@ -2476,33 +2506,7 @@
647 (Put the interface in a module where it will be possible for the ZCML
648 handler to inspect.)
649
650- >>> import sys
651- >>> from types import ModuleType
652- >>> def create_test_module(name, *contents):
653- ... """Defines a new module and adds it to sys.modules."""
654- ... new_module = ModuleType(name)
655- ... sys.modules['lazr.restful.' + name] = new_module
656- ... for object in contents:
657- ... setattr(new_module, object.__name__, object)
658- ... return new_module
659-
660- >>> from zope.configuration import xmlconfig
661- >>> def register_test_module(name, *contents):
662- ... new_module = create_test_module(name, *contents)
663- ... try:
664- ... zcmlcontext = xmlconfig.string("""
665- ... <configure
666- ... xmlns:webservice=
667- ... "http://namespaces.canonical.com/webservice">
668- ... <include package="lazr.restful" file="meta.zcml" />
669- ... <webservice:register module="lazr.restful.%s" />
670- ... </configure>
671- ... """ % name)
672- ... except Exception, e:
673- ... del sys.modules['lazr.restful.' + name]
674- ... raise e
675- ... return new_module
676-
677+ >>> from lazr.restful.testing.helpers import register_test_module
678 >>> bookexample = register_test_module(
679 ... 'bookexample', IBook, IBookSet, IBookOnSteroids,
680 ... IBookSetOnSteroids, ISimpleComment, InvalidEmail)
681@@ -2559,6 +2563,7 @@
682
683 (Clean-up.)
684
685+ >>> import sys
686 >>> del bookexample
687 >>> del sys.modules['lazr.restful.bookexample']
688
689@@ -2878,3 +2883,4 @@
690 <...POST_IBetaMutatorEntry3_set_value_beta...>
691 >>> operation_for(context, '3.0', 'set_value')
692 <...POST_IBetaMutatorEntry3_set_value_beta...>
693+
694
695=== modified file 'src/lazr/restful/docs/webservice.txt'
696--- src/lazr/restful/docs/webservice.txt 2010-05-20 18:11:11 +0000
697+++ src/lazr/restful/docs/webservice.txt 2010-08-05 14:04:50 +0000
698@@ -636,11 +636,23 @@
699 >>> from lazr.restful import Entry
700 >>> from lazr.restful.testing.webservice import FakeRequest
701
702+ >>> from UserDict import UserDict
703+ >>> class FakeDict(UserDict):
704+ ... def __init__(self, interface):
705+ ... UserDict.__init__(self)
706+ ... self.interface = interface
707+ ... def __getitem__(self, key):
708+ ... return self.interface
709+
710 >>> class AuthorEntry(Entry):
711 ... """An author, as exposed through the web service."""
712 ... adapts(IAuthor)
713 ... delegates(IAuthorEntry)
714 ... schema = IAuthorEntry
715+ ... # This dict is normally generated by lazr.restful, but since we
716+ ... # create the adapters manually here, we need to do the same for
717+ ... # this dict.
718+ ... _orig_interfaces = FakeDict(IAuthor)
719
720 >>> request = FakeRequest()
721 >>> verifyObject(IAuthorEntry, AuthorEntry(A1, request))
722@@ -675,20 +687,36 @@
723 ... """A cookbook, as exposed through the web service."""
724 ... delegates(ICookbookEntry)
725 ... schema = ICookbookEntry
726+ ... # This dict is normally generated by lazr.restful, but since we
727+ ... # create the adapters manually here, we need to do the same for
728+ ... # this dict.
729+ ... _orig_interfaces = FakeDict(ICookbook)
730
731 >>> class DishEntry(Entry):
732 ... """A dish, as exposed through the web service."""
733 ... delegates(IDishEntry)
734 ... schema = IDishEntry
735+ ... # This dict is normally generated by lazr.restful, but since we
736+ ... # create the adapters manually here, we need to do the same for
737+ ... # this dict.
738+ ... _orig_interfaces = FakeDict(IDish)
739
740 >>> class CommentEntry(Entry):
741 ... """A comment, as exposed through the web service."""
742 ... delegates(ICommentEntry)
743 ... schema = ICommentEntry
744+ ... # This dict is normally generated by lazr.restful, but since we
745+ ... # create the adapters manually here, we need to do the same for
746+ ... # this dict.
747+ ... _orig_interfaces = FakeDict(IComment)
748
749 >>> class RecipeEntry(Entry):
750 ... delegates(IRecipeEntry)
751 ... schema = IRecipeEntry
752+ ... # This dict is normally generated by lazr.restful, but since we
753+ ... # create the adapters manually here, we need to do the same for
754+ ... # this dict.
755+ ... _orig_interfaces = FakeDict(IRecipe)
756
757 We need to register these entries as a multiadapter adapter from
758 (e.g.) ``IAuthor`` and ``IWebServiceClientRequest`` to (e.g.)
759
760=== modified file 'src/lazr/restful/example/base/tests/test_integration.py'
761--- src/lazr/restful/example/base/tests/test_integration.py 2009-09-01 13:12:32 +0000
762+++ src/lazr/restful/example/base/tests/test_integration.py 2010-08-05 14:04:50 +0000
763@@ -29,6 +29,7 @@
764 doctest.REPORT_NDIFF)
765
766 class FunctionalLayer:
767+ allow_teardown = False
768 zcml = os.path.abspath(resource_filename('lazr.restful', 'ftesting.zcml'))
769 zcml_layer(FunctionalLayer)
770
771
772=== added directory 'src/lazr/restful/example/base_extended'
773=== added file 'src/lazr/restful/example/base_extended/README.txt'
774--- src/lazr/restful/example/base_extended/README.txt 1970-01-01 00:00:00 +0000
775+++ src/lazr/restful/example/base_extended/README.txt 2010-08-05 14:04:50 +0000
776@@ -0,0 +1,22 @@
777+This is a very simple webservice that demonstrates how to use contributing
778+interfaces to add fields to an existing webservice using a plugin-like
779+pattern.
780+
781+Here we've just added a 'comments' field to the IRecipe entry.
782+
783+ >>> from lazr.restful.testing.webservice import WebServiceCaller
784+ >>> webservice = WebServiceCaller(domain='cookbooks.dev')
785+
786+ # The comments DB for this webservice is empty so we'll add some comments
787+ # to the recipe with ID=1
788+ >>> from lazr.restful.example.base_extended.comments import comments_db
789+ >>> comments_db[1] = ['Comment 1', 'Comment 2']
790+
791+And as we can see below, a recipe's representation now include its comments.
792+
793+ >>> print "\n".join(webservice.get('/recipes/1').jsonBody()['comments'])
794+ Comment 1
795+ Comment 2
796+
797+ >>> webservice.get('/recipes/2').jsonBody()['comments']
798+ []
799
800=== added file 'src/lazr/restful/example/base_extended/__init__.py'
801--- src/lazr/restful/example/base_extended/__init__.py 1970-01-01 00:00:00 +0000
802+++ src/lazr/restful/example/base_extended/__init__.py 2010-08-05 14:04:50 +0000
803@@ -0,0 +1,3 @@
804+"""A simple webservice which uses contributing interfaces to extend the 'base'
805+ one.
806+"""
807
808=== added file 'src/lazr/restful/example/base_extended/comments.py'
809--- src/lazr/restful/example/base_extended/comments.py 1970-01-01 00:00:00 +0000
810+++ src/lazr/restful/example/base_extended/comments.py 2010-08-05 14:04:50 +0000
811@@ -0,0 +1,32 @@
812+
813+from zope.component import adapts
814+from zope.interface import implements, Interface
815+from zope.schema import List, Text
816+
817+from lazr.restful.example.base.interfaces import IRecipe
818+
819+from lazr.restful.declarations import (
820+ export_as_webservice_entry, exported)
821+
822+
823+class IHasComments(Interface):
824+ export_as_webservice_entry(contributes_to=[IRecipe])
825+ comments = exported(
826+ List(title=u'Comments made by users', value_type=Text()))
827+
828+
829+class RecipeToHasCommentsAdapter:
830+ implements(IHasComments)
831+ adapts(IRecipe)
832+
833+ def __init__(self, recipe):
834+ self.recipe = recipe
835+
836+ @property
837+ def comments(self):
838+ return comments_db.get(self.recipe.id, [])
839+
840+
841+# A fake database for storing comments. Monkey-patch this to test the
842+# IHasComments adapter.
843+comments_db = {}
844
845=== added file 'src/lazr/restful/example/base_extended/site.zcml'
846--- src/lazr/restful/example/base_extended/site.zcml 1970-01-01 00:00:00 +0000
847+++ src/lazr/restful/example/base_extended/site.zcml 2010-08-05 14:04:50 +0000
848@@ -0,0 +1,18 @@
849+<configure
850+ xmlns="http://namespaces.zope.org/zope"
851+ xmlns:webservice="http://namespaces.canonical.com/webservice"
852+ xmlns:grok="http://namespaces.zope.org/grok">
853+
854+ <include package="lazr.restful" file="basic-site.zcml" />
855+
856+ <include package="lazr.restful.example.base" />
857+
858+ <webservice:register module="lazr.restful.example.base_extended.comments" />
859+ <grok:grok package="lazr.restful.example.base_extended" />
860+
861+ <adapter
862+ factory="lazr.restful.example.base_extended.comments.RecipeToHasCommentsAdapter" />
863+
864+ <securityPolicy
865+ component="zope.security.simplepolicies.PermissiveSecurityPolicy" />
866+</configure>
867
868=== added directory 'src/lazr/restful/example/base_extended/tests'
869=== added file 'src/lazr/restful/example/base_extended/tests/__init__.py'
870=== added file 'src/lazr/restful/example/base_extended/tests/test_integration.py'
871--- src/lazr/restful/example/base_extended/tests/test_integration.py 1970-01-01 00:00:00 +0000
872+++ src/lazr/restful/example/base_extended/tests/test_integration.py 2010-08-05 14:04:50 +0000
873@@ -0,0 +1,38 @@
874+# Copyright 20010 Canonical Ltd. All rights reserved.
875+
876+"""Test harness for LAZR doctests."""
877+
878+__metaclass__ = type
879+__all__ = []
880+
881+import os
882+import doctest
883+from pkg_resources import resource_filename
884+
885+from van.testing.layer import zcml_layer, wsgi_intercept_layer
886+
887+from lazr.restful.example.base.tests.test_integration import (
888+ CookbookWebServiceTestPublication, DOCTEST_FLAGS)
889+from lazr.restful.testing.webservice import WebServiceApplication
890+
891+
892+class FunctionalLayer:
893+ allow_teardown = False
894+ zcml = os.path.abspath(resource_filename(
895+ 'lazr.restful.example.base_extended', 'site.zcml'))
896+zcml_layer(FunctionalLayer)
897+
898+
899+class WSGILayer(FunctionalLayer):
900+ @classmethod
901+ def make_application(self):
902+ return WebServiceApplication({}, CookbookWebServiceTestPublication)
903+wsgi_intercept_layer(WSGILayer)
904+
905+
906+def additional_tests():
907+ """See `zope.testing.testrunner`."""
908+ tests = ['../README.txt']
909+ suite = doctest.DocFileSuite(optionflags=DOCTEST_FLAGS, *tests)
910+ suite.layer = WSGILayer
911+ return suite
912
913=== modified file 'src/lazr/restful/metazcml.py'
914--- src/lazr/restful/metazcml.py 2010-03-03 13:24:04 +0000
915+++ src/lazr/restful/metazcml.py 2010-08-05 14:04:50 +0000
916@@ -7,6 +7,7 @@
917
918
919 import inspect
920+import itertools
921
922 from zope.component import getUtility
923 from zope.component.zcml import handler
924@@ -16,14 +17,15 @@
925
926
927 from lazr.restful.declarations import (
928- LAZR_WEBSERVICE_EXPORTED, OPERATION_TYPES, REMOVED_OPERATION_TYPE,
929- generate_collection_adapter, generate_entry_adapters,
930- generate_entry_interfaces, generate_operation_adapter)
931+ COLLECTION_TYPE, ENTRY_TYPE, LAZR_WEBSERVICE_EXPORTED, OPERATION_TYPES,
932+ REMOVED_OPERATION_TYPE, generate_collection_adapter,
933+ generate_entry_adapters, generate_entry_interfaces,
934+ generate_operation_adapter)
935 from lazr.restful.error import WebServiceExceptionView
936
937 from lazr.restful.interfaces import (
938 ICollection, IEntry, IResourceDELETEOperation, IResourceGETOperation,
939- IResourceOperation, IResourcePOSTOperation, IWebServiceClientRequest,
940+ IResourcePOSTOperation, IWebServiceClientRequest,
941 IWebServiceConfiguration, IWebServiceVersion)
942
943
944@@ -34,7 +36,7 @@
945 title=u'Module which will be inspected for webservice declarations')
946
947
948-def generate_and_register_entry_adapters(interface, info):
949+def generate_and_register_entry_adapters(interface, info, contributors):
950 """Generate an entry adapter for every version of the web service.
951
952 This code generates an IEntry subinterface for every version, each
953@@ -48,9 +50,10 @@
954 versions = list(config.active_versions)
955
956 # Generate an interface and an adapter for every version.
957- web_interfaces = generate_entry_interfaces(interface, *versions)
958- web_factories = generate_entry_adapters(interface, web_interfaces)
959- provides = IEntry
960+ web_interfaces = generate_entry_interfaces(
961+ interface, contributors, *versions)
962+ web_factories = generate_entry_adapters(
963+ interface, contributors, web_interfaces)
964 for i in range(0, len(web_interfaces)):
965 interface_version, web_interface = web_interfaces[i]
966 factory_version, factory = web_factories[i]
967@@ -161,10 +164,79 @@
968 tag = interface.queryTaggedValue(LAZR_WEBSERVICE_EXPORTED)
969 if tag is None:
970 continue
971- if tag['type'] in ['entry', 'collection']:
972+ if tag['type'] in [ENTRY_TYPE, COLLECTION_TYPE]:
973 yield interface
974
975
976+def find_interfaces_and_contributors(module):
977+ """Find the interfaces and its contributors marked for export.
978+
979+ Return a dictionary with interfaces as keys and their contributors as
980+ values.
981+ """
982+ interfaces_with_contributors = {}
983+ for interface in find_exported_interfaces(module):
984+ if issubclass(interface, Exception):
985+ # Exceptions can't have interfaces, so just store it in
986+ # interfaces_with_contributors and move on.
987+ interfaces_with_contributors.setdefault(interface, [])
988+ continue
989+
990+ tag = interface.getTaggedValue(LAZR_WEBSERVICE_EXPORTED)
991+ if tag.get('contributes_to'):
992+ # Append this interface (which is a contributing interface) to the
993+ # list of contributors of every interface it contributes to.
994+ for iface in tag['contributes_to']:
995+ if iface.queryTaggedValue(LAZR_WEBSERVICE_EXPORTED) is None:
996+ raise AttemptToContributeToNonExportedInterface(
997+ "Interface %s contributes to %s, which is not "
998+ "exported." % (interface.__name__, iface.__name__))
999+ raise AssertionError('foo')
1000+ contributors = interfaces_with_contributors.setdefault(
1001+ iface, [])
1002+ contributors.append(interface)
1003+ else:
1004+ # This is a regular interface, but one of its contributing
1005+ # interfaces may have been processed previously and in that case a
1006+ # key for it would already exist in interfaces_with_contributors;
1007+ # that's why we use setdefault.
1008+ interfaces_with_contributors.setdefault(interface, [])
1009+
1010+ # For every exported interface, check that none of its names are exported
1011+ # in more than one contributing interface.
1012+ for interface, contributors in interfaces_with_contributors.items():
1013+ if len(contributors) == 0:
1014+ continue
1015+ names = {}
1016+ for iface in itertools.chain([interface], contributors):
1017+ for name, f in iface.namesAndDescriptions(all=True):
1018+ if f.queryTaggedValue(LAZR_WEBSERVICE_EXPORTED) is not None:
1019+ L = names.setdefault(name, [])
1020+ L.append(iface)
1021+ for name, interfaces in names.items():
1022+ if len(interfaces) > 1:
1023+ raise ConflictInContributingInterfaces(name, interfaces)
1024+ return interfaces_with_contributors
1025+
1026+
1027+class ConflictInContributingInterfaces(Exception):
1028+ """More than one interface tried to contribute a given attribute/method to
1029+ another interface.
1030+ """
1031+
1032+ def __init__(self, name, interfaces):
1033+ self.msg = (
1034+ "'%s' is exported in more than one contributing interface: %s"
1035+ % (name, ", ".join(i.__name__ for i in interfaces)))
1036+
1037+ def __str__(self):
1038+ return self.msg
1039+
1040+
1041+class AttemptToContributeToNonExportedInterface(Exception):
1042+ """An interface contributes to another one which is not exported."""
1043+
1044+
1045 def register_webservice(context, module):
1046 """Generate and register web service adapters.
1047
1048@@ -175,19 +247,21 @@
1049 if not inspect.ismodule(module):
1050 raise TypeError("module attribute must be a module: %s, %s" %
1051 module, type(module))
1052- for interface in find_exported_interfaces(module):
1053+ interfaces_with_contributors = find_interfaces_and_contributors(module)
1054+
1055+ for interface, contributors in interfaces_with_contributors.items():
1056 if issubclass(interface, Exception):
1057 register_exception_view(context, interface)
1058 continue
1059
1060 tag = interface.getTaggedValue(LAZR_WEBSERVICE_EXPORTED)
1061- if tag['type'] == 'entry':
1062+ if tag['type'] == ENTRY_TYPE:
1063 context.action(
1064 discriminator=('webservice entry interface', interface),
1065 callable=generate_and_register_entry_adapters,
1066- args=(interface, context.info),
1067+ args=(interface, context.info, contributors),
1068 )
1069- elif tag['type'] == 'collection':
1070+ elif tag['type'] == COLLECTION_TYPE:
1071 for version in tag['collection_default_content'].keys():
1072 factory = generate_collection_adapter(interface, version)
1073 provides = ICollection
1074@@ -203,11 +277,12 @@
1075 raise AssertionError('Unknown export type: %s' % tag['type'])
1076 context.action(
1077 discriminator=('webservice versioned operations', interface),
1078- args=(context, interface),
1079+ args=(context, interface, contributors),
1080 callable=generate_and_register_webservice_operations)
1081
1082
1083-def generate_and_register_webservice_operations(context, interface):
1084+def generate_and_register_webservice_operations(
1085+ context, interface, contributors):
1086 """Create and register adapters for all exported methods.
1087
1088 Different versions of the web service may publish the same
1089@@ -228,7 +303,11 @@
1090 else:
1091 block_mutator_operations_as_of_version = None
1092
1093- for name, method in interface.namesAndDescriptions(True):
1094+ methods = interface.namesAndDescriptions(True)
1095+ for iface in contributors:
1096+ methods.extend(iface.namesAndDescriptions(True))
1097+
1098+ for name, method in methods:
1099 tag = method.queryTaggedValue(LAZR_WEBSERVICE_EXPORTED)
1100 if tag is None or tag['type'] not in OPERATION_TYPES:
1101 # This method is not published as a named operation.
1102
1103=== added file 'src/lazr/restful/testing/helpers.py'
1104--- src/lazr/restful/testing/helpers.py 1970-01-01 00:00:00 +0000
1105+++ src/lazr/restful/testing/helpers.py 2010-08-05 14:04:50 +0000
1106@@ -0,0 +1,45 @@
1107+import sys
1108+from types import ModuleType
1109+
1110+from zope.configuration import xmlconfig
1111+from zope.interface import implements
1112+
1113+from lazr.restful.interfaces import IWebServiceConfiguration
1114+
1115+
1116+def create_test_module(name, *contents):
1117+ """Defines a new module and adds it to sys.modules."""
1118+ new_module = ModuleType(name)
1119+ sys.modules['lazr.restful.' + name] = new_module
1120+ for object in contents:
1121+ setattr(new_module, object.__name__, object)
1122+ return new_module
1123+
1124+
1125+def register_test_module(name, *contents):
1126+ new_module = create_test_module(name, *contents)
1127+ try:
1128+ xmlconfig.string("""
1129+ <configure
1130+ xmlns:webservice=
1131+ "http://namespaces.canonical.com/webservice">
1132+ <include package="lazr.restful" file="meta.zcml" />
1133+ <webservice:register module="lazr.restful.%s" />
1134+ </configure>
1135+ """ % name)
1136+ except Exception, e:
1137+ del sys.modules['lazr.restful.' + name]
1138+ raise e
1139+ return new_module
1140+
1141+
1142+class TestWebServiceConfiguration:
1143+ implements(IWebServiceConfiguration)
1144+ view_permission = "lazr.View"
1145+ active_versions = ["beta", "1.0"]
1146+ last_version_with_mutator_named_operations = "1.0"
1147+ code_revision = "1.0b"
1148+ default_batch_size = 50
1149+
1150+ def get_request_user(self):
1151+ return 'A user'
1152
1153=== added file 'src/lazr/restful/tests/test_declarations.py'
1154--- src/lazr/restful/tests/test_declarations.py 1970-01-01 00:00:00 +0000
1155+++ src/lazr/restful/tests/test_declarations.py 2010-08-05 14:04:50 +0000
1156@@ -0,0 +1,335 @@
1157+import unittest
1158+
1159+from zope.component import (
1160+ adapts, getMultiAdapter, getSiteManager, getUtility, provideUtility)
1161+from zope.component.interfaces import ComponentLookupError
1162+from zope.interface import alsoProvides, Attribute, implements, Interface
1163+from zope.publisher.interfaces.http import IHTTPRequest
1164+from zope.schema import Int, Object, TextLine
1165+from zope.security.checker import MultiChecker, ProxyFactory
1166+from zope.security.management import endInteraction, newInteraction
1167+
1168+from lazr.restful.declarations import (
1169+ export_as_webservice_entry, exported, export_read_operation,
1170+ export_write_operation, mutator_for, operation_for_version,
1171+ operation_parameters)
1172+from lazr.restful.interfaces import (
1173+ IEntry, IResourceGETOperation, IWebServiceConfiguration,
1174+ IWebServiceVersion)
1175+from lazr.restful.marshallers import SimpleFieldMarshaller
1176+from lazr.restful.metazcml import (
1177+ AttemptToContributeToNonExportedInterface,
1178+ ConflictInContributingInterfaces, find_interfaces_and_contributors)
1179+from lazr.restful._resource import EntryAdapterUtility, EntryResource
1180+from lazr.restful.testing.webservice import FakeRequest
1181+from lazr.restful.testing.helpers import (
1182+ create_test_module, TestWebServiceConfiguration, register_test_module)
1183+
1184+
1185+class ContributingInterfacesTestCase(unittest.TestCase):
1186+ """Tests for interfaces that contribute fields/operations to others."""
1187+
1188+ def setUp(self):
1189+ provideUtility(
1190+ TestWebServiceConfiguration(), IWebServiceConfiguration)
1191+ sm = getSiteManager()
1192+ sm.registerUtility(
1193+ ITestServiceRequestBeta, IWebServiceVersion, name='beta')
1194+ sm.registerUtility(
1195+ ITestServiceRequest10, IWebServiceVersion, name='1.0')
1196+ sm.registerAdapter(ProductToHasBugsAdapter)
1197+ sm.registerAdapter(ProjectToHasBugsAdapter)
1198+ sm.registerAdapter(ProductToHasBranchesAdapter)
1199+ sm.registerAdapter(DummyFieldMarshaller)
1200+ self.beta_request = FakeRequest(version='beta')
1201+ alsoProvides(
1202+ self.beta_request, getUtility(IWebServiceVersion, name='beta'))
1203+ self.one_zero_request = FakeRequest(version='1.0')
1204+ alsoProvides(
1205+ self.one_zero_request, getUtility(IWebServiceVersion, name='1.0'))
1206+ self.product = Product()
1207+ self.project = Project()
1208+
1209+ def test_attributes(self):
1210+ # The bug_count field comes from IHasBugs (which IProduct does not
1211+ # provide, although it can be adapted into) but that field is
1212+ # available in the webservice (IEntry) adapter for IProduct, and that
1213+ # adapter knows it needs to adapt the product into an IHasBugs to
1214+ # access .bug_count.
1215+ self.product._bug_count = 10
1216+ register_test_module('testmod', IProduct, IHasBugs)
1217+ adapter = getMultiAdapter((self.product, self.beta_request), IEntry)
1218+ self.assertEqual(adapter.bug_count, 10)
1219+
1220+ def test_operations(self):
1221+ # Although getBugsCount() is not provided by IProduct, it is available
1222+ # on the webservice adapter as IHasBugs contributes it to IProduct.
1223+ self.product._bug_count = 10
1224+ register_test_module('testmod', IProduct, IHasBugs)
1225+ adapter = getMultiAdapter(
1226+ (self.product, self.beta_request),
1227+ IResourceGETOperation, name='getBugsCount')
1228+ self.assertEqual(adapter(), '10')
1229+
1230+ def test_contributing_interface_with_differences_between_versions(self):
1231+ # In the 'beta' version, IHasBranches.development_branches is exported
1232+ # with its original name whereas for the '1.0' version it's exported
1233+ # as 'development_branch_10'.
1234+ self.product._dev_branch = Branch('A product branch')
1235+ register_test_module('testmod', IProduct, IHasBranches)
1236+ adapter = getMultiAdapter((self.product, self.beta_request), IEntry)
1237+ self.assertEqual(adapter.development_branch, self.product._dev_branch)
1238+
1239+ adapter = getMultiAdapter(
1240+ (self.product, self.one_zero_request), IEntry)
1241+ self.assertEqual(
1242+ adapter.development_branch_10, self.product._dev_branch)
1243+
1244+ def test_mutator_for_just_one_version(self):
1245+ # On the 'beta' version, IHasBranches contributes a read only
1246+ # development_branch field, but on version '1.0' that field can be
1247+ # modified as we define a mutator for it.
1248+ self.product._dev_branch = Branch('A product branch')
1249+ register_test_module('testmod', IProduct, IHasBranches)
1250+ adapter = getMultiAdapter((self.product, self.beta_request), IEntry)
1251+ try:
1252+ adapter.development_branch = None
1253+ except AttributeError:
1254+ pass
1255+ else:
1256+ self.fail('IHasBranches.development_branch should be read-only '
1257+ 'on the beta version')
1258+
1259+ adapter = getMultiAdapter(
1260+ (self.product, self.one_zero_request), IEntry)
1261+ self.assertEqual(
1262+ adapter.development_branch_10, self.product._dev_branch)
1263+ adapter.development_branch_10 = None
1264+ self.assertEqual(
1265+ adapter.development_branch_10, None)
1266+
1267+ def test_contributing_to_multiple_interfaces(self):
1268+ # Check that the webservice adapter for both IProduct and IProject
1269+ # have the IHasBugs attributes, as that interface contributes to them.
1270+ self.product._bug_count = 10
1271+ self.project._bug_count = 100
1272+ register_test_module('testmod', IProduct, IProject, IHasBugs)
1273+ adapter = getMultiAdapter((self.product, self.beta_request), IEntry)
1274+ self.assertEqual(adapter.bug_count, 10)
1275+
1276+ adapter = getMultiAdapter((self.project, self.beta_request), IEntry)
1277+ self.assertEqual(adapter.bug_count, 100)
1278+
1279+ def test_multiple_contributing_interfaces(self):
1280+ # Check that the webservice adapter for IProduct has the attributes
1281+ # from both IHasBugs and IHasBranches.
1282+ self.product._bug_count = 10
1283+ self.product._dev_branch = Branch('A product branch')
1284+ register_test_module('testmod', IProduct, IHasBugs, IHasBranches)
1285+ adapter = getMultiAdapter((self.product, self.beta_request), IEntry)
1286+ self.assertEqual(adapter.bug_count, 10)
1287+ self.assertEqual(adapter.development_branch, self.product._dev_branch)
1288+
1289+ def test_redacted_fields_with_no_permission_checker(self):
1290+ # When looking up an entry's redacted_fields, we take into account the
1291+ # interface where the field is defined and adapt the context to that
1292+ # interface before accessing that field.
1293+ register_test_module('testmod', IProduct, IHasBugs, IHasBranches)
1294+ entry_resource = EntryResource(self.product, self.beta_request)
1295+ self.assertEquals([], entry_resource.redacted_fields)
1296+
1297+ def test_redacted_fields_with_permission_checker(self):
1298+ # When looking up an entry's redacted_fields for an object which is
1299+ # security proxied, we use the security checker for the interface
1300+ # where the field is defined.
1301+ register_test_module('testmod', IProduct, IHasBugs, IHasBranches)
1302+ newInteraction()
1303+ try:
1304+ secure_product = ProxyFactory(
1305+ self.product,
1306+ checker=MultiChecker([(IProduct, 'zope.Public')]))
1307+ entry_resource = EntryResource(secure_product, self.beta_request)
1308+ self.assertEquals([], entry_resource.redacted_fields)
1309+ finally:
1310+ endInteraction()
1311+
1312+ def test_duplicate_contributed_attributes(self):
1313+ # We do not allow a given attribute to be contributed to a given
1314+ # interface by more than one contributing interface.
1315+ testmod = create_test_module('testmod', IProduct, IHasBugs, IHasBugs2)
1316+ self.assertRaises(
1317+ ConflictInContributingInterfaces,
1318+ find_interfaces_and_contributors, testmod)
1319+
1320+ def test_contributing_interface_not_exported(self):
1321+ # Contributing interfaces are not exported by themselves -- they only
1322+ # contribute their exported fields/operations to other entries.
1323+ class DummyHasBranches:
1324+ implements(IHasBranches)
1325+ dummy = DummyHasBranches()
1326+ register_test_module('testmod', IProduct, IHasBranches)
1327+ self.assertRaises(
1328+ ComponentLookupError,
1329+ getMultiAdapter, (dummy, self.beta_request), IEntry)
1330+
1331+ def test_cannot_contribute_to_non_exported_interface(self):
1332+ # A contributing interface can only contribute to exported interfaces.
1333+ class INotExported(Interface):
1334+ pass
1335+ class IContributor(Interface):
1336+ export_as_webservice_entry(contributes_to=[INotExported])
1337+ title = exported(TextLine(title=u'The project title'))
1338+ testmod = create_test_module('testmod', IContributor, INotExported)
1339+ self.assertRaises(
1340+ AttemptToContributeToNonExportedInterface,
1341+ find_interfaces_and_contributors, testmod)
1342+
1343+ def test_duplicate_contributed_methods(self):
1344+ # We do not allow a given method to be contributed to a given
1345+ # interface by more than one contributing interface.
1346+ testmod = create_test_module('testmod', IProduct, IHasBugs, IHasBugs3)
1347+ self.assertRaises(
1348+ ConflictInContributingInterfaces,
1349+ find_interfaces_and_contributors, testmod)
1350+
1351+ def test_ConflictInContributingInterfaces(self):
1352+ # The ConflictInContributingInterfaces exception states what are the
1353+ # contributing interfaces that caused the conflict.
1354+ e = ConflictInContributingInterfaces('foo', [IHasBugs, IHasBugs2])
1355+ expected_msg = ("'foo' is exported in more than one contributing "
1356+ "interface: IHasBugs, IHasBugs2")
1357+ self.assertEquals(str(e), expected_msg)
1358+
1359+ def test_type_name(self):
1360+ # Even though the generated adapters will contain stuff from various
1361+ # different adapters, its type name is that of the main interface and
1362+ # not one of its contributors.
1363+ register_test_module('testmod', IProduct, IHasBugs)
1364+ adapter = getMultiAdapter((self.product, self.beta_request), IEntry)
1365+ self.assertEqual(
1366+ 'product', EntryAdapterUtility(adapter.__class__).singular_type)
1367+
1368+
1369+class IProduct(Interface):
1370+ export_as_webservice_entry()
1371+ title = exported(TextLine(title=u'The product title'))
1372+ # Need to define the two attributes below because we have a test which
1373+ # wraps a Product object with a security proxy and later uses adapters
1374+ # that access _dev_branch and _bug_count.
1375+ _dev_branch = Attribute('dev branch')
1376+ _bug_count = Attribute('bug count')
1377+
1378+
1379+class Product(object):
1380+ implements(IProduct)
1381+ title = 'A product'
1382+ _bug_count = 0
1383+ _dev_branch = None
1384+
1385+
1386+class IProject(Interface):
1387+ export_as_webservice_entry()
1388+ title = exported(TextLine(title=u'The project title'))
1389+
1390+
1391+class Project(object):
1392+ implements(IProject)
1393+ title = 'A project'
1394+ _bug_count = 0
1395+
1396+
1397+class IHasBugs(Interface):
1398+ export_as_webservice_entry(contributes_to=[IProduct, IProject])
1399+ bug_count = exported(Int(title=u'Number of bugs'))
1400+ not_exported = TextLine(title=u'Not exported')
1401+
1402+ @export_read_operation()
1403+ def getBugsCount():
1404+ pass
1405+
1406+
1407+class IHasBugs2(Interface):
1408+ export_as_webservice_entry(contributes_to=[IProduct])
1409+ bug_count = exported(Int(title=u'Number of bugs'))
1410+ not_exported = TextLine(title=u'Not exported')
1411+
1412+
1413+class IHasBugs3(Interface):
1414+ export_as_webservice_entry(contributes_to=[IProduct])
1415+ not_exported = TextLine(title=u'Not exported')
1416+
1417+ @export_read_operation()
1418+ def getBugsCount():
1419+ pass
1420+
1421+
1422+class ProductToHasBugsAdapter(object):
1423+ adapts(IProduct)
1424+ implements(IHasBugs)
1425+
1426+ def __init__(self, context):
1427+ self.context = context
1428+ self.bug_count = context._bug_count
1429+
1430+ def getBugsCount(self):
1431+ return self.bug_count
1432+
1433+
1434+class ProjectToHasBugsAdapter(ProductToHasBugsAdapter):
1435+ adapts(IProject)
1436+
1437+
1438+class IBranch(Interface):
1439+ name = TextLine(title=u'The branch name')
1440+
1441+
1442+class Branch(object):
1443+ implements(IBranch)
1444+
1445+ def __init__(self, name):
1446+ self.name = name
1447+
1448+
1449+class IHasBranches(Interface):
1450+ export_as_webservice_entry(contributes_to=[IProduct])
1451+ not_exported = TextLine(title=u'Not exported')
1452+ development_branch = exported(
1453+ Object(schema=IBranch, readonly=True),
1454+ ('1.0', dict(exported_as='development_branch_10')),
1455+ ('beta', dict(exported_as='development_branch')))
1456+
1457+ @mutator_for(development_branch)
1458+ @export_write_operation()
1459+ @operation_parameters(value=TextLine())
1460+ @operation_for_version('1.0')
1461+ def set_dev_branch(value):
1462+ pass
1463+
1464+
1465+class ProductToHasBranchesAdapter(object):
1466+ adapts(IProduct)
1467+ implements(IHasBranches)
1468+
1469+ def __init__(self, context):
1470+ self.context = context
1471+
1472+ @property
1473+ def development_branch(self):
1474+ return self.context._dev_branch
1475+
1476+ def set_dev_branch(self, value):
1477+ self.context._dev_branch = value
1478+
1479+
1480+# One of our tests will try to unmarshall some entries, but even though we
1481+# don't care about the unmarshalling itself, we need to register a generic
1482+# marshaller so that the adapter lookup doesn't fail and cause an error on the
1483+# test.
1484+class DummyFieldMarshaller(SimpleFieldMarshaller):
1485+ adapts(Interface, IHTTPRequest)
1486+
1487+
1488+class ITestServiceRequestBeta(IWebServiceVersion):
1489+ pass
1490+class ITestServiceRequest10(IWebServiceVersion):
1491+ pass
1492
1493=== modified file 'src/lazr/restful/version.txt'
1494--- src/lazr/restful/version.txt 2010-06-14 15:29:08 +0000
1495+++ src/lazr/restful/version.txt 2010-08-05 14:04:50 +0000
1496@@ -1,1 +1,1 @@
1497-0.9.29
1498+0.10.0

Subscribers

People subscribed via source and target branches