Merge lp:~salgado/lazr.restful/extension-interfaces into lp:lazr.restful
- extension-interfaces
- Merge into trunk
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Gary Poster | Approve | ||
Leonard Richardson (community) | Needs Fixing | ||
Review via email: mp+29388@code.launchpad.net |
Commit message
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_
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/
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:/
Leonard Richardson (leonardr) wrote : | # |
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:/
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
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.
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
>
> - I see that find_interfaces
> raising ConflictInContr
> 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://
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:/
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
+1
>
>>
>> - I see that find_interfaces
>> raising ConflictInContr
>> 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://
> 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
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 ConflictInContr
You still need to describe this change in the NEWS file.
I don't know how useful that TestWebServiceC
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."
- 145. By Guilherme Salgado
-
Improve the ConflictInContr
ibutingInterfac es exception to state what interfaces contain the conflicting attributes - 146. By Guilherme Salgado
-
Add new entry to NEWS.txt
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 ConflictInContr
> 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 TestWebServiceC
> 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://
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://
>
> 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:/
- 147. By Guilherme Salgado
-
Some comments/docstrings on tests
- 148. By Guilherme Salgado
-
Fix a bug in generate_
entry_interface s 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
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-
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://
> 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_
= 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
Guilherme Salgado (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://
> > 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://
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.
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'...
Guilherme Salgado (salgado) wrote : | # |
http://
Gary Poster (gary) wrote : | # |
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://
>>> 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...
Gary Poster (gary) wrote : | # |
For posterity...
On IRC I mentioned to Salgado that in http://
- 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
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://
>
> 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://
http://
them to my branch. Any idea why it's not working?
--
Guilherme Salgado <https:/
- 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
- 155. By Guilherme Salgado
-
Fix version
Preview Diff
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 |
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?