Merge lp:~cjwatson/lazr.restful/py3-declarations into lp:lazr.restful

Proposed by Colin Watson
Status: Merged
Merged at revision: 243
Proposed branch: lp:~cjwatson/lazr.restful/py3-declarations
Merge into: lp:lazr.restful
Diff against target: 2275 lines (+681/-421)
11 files modified
NEWS.rst (+10/-0)
src/lazr/restful/declarations.py (+205/-79)
src/lazr/restful/docs/webservice-declarations.rst (+162/-214)
src/lazr/restful/example/base/interfaces.py (+20/-12)
src/lazr/restful/example/base_extended/comments.py (+6/-4)
src/lazr/restful/example/multiversion/resources.py (+16/-8)
src/lazr/restful/example/wsgi/README.txt (+4/-4)
src/lazr/restful/example/wsgi/resources.py (+7/-5)
src/lazr/restful/testing/webservice.py (+3/-3)
src/lazr/restful/tests/test_declarations.py (+232/-79)
src/lazr/restful/tests/test_webservice.py (+16/-13)
To merge this branch: bzr merge lp:~cjwatson/lazr.restful/py3-declarations
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+378548@code.launchpad.net

Commit message

Deprecate class advice APIs and provide replacements.

Description of the change

lazr.restful.declarations.export_as_webservice_entry and lazr.restful.declarations.export_as_webservice_collection can't work on Python 3, because the whole "class advice" strategy relies on metaclass hacking that is no longer possible. Deprecate these and provide equivalent class decorators (@exported_as_webservice_entry and @exported_as_webservice_collection) instead.

I've run the Launchpad test suite on a branch converted to use the new decorators, and everything looks fine.

To post a comment you must log in.
237. By Colin Watson

Add exported_as_webservice_{collection,entry} to __all__.

238. By Colin Watson

Fix skipping of test classes with testtools.

testtools doesn't currently support skipping test classes the normal
unittest way; see https://github.com/testing-cabal/testtools/issues/205.
However, we can avoid having to apply @unittest.skipIf to every test case in
the class by calling skipTest in setUp instead.

239. By Colin Watson

Merge trunk.

Revision history for this message
William Grant (wgrant) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'NEWS.rst'
2--- NEWS.rst 2020-02-19 15:03:01 +0000
3+++ NEWS.rst 2020-02-19 16:09:23 +0000
4@@ -2,6 +2,16 @@
5 NEWS for lazr.restful
6 =====================
7
8+0.22.0
9+======
10+
11+Deprecate the "class advice" APIs from ``lazr.restful.declarations``:
12+``export_as_webservice_entry`` and ``export_as_webservice_collection``. In
13+their place, prefer the equivalent class decorators:
14+``@exported_as_webservice_entry`` and
15+``@exported_as_webservice_collection``. The functions based on class advice
16+will not work on Python 3.
17+
18 0.21.1 (2020-02-19)
19 ===================
20
21
22=== modified file 'src/lazr/restful/declarations.py'
23--- src/lazr/restful/declarations.py 2020-02-04 11:52:59 +0000
24+++ src/lazr/restful/declarations.py 2020-02-19 16:09:23 +0000
25@@ -19,7 +19,6 @@
26 'call_with',
27 'collection_default_content',
28 'error_status',
29- 'exported',
30 'export_as_webservice_collection',
31 'export_as_webservice_entry',
32 'export_destructor_operation',
33@@ -27,6 +26,9 @@
34 'export_operation_as',
35 'export_read_operation',
36 'export_write_operation',
37+ 'exported',
38+ 'exported_as_webservice_collection',
39+ 'exported_as_webservice_entry',
40 'generate_collection_adapter',
41 'generate_entry_adapters',
42 'generate_entry_interfaces',
43@@ -159,6 +161,84 @@
44 return f_locals.setdefault(TAGGED_DATA, {})
45
46
47+def _export_as_webservice_entry(interface,
48+ singular_name=None, plural_name=None,
49+ contributes_to=None, publish_web_link=True,
50+ as_of=None, versioned_annotations=None):
51+ """Tag an interface as exported on the web service as an entry.
52+
53+ This is the core of export_as_webservice_entry and
54+ exported_as_webservice_entry.
55+ """
56+ annotation_stack = VersionedDict()
57+
58+ if singular_name is None:
59+ # By convention, interfaces are called IWord1[Word2...]. The default
60+ # behavior assumes this convention and yields a singular name of
61+ # "word1_word2".
62+ my_singular_name = camelcase_to_underscore_separated(
63+ interface.__name__[1:])
64+ else:
65+ my_singular_name = singular_name
66+
67+ # Turn the named arguments into a dictionary for the first exported
68+ # version.
69+ initial_version = dict(
70+ type=ENTRY_TYPE, singular_name=my_singular_name,
71+ plural_name=plural_name, contributes_to=contributes_to,
72+ publish_web_link=publish_web_link, exported=True,
73+ _as_of_was_used=(as_of is not None))
74+
75+ afterwards = versioned_annotations or []
76+ for version, annotations in itertools.chain(
77+ [(as_of, initial_version)], afterwards):
78+ annotation_stack.push(version)
79+
80+ for key, value in annotations.items():
81+ if annotations != initial_version:
82+ # Make sure that the 'annotations' dict contains only
83+ # recognized annotations.
84+ if key not in ENTRY_ANNOTATION_KEYS:
85+ raise ValueError(
86+ 'Unrecognized annotation for version "%s": '
87+ '"%s"' % (version, key))
88+ annotation_stack[key] = value
89+ # If this version provides a singular name but not a plural name,
90+ # apply the default pluralization rule.
91+ if (annotations.get('singular_name') is not None
92+ and annotations.get('plural_name') is None):
93+ annotation_stack['plural_name'] = (
94+ annotations['singular_name'] + 's')
95+
96+ # When called from the @exported_as_webservice_entry decorator, this
97+ # would overwrite any interface annotations made by method decorators;
98+ # but there are currently no such annotations that are valid for methods
99+ # of entry interfaces anyway.
100+ interface.setTaggedValue(LAZR_WEBSERVICE_EXPORTED, annotation_stack)
101+
102+ # Set the name of the fields that didn't specify it using the
103+ # 'export_as' parameter in exported(). This must be done here, because
104+ # the field's __name__ attribute is only set when the interface is
105+ # created.
106+ for name, field in getFields(interface).items():
107+ tag_stack = field.queryTaggedValue(LAZR_WEBSERVICE_EXPORTED)
108+ if tag_stack is None or tag_stack.is_empty:
109+ continue
110+ if tag_stack['type'] != FIELD_TYPE:
111+ continue
112+ for version, tags in tag_stack.stack:
113+ # Set 'as' for every version in which the field is published but
114+ # no 'as' is specified. Also set 'original_name' for every
115+ # version in which the field is published--this will help with
116+ # performance optimizations around permission checks.
117+ if tags.get('exported') != False:
118+ tags['original_name'] = name
119+ if tags.get('as') is None:
120+ tags['as'] = name
121+
122+ annotate_exported_methods(interface)
123+
124+
125 def export_as_webservice_entry(singular_name=None, plural_name=None,
126 contributes_to=None, publish_web_link=True,
127 as_of=None, versioned_annotations=None):
128@@ -168,6 +248,10 @@
129 actually not be exported on its own but instead will contribute its
130 attributes/methods to other exported entries.
131
132+ This function does not work with Python 3. Rather than calling this
133+ within the class definition, decorate the class with
134+ @exported_as_webservice_entry instead.
135+
136 :param singular_name: The human-readable singular name of the entry,
137 eg. "paintbrush".
138 :param plural_name: The human-readable plural name of the entry,
139@@ -192,78 +276,71 @@
140 work just like the corresponding arguments to this method.
141 """
142 _check_called_from_interface_def('export_as_webservice_entry()')
143+
144 def mark_entry(interface):
145 """Class advisor that tags the interface once it is created."""
146 _check_interface('export_as_webservice_entry()', interface)
147-
148- annotation_stack = VersionedDict()
149-
150- if singular_name is None:
151- # By convention, interfaces are called IWord1[Word2...]. The
152- # default behavior assumes this convention and yields a
153- # singular name of "word1_word2".
154- my_singular_name = camelcase_to_underscore_separated(
155- interface.__name__[1:])
156- else:
157- my_singular_name = singular_name
158-
159- # Turn the named arguments into a dictionary for the first
160- # exported version.
161- initial_version = dict(
162- type=ENTRY_TYPE, singular_name=my_singular_name,
163- plural_name=plural_name, contributes_to=contributes_to,
164- publish_web_link=publish_web_link, exported=True,
165- _as_of_was_used=(not as_of is None))
166-
167- afterwards = versioned_annotations or []
168- for version, annotations in itertools.chain(
169- [(as_of, initial_version)], afterwards):
170- annotation_stack.push(version)
171-
172- for key, value in annotations.items():
173- if annotations != initial_version:
174- # Make sure that the 'annotations' dict
175- # contains only recognized annotations.
176- if key not in ENTRY_ANNOTATION_KEYS:
177- raise ValueError(
178- 'Unrecognized annotation for version "%s": '
179- '"%s"' % (version, key))
180- annotation_stack[key] = value
181- # If this version provides a singular name but not a
182- # plural name, apply the default pluralization rule.
183- if (annotations.get('singular_name') is not None
184- and annotations.get('plural_name') is None):
185- annotation_stack['plural_name'] = (
186- annotations['singular_name'] + 's')
187-
188- interface.setTaggedValue(LAZR_WEBSERVICE_EXPORTED, annotation_stack)
189-
190- # Set the name of the fields that didn't specify it using the
191- # 'export_as' parameter in exported(). This must be done here,
192- # because the field's __name__ attribute is only set when the
193- # interface is created.
194- for name, field in getFields(interface).items():
195- tag_stack = field.queryTaggedValue(LAZR_WEBSERVICE_EXPORTED)
196- if tag_stack is None or tag_stack.is_empty:
197- continue
198- if tag_stack['type'] != FIELD_TYPE:
199- continue
200- for version, tags in tag_stack.stack:
201- # Set 'as' for every version in which the field is
202- # published but no 'as' is specified. Also set
203- # 'original_name' for every version in which the field
204- # is published--this will help with performance
205- # optimizations around permission checks.
206- if tags.get('exported') != False:
207- tags['original_name'] = name
208- if tags.get('as') is None:
209- tags['as'] = name
210-
211- annotate_exported_methods(interface)
212+ _export_as_webservice_entry(
213+ interface, singular_name=singular_name, plural_name=plural_name,
214+ contributes_to=contributes_to, publish_web_link=publish_web_link,
215+ as_of=as_of, versioned_annotations=versioned_annotations)
216 return interface
217+
218 addClassAdvisor(mark_entry)
219
220
221+class exported_as_webservice_entry:
222+ """Mark the content interface as exported on the web service as an entry.
223+
224+ If contributes_to is a non-empty sequence of Interfaces, this entry will
225+ actually not be exported on its own but instead will contribute its
226+ attributes/methods to other exported entries.
227+
228+ :param singular_name: The human-readable singular name of the entry,
229+ eg. "paintbrush".
230+ :param plural_name: The human-readable plural name of the entry,
231+ eg. "paintbrushes"
232+ :param contributes_to: An optional list of exported interfaces to which
233+ this interface contributes.
234+ :param publish_web_link: This parameter is ignored unless there is
235+ a correspondence between this web service's entries and the
236+ pages on some website. If that is so, and if this parameter is
237+ set to True, the representation of this entry will include a
238+ web_link pointing to the corresponding page on the website. If
239+ False, web_link will be omitted.
240+ :param as_of: The first version of the web service to feature this entry.
241+ :param versioned_annotations: A list of 2-tuples (version,
242+ {params}), with more recent web service versions earlier in
243+ the list and older versions later in the list.
244+
245+ A 'params' dictionary may contain the key 'exported', which
246+ controls whether or not to publish the entry at all in the
247+ given version. It may also contain the keys 'singular_name',
248+ 'plural_name', 'contributes_to', or 'publish_web_link', which
249+ work just like the corresponding arguments to this method.
250+ """
251+
252+ def __init__(self, singular_name=None, plural_name=None,
253+ contributes_to=None, publish_web_link=True,
254+ as_of=None, versioned_annotations=None):
255+ self.singular_name = singular_name
256+ self.plural_name = plural_name
257+ self.contributes_to = contributes_to
258+ self.publish_web_link = publish_web_link
259+ self.as_of = as_of
260+ self.versioned_annotations = versioned_annotations
261+
262+ def __call__(self, interface):
263+ _check_interface('exported_as_webservice_entry()', interface)
264+ _export_as_webservice_entry(
265+ interface,
266+ singular_name=self.singular_name, plural_name=self.plural_name,
267+ contributes_to=self.contributes_to,
268+ publish_web_link=self.publish_web_link,
269+ as_of=self.as_of, versioned_annotations=self.versioned_annotations)
270+ return interface
271+
272+
273 def exported(field, *versioned_annotations, **kwparams):
274 """Mark the field as part of the entry data model.
275
276@@ -356,9 +433,22 @@
277 return field
278
279
280+def _check_collection_default_content(name, interface):
281+ """Check that the interface has a method providing the default content."""
282+ tag = interface.queryTaggedValue(LAZR_WEBSERVICE_EXPORTED, {})
283+ if 'collection_default_content' not in tag:
284+ raise TypeError(
285+ "%s is missing a method tagged with @collection_default_content." %
286+ name)
287+
288+
289 def export_as_webservice_collection(entry_schema):
290 """Mark the interface as exported on the web service as a collection.
291
292+ This function does not work with Python 3. Rather than calling this
293+ within the class definition, decorate the class with
294+ @exported_as_webservice_collection instead.
295+
296 :raises TypeError: if the interface doesn't have a method decorated with
297 @collection_default_content.
298 """
299@@ -376,24 +466,46 @@
300 def mark_collection(interface):
301 """Class advisor that tags the interface once it is created."""
302 _check_interface('export_as_webservice_collection()', interface)
303-
304- tag = interface.getTaggedValue(LAZR_WEBSERVICE_EXPORTED)
305- if 'collection_default_content' not in tag:
306- raise TypeError(
307- "export_as_webservice_collection() is missing a method "
308- "tagged with @collection_default_content.")
309-
310+ _check_collection_default_content(
311+ 'export_as_webservice_collection()', interface)
312 annotate_exported_methods(interface)
313 return interface
314
315 addClassAdvisor(mark_collection)
316
317
318+class exported_as_webservice_collection:
319+ """Mark the interface as exported on the web service as a collection.
320+
321+ :raises TypeError: if the interface doesn't have a method decorated with
322+ @collection_default_content.
323+ """
324+
325+ def __init__(self, entry_schema):
326+ if not IInterface.providedBy(entry_schema):
327+ raise TypeError("entry_schema must be an interface.")
328+ self.entry_schema = entry_schema
329+
330+ def __call__(self, interface):
331+ _check_interface('exported_as_webservice_collection()', interface)
332+ _check_collection_default_content(
333+ 'exported_as_webservice_collection()', interface)
334+ tag = interface.queryTaggedValue(LAZR_WEBSERVICE_EXPORTED)
335+ if tag is None:
336+ tag = {}
337+ interface.setTaggedValue(LAZR_WEBSERVICE_EXPORTED, tag)
338+ tag['type'] = COLLECTION_TYPE
339+ tag['collection_entry_schema'] = self.entry_schema
340+ annotate_exported_methods(interface)
341+ return interface
342+
343+
344 class collection_default_content:
345 """Decorates the method that provides the default values of a collection.
346
347- :raises TypeError: if not called from within an interface exported as a
348- collection, or if used more than once in the same interface.
349+ :raises TypeError: if called from within an interface exported as
350+ something other than a collection, or if used more than once for the
351+ same version in the same interface.
352 """
353
354 def __init__(self, version=None, **params):
355@@ -407,9 +519,20 @@
356 """
357 _check_called_from_interface_def('@collection_default_content')
358
359+ # We used to check that this decorator is being used from within an
360+ # interface exported as a collection. However, it isn't possible to
361+ # do this when using the @exported_as_webservice_collection
362+ # decorator rather than the export_as_webservice_collection function
363+ # (which is based on class advice, and so cannot work in Python 3):
364+ # that decorator isn't called until the rest of the interface has
365+ # been defined, so it hasn't had a chance to set any tags on the
366+ # interface by the time decorators of methods in that interface are
367+ # called. The best we can do is to check that we don't already
368+ # positively know that the interface is exported as an entry
369+ # instead.
370 tags = _get_interface_tags()
371- tag = tags.get(LAZR_WEBSERVICE_EXPORTED)
372- if tag is None or tag['type'] != COLLECTION_TYPE:
373+ tag = tags.setdefault(LAZR_WEBSERVICE_EXPORTED, {})
374+ if 'type' in tag and tag['type'] != COLLECTION_TYPE:
375 raise TypeError(
376 "@collection_default_content can only be used from within an "
377 "interface exported as a collection.")
378@@ -494,8 +617,11 @@
379
380 The actual method will be wrapped in an IMethod specification once the
381 Interface is complete. So we save the annotations in an attribute of the
382- method, and the class advisor invoked by export_as_webservice_entry() and
383- export_as_webservice_collection() will do the final tagging.
384+ method, and either the class advisor invoked by
385+ export_as_webservice_entry() and export_as_webservice_collection() or
386+ the @exported_as_webservice_entry() and
387+ @exported_as_webservice_collection() decorators will do the final
388+ tagging.
389 """
390
391 def __call__(self, method):
392
393=== modified file 'src/lazr/restful/docs/webservice-declarations.rst'
394--- src/lazr/restful/docs/webservice-declarations.rst 2020-02-04 13:17:32 +0000
395+++ src/lazr/restful/docs/webservice-declarations.rst 2020-02-19 16:09:23 +0000
396@@ -20,7 +20,7 @@
397
398 Only entries are exported as data. You can mark that one of your content
399 interface is exported on the web service as an entry, by using the
400-export_as_webservice_entry() declaration.
401+exported_as_webservice_entry() declaration.
402
403 You can mark the fields that should be part of the entry data model by
404 using the exported() wrapper. It takes an optional 'exported_as' parameter
405@@ -34,10 +34,12 @@
406 >>> from zope.interface import Interface
407 >>> from zope.schema import Text, TextLine, Float, List
408 >>> from lazr.restful.declarations import (
409- ... export_as_webservice_entry, exported)
410- >>> class IBook(Interface):
411+ ... exported,
412+ ... exported_as_webservice_entry,
413+ ... )
414+ >>> @exported_as_webservice_entry()
415+ ... class IBook(Interface):
416 ... """A simple book data model."""
417- ... export_as_webservice_entry()
418 ...
419 ... title = exported(TextLine(title=u'The book title'))
420 ...
421@@ -94,8 +96,8 @@
422 Only IField can be exported as entry fields.
423
424 >>> from zope.interface import Attribute
425- >>> class Foo(Interface):
426- ... export_as_webservice_entry()
427+ >>> @exported_as_webservice_entry()
428+ ... class Foo(Interface):
429 ... not_a_field = exported(Attribute('A standard attribute'))
430 Traceback (most recent call last):
431 ...
432@@ -104,8 +106,8 @@
433 Object fields cannot be exported because they cause validation problems.
434
435 >>> from zope.schema import Object
436- >>> class UsesIObject(Interface):
437- ... export_as_webservice_entry()
438+ >>> @exported_as_webservice_entry()
439+ ... class UsesIObject(Interface):
440 ... object = exported(Object(schema=Interface))
441 Traceback (most recent call last):
442 TypeError: Object exported; use Reference instead.
443@@ -114,28 +116,21 @@
444 avoid the validation problems.
445
446 >>> from lazr.restful.fields import Reference
447- >>> class UsesIReference(Interface):
448- ... export_as_webservice_entry()
449+ >>> @exported_as_webservice_entry()
450+ ... class UsesIReference(Interface):
451 ... object = exported(Reference(schema=Interface))
452
453-In the same vein, export_as_webservice_entry() can only be used on
454+In the same vein, exported_as_webservice_entry() can only be used on
455 Interface.
456
457- >>> class NotAnInterface(object):
458- ... export_as_webservice_entry()
459+ >>> @exported_as_webservice_entry()
460+ ... class NotAnInterface(object):
461+ ... pass
462 Traceback (most recent call last):
463 ...
464- TypeError: export_as_webservice_entry() can only be used on an
465+ TypeError: exported_as_webservice_entry() can only be used on an
466 interface.
467
468-And from within a class declaration.
469-
470- >>> export_as_webservice_entry()
471- Traceback (most recent call last):
472- ...
473- TypeError: export_as_webservice_entry() can only be used from within
474- an interface definition.
475-
476 publish_web_link
477 ----------------
478
479@@ -143,11 +138,11 @@
480 lazr.restful will publish a web_link for each entry, pointing to the
481 corresponding website page. For a given entry type, you can suppress
482 this by passing in False for the `publish_web_link` argument to
483-`export_as_webservice_entry`.
484+`exported_as_webservice_entry`.
485
486 >>> from zope.interface import Attribute
487- >>> class INotOnTheWebsite(Interface):
488- ... export_as_webservice_entry(publish_web_link=False)
489+ >>> @exported_as_webservice_entry(publish_web_link=False)
490+ ... class INotOnTheWebsite(Interface):
491 ... field = exported(TextLine(title=u"A field."))
492 >>> print_export_tag(INotOnTheWebsite)
493 _as_of_was_used: False
494@@ -169,24 +164,24 @@
495
496 >>> from zope.schema import Object
497 >>> from lazr.restful.fields import CollectionField
498- >>> class IBookWithComments(IBook):
499+ >>> @exported_as_webservice_entry()
500+ ... class IBookWithComments(IBook):
501 ... """A book with some comments."""
502- ... export_as_webservice_entry()
503 ...
504 ... comments = exported(CollectionField(
505 ... value_type=Object(schema=ISimpleComment)))
506
507 Top-level collections are different though, they are exported by using the
508-export_as_webservice_collection() in the ``Set`` class. The method that returns
509-all of the collection items must be tagged with @collection_default_content
510-decorator.
511+exported_as_webservice_collection() in the ``Set`` class. The method that
512+returns all of the collection items must be tagged with
513+@collection_default_content decorator.
514
515 >>> from lazr.restful.declarations import (
516- ... export_as_webservice_collection, collection_default_content,
517+ ... exported_as_webservice_collection, collection_default_content,
518 ... REQUEST_USER)
519- >>> class IBookSet(Interface):
520+ >>> @exported_as_webservice_collection(IBook)
521+ ... class IBookSet(Interface):
522 ... """Set of all the books in the system."""
523- ... export_as_webservice_collection(IBook)
524 ...
525 ... @collection_default_content()
526 ... def getAllBooks():
527@@ -197,9 +192,9 @@
528 special REQUEST_USER marker that can be used to specify that this
529 parameter should contain the logged in user.
530
531- >>> class ICheckedOutBookSet(Interface):
532+ >>> @exported_as_webservice_collection(IBook)
533+ ... class ICheckedOutBookSet(Interface):
534 ... """Give access to the checked out books."""
535- ... export_as_webservice_collection(IBook)
536 ...
537 ... @collection_default_content(user=REQUEST_USER, title='')
538 ... def getByTitle(title, user):
539@@ -227,15 +222,16 @@
540 The entry schema for a collection must be provided and must be an
541 interface:
542
543- >>> class MissingEntrySchema(Interface):
544- ... export_as_webservice_collection()
545+ >>> @exported_as_webservice_collection()
546+ ... class MissingEntrySchema(Interface):
547+ ... pass
548 Traceback (most recent call last):
549 ...
550- TypeError: export_as_webservice_collection() takes exactly 1
551- argument (0 given)
552+ TypeError: __init__() takes exactly 2 arguments (1 given)
553
554- >>> class InvalidEntrySchema(Interface):
555- ... export_as_webservice_collection("not an interface")
556+ >>> @exported_as_webservice_collection("not an interface")
557+ ... class InvalidEntrySchema(Interface):
558+ ... pass
559 Traceback (most recent call last):
560 ...
561 TypeError: entry_schema must be an interface.
562@@ -246,17 +242,18 @@
563 >>> class IDummyInterface(Interface):
564 ... pass
565
566- >>> class MissingDefaultContent(Interface):
567- ... export_as_webservice_collection(IDummyInterface)
568+ >>> @exported_as_webservice_collection(IDummyInterface)
569+ ... class MissingDefaultContent(Interface):
570+ ... pass
571 Traceback (most recent call last):
572 ...
573- TypeError: export_as_webservice_collection() is missing a method
574+ TypeError: exported_as_webservice_collection() is missing a method
575 tagged with @collection_default_content.
576
577 As it is an error, to mark more than one method:
578
579- >>> class TwoDefaultContent(Interface):
580- ... export_as_webservice_collection(IDummyInterface)
581+ >>> @exported_as_webservice_collection(IDummyInterface)
582+ ... class TwoDefaultContent(Interface):
583 ... @collection_default_content()
584 ... def getAll1():
585 ... """A first getAll()."""
586@@ -268,23 +265,16 @@
587 TypeError: Only one method can be marked with
588 @collection_default_content for version '(earliest version)'.
589
590-export_as_webservice_collection() can only be used on Interface.
591+exported_as_webservice_collection() can only be used on Interface.
592
593- >>> class NotAnInterface(object):
594- ... export_as_webservice_collection(IDummyInterface)
595+ >>> @exported_as_webservice_collection(IDummyInterface)
596+ ... class NotAnInterface(object):
597+ ... pass
598 Traceback (most recent call last):
599 ...
600- TypeError: export_as_webservice_collection() can only be used on an
601+ TypeError: exported_as_webservice_collection() can only be used on an
602 interface.
603
604-And from within a class declaration.
605-
606- >>> export_as_webservice_collection(IDummyInterface)
607- Traceback (most recent call last):
608- ...
609- TypeError: export_as_webservice_collection() can only be used from
610- within an interface definition.
611-
612 collection_default_content() can only be used from within an Interface
613 declaration:
614
615@@ -295,17 +285,6 @@
616 TypeError: @collection_default_content can only be used from within
617 an interface definition.
618
619-And the interface must have been exported as a collection:
620-
621- >>> class NotExported(Interface):
622- ... export_as_webservice_entry()
623- ... @collection_default_content()
624- ... def a_function(): pass
625- Traceback (most recent call last):
626- ...
627- TypeError: @collection_default_content can only be used from within an
628- interface exported as a collection.
629-
630 Exporting methods
631 =================
632
633@@ -361,9 +340,9 @@
634 ... operation_returns_entry, operation_returns_collection_of,
635 ... rename_parameters_as)
636 >>> from lazr.restful.interface import copy_field
637- >>> class IBookSetOnSteroids(IBookSet):
638+ >>> @exported_as_webservice_collection(IBook)
639+ ... class IBookSetOnSteroids(IBookSet):
640 ... """IBookSet supporting some methods."""
641- ... export_as_webservice_collection(IBook)
642 ...
643 ... @collection_default_content()
644 ... @operation_parameters(
645@@ -405,9 +384,9 @@
646 >>> from lazr.restful.declarations import (
647 ... call_with, export_destructor_operation, export_write_operation,
648 ... REQUEST_USER)
649- >>> class IBookOnSteroids(IBook):
650+ >>> @exported_as_webservice_entry()
651+ ... class IBookOnSteroids(IBook):
652 ... """IBook with some methods."""
653- ... export_as_webservice_entry()
654 ...
655 ... @call_with(who=REQUEST_USER, kind='normal')
656 ... @export_write_operation()
657@@ -474,9 +453,8 @@
658 @export_factory_operation to specify parameters that are not part of the
659 schema.
660
661- >>> class ComplexBookFactory(Interface):
662- ... export_as_webservice_entry()
663- ...
664+ >>> @exported_as_webservice_entry()
665+ ... class ComplexBookFactory(Interface):
666 ... @operation_parameters(collection=TextLine())
667 ... @export_factory_operation(IBook, ['author', 'title'])
668 ... def create_book(author, title, collection):
669@@ -498,9 +476,8 @@
670 Parameters default and required attributes are set automatically based
671 on the method signature.
672
673- >>> class ComplexParameterDefinition(Interface):
674- ... export_as_webservice_entry()
675- ...
676+ >>> @exported_as_webservice_entry()
677+ ... class ComplexParameterDefinition(Interface):
678 ... @operation_parameters(
679 ... required1=TextLine(),
680 ... required2=TextLine(default=u'Not required'),
681@@ -568,8 +545,8 @@
682 An error is also reported if not enough parameters are defined as
683 exported:
684
685- >>> class MissingParameter(Interface):
686- ... export_as_webservice_entry()
687+ >>> @exported_as_webservice_entry()
688+ ... class MissingParameter(Interface):
689 ... @call_with(param1=1)
690 ... @operation_parameters(
691 ... param2=TextLine())
692@@ -583,8 +560,8 @@
693 Defining a parameter not available on the method also results in an
694 error:
695
696- >>> class BadParameter(Interface):
697- ... export_as_webservice_entry()
698+ >>> @exported_as_webservice_entry()
699+ ... class BadParameter(Interface):
700 ... @operation_parameters(
701 ... no_such_param=TextLine())
702 ... @export_read_operation()
703@@ -597,8 +574,8 @@
704 But that's not a problem if the exported method actually takes arbitrary
705 keyword parameters:
706
707- >>> class AnyParameter(Interface):
708- ... export_as_webservice_entry()
709+ >>> @exported_as_webservice_entry()
710+ ... class AnyParameter(Interface):
711 ... @operation_parameters(
712 ... param1=TextLine())
713 ... @export_read_operation()
714@@ -607,8 +584,8 @@
715 When using @export_factory_operation, TypeError will also be raised if
716 one of the field doesn't exists in the schema:
717
718- >>> class MissingParameter(Interface):
719- ... export_as_webservice_entry()
720+ >>> @exported_as_webservice_entry()
721+ ... class MissingParameter(Interface):
722 ... @export_factory_operation(IBook, ['no_such_field'])
723 ... def a_method(): pass
724 Traceback (most recent call last):
725@@ -617,8 +594,8 @@
726
727 Or if the field name doesn't represent a field:
728
729- >>> class NotAField(Interface):
730- ... export_as_webservice_entry()
731+ >>> @exported_as_webservice_entry()
732+ ... class NotAField(Interface):
733 ... @export_factory_operation(IBookOnSteroids, ['checkout'])
734 ... def a_method(): pass
735 Traceback (most recent call last):
736@@ -627,8 +604,8 @@
737
738 Or if @operation_parameters redefine a field specified in the factory:
739
740- >>> class Redefinition(Interface):
741- ... export_as_webservice_entry()
742+ >>> @exported_as_webservice_entry()
743+ ... class Redefinition(Interface):
744 ... @operation_parameters(title=TextLine())
745 ... @export_factory_operation(IBookOnSteroids, ['title'])
746 ... def create_book(title): pass
747@@ -638,8 +615,8 @@
748
749 All parameters definitions must be schema fields:
750
751- >>> class BadParameterDefinition(Interface):
752- ... export_as_webservice_entry()
753+ >>> @exported_as_webservice_entry()
754+ ... class BadParameterDefinition(Interface):
755 ... @operation_parameters(a_param=object())
756 ... @export_read_operation()
757 ... def a_method(): pass
758@@ -673,8 +650,8 @@
759 @operation_returns_collection_of will only accept an IInterface as
760 argument.
761
762- >>> class ReturnOtherThanInterface(Interface):
763- ... export_as_webservice_entry()
764+ >>> @exported_as_webservice_entry()
765+ ... class ReturnOtherThanInterface(Interface):
766 ... @operation_returns_entry("not-an-interface")
767 ... @export_read_operation()
768 ... def a_method(**kwargs): pass
769@@ -682,8 +659,8 @@
770 ...
771 TypeError: Entry type not-an-interface does not provide IInterface.
772
773- >>> class ReturnOtherThanInterface(Interface):
774- ... export_as_webservice_entry()
775+ >>> @exported_as_webservice_entry()
776+ ... class ReturnOtherThanInterface(Interface):
777 ... @operation_returns_collection_of("not-an-interface")
778 ... @export_read_operation()
779 ... def a_method(**kwargs): pass
780@@ -780,9 +757,8 @@
781 ... def rename(new_name):
782 ... """Rename the object."""
783
784- >>> class IUser(IHasName):
785- ... export_as_webservice_entry()
786- ...
787+ >>> @exported_as_webservice_entry()
788+ ... class IUser(IHasName):
789 ... nickname = exported(TextLine())
790 ...
791 ... @operation_parameters(to=Object(IHasName), msg=TextLine())
792@@ -824,9 +800,8 @@
793 adapters for your entry's class instead of to the class itself. For example,
794 we can have an IDeveloper interface contributing to IUser.
795
796- >>> class IDeveloper(Interface):
797- ... export_as_webservice_entry(contributes_to=[IUser])
798- ...
799+ >>> @exported_as_webservice_entry(contributes_to=[IUser])
800+ ... class IDeveloper(Interface):
801 ... programming_languages = exported(List(
802 ... title=u'Programming Languages spoken by this developer'))
803
804@@ -1359,8 +1334,8 @@
805 A method can be designated as a destructor for the entry. Here, the
806 destroy() method is designated as the destructor for IHasText.
807
808- >>> class IHasText(Interface):
809- ... export_as_webservice_entry()
810+ >>> @exported_as_webservice_entry()
811+ ... class IHasText(Interface):
812 ... text = exported(TextLine(readonly=True))
813 ...
814 ... @export_destructor_operation()
815@@ -1370,8 +1345,8 @@
816
817 A destructor method cannot take any free arguments.
818
819- >>> class IHasText(Interface):
820- ... export_as_webservice_entry()
821+ >>> @exported_as_webservice_entry()
822+ ... class IHasText(Interface):
823 ... text = exported(TextLine(readonly=True))
824 ...
825 ... @export_destructor_operation()
826@@ -1384,8 +1359,8 @@
827 In version (earliest version), the "destroy" method takes 1:
828 "argument".
829
830- >>> class IHasText(Interface):
831- ... export_as_webservice_entry()
832+ >>> @exported_as_webservice_entry()
833+ ... class IHasText(Interface):
834 ... text = exported(TextLine(readonly=True))
835 ...
836 ... @export_destructor_operation()
837@@ -1397,8 +1372,8 @@
838 An entry cannot have more than one destructor.
839
840 >>> from lazr.restful.declarations import export_destructor_operation
841- >>> class IHasText(Interface):
842- ... export_as_webservice_entry()
843+ >>> @exported_as_webservice_entry()
844+ ... class IHasText(Interface):
845 ... text = exported(TextLine(readonly=True))
846 ...
847 ... @export_destructor_operation()
848@@ -1421,8 +1396,8 @@
849 set_text() method is designated as the mutator for the 'text' field.
850
851 >>> from lazr.restful.declarations import mutator_for
852- >>> class IHasText(Interface):
853- ... export_as_webservice_entry()
854+ >>> @exported_as_webservice_entry()
855+ ... class IHasText(Interface):
856 ... text = exported(TextLine(readonly=True))
857 ...
858 ... @mutator_for(text)
859@@ -1473,8 +1448,8 @@
860
861 It's not necessary to expose the mutator method as a write operation.
862
863- >>> class IHasText(Interface):
864- ... export_as_webservice_entry()
865+ >>> @exported_as_webservice_entry()
866+ ... class IHasText(Interface):
867 ... text = exported(TextLine(readonly=True))
868 ...
869 ... @mutator_for(text)
870@@ -1485,8 +1460,8 @@
871 A mutator method must take only one argument: the new value for the
872 field. Taking no arguments is obviously an error.
873
874- >>> class ZeroArgumentMutator(Interface):
875- ... export_as_webservice_entry()
876+ >>> @exported_as_webservice_entry()
877+ ... class ZeroArgumentMutator(Interface):
878 ... value = exported(TextLine(readonly=True))
879 ...
880 ... @mutator_for(value)
881@@ -1499,8 +1474,8 @@
882
883 Taking more than one argument is also an error...
884
885- >>> class TwoArgumentMutator(Interface):
886- ... export_as_webservice_entry()
887+ >>> @exported_as_webservice_entry()
888+ ... class TwoArgumentMutator(Interface):
889 ... value = exported(TextLine(readonly=True))
890 ...
891 ... @mutator_for(value)
892@@ -1514,8 +1489,8 @@
893 ...unless all but one of the arguments are spoken for by a call_with()
894 annotation. This definition does not result in a TypeError.
895
896- >>> class OneFixedArgumentMutator(Interface):
897- ... export_as_webservice_entry()
898+ >>> @exported_as_webservice_entry()
899+ ... class OneFixedArgumentMutator(Interface):
900 ... value = exported(TextLine(readonly=True))
901 ...
902 ... @mutator_for(value)
903@@ -1528,8 +1503,8 @@
904 A field can only have a mutator if it's read-only (not settable
905 directly).
906
907- >>> class WritableMutator(Interface):
908- ... export_as_webservice_entry()
909+ >>> @exported_as_webservice_entry()
910+ ... class WritableMutator(Interface):
911 ... value = exported(TextLine(readonly=False))
912 ...
913 ... @mutator_for(value)
914@@ -1542,8 +1517,8 @@
915
916 A field can only have one mutator.
917
918- >>> class FieldWithTwoMutators(Interface):
919- ... export_as_webservice_entry()
920+ >>> @exported_as_webservice_entry()
921+ ... class FieldWithTwoMutators(Interface):
922 ... value = exported(TextLine(readonly=True))
923 ...
924 ... @mutator_for(value)
925@@ -1567,8 +1542,8 @@
926
927 A read-write field can be published as read-only in the web service.
928
929- >>> class ExternallyReadOnlyField(Interface):
930- ... export_as_webservice_entry()
931+ >>> @exported_as_webservice_entry()
932+ ... class ExternallyReadOnlyField(Interface):
933 ... value = exported(TextLine(readonly=False), readonly=True)
934
935 >>> interfaces = generate_entry_interfaces(
936@@ -1584,8 +1559,8 @@
937 just by declaring it read-write. You have to provide a
938 mutator.
939
940- >>> class InternallyReadOnlyField(Interface):
941- ... export_as_webservice_entry()
942+ >>> @exported_as_webservice_entry()
943+ ... class InternallyReadOnlyField(Interface):
944 ... value = exported(TextLine(readonly=True), readonly=False)
945
946 >>> generate_entry_interfaces(InternallyReadOnlyField, [], 'beta')
947@@ -1602,10 +1577,9 @@
948 the @cache_for decorator:
949
950 >>> from lazr.restful.declarations import cache_for
951- >>>
952- >>> class ICachedBookSet(IBookSet):
953+ >>> @exported_as_webservice_collection(IBook)
954+ ... class ICachedBookSet(IBookSet):
955 ... """IBookSet supporting caching."""
956- ... export_as_webservice_collection(IBook)
957 ...
958 ... @collection_default_content()
959 ... @export_read_operation()
960@@ -1667,9 +1641,8 @@
961
962 >>> from lazr.restful.declarations import generate_operation_adapter
963
964- >>> class IMultiVersionCollection(Interface):
965- ... export_as_webservice_collection(Interface)
966- ...
967+ >>> @exported_as_webservice_collection(Interface)
968+ ... class IMultiVersionCollection(Interface):
969 ... @collection_default_content('2.0')
970 ... def content_20():
971 ... """The content method for version 2.0."""
972@@ -1768,9 +1741,8 @@
973 'new_in_10' in '1.0', and renamed to 'renamed_in_30' in '3.0'.
974
975 >>> from zope.schema import Text, Float
976- >>> class IMultiVersionEntry(Interface):
977- ... export_as_webservice_entry()
978- ...
979+ >>> @exported_as_webservice_entry()
980+ ... class IMultiVersionEntry(Interface):
981 ... field = exported(TextLine())
982 ...
983 ... field2 = exported(Text(), exported_as='unchanging_name')
984@@ -1943,9 +1915,8 @@
985 Why does generate_entry_interfaces need a list of version strings?
986 This example should make it clear.
987
988- >>> class IAmbiguousMultiVersion(Interface):
989- ... export_as_webservice_entry()
990- ...
991+ >>> @exported_as_webservice_entry()
992+ ... class IAmbiguousMultiVersion(Interface):
993 ... field1 = exported(TextLine(),
994 ... ('foo', dict(exported_as='foo_name')))
995 ... field2 = exported(TextLine(),
996@@ -1975,9 +1946,8 @@
997 generate_entry_interfaces() modified the class to reflect the original
998 list of versions.)
999
1000- >>> class IAmbiguousMultiVersion(Interface):
1001- ... export_as_webservice_entry()
1002- ...
1003+ >>> @exported_as_webservice_entry()
1004+ ... class IAmbiguousMultiVersion(Interface):
1005 ... field1 = exported(TextLine(),
1006 ... ('foo', dict(exported_as='foo_name')))
1007 ... field2 = exported(TextLine(),
1008@@ -2009,9 +1979,8 @@
1009 You'll get an error if you annotate a field with a version that turns
1010 out not to be included in the version list.
1011
1012- >>> class INonexistentVersionEntry(Interface):
1013- ... export_as_webservice_entry()
1014- ...
1015+ >>> @exported_as_webservice_entry()
1016+ ... class INonexistentVersionEntry(Interface):
1017 ... field = exported(TextLine(),
1018 ... ('2.0', dict(exported_as='foo')),
1019 ... ('1.0', dict(exported_as='bar')))
1020@@ -2026,9 +1995,8 @@
1021 You'll get an error if you put an earlier version's annotations on top
1022 of a later version.
1023
1024- >>> class IWrongOrderEntry(Interface):
1025- ... export_as_webservice_entry()
1026- ...
1027+ >>> @exported_as_webservice_entry()
1028+ ... class IWrongOrderEntry(Interface):
1029 ... field = exported(TextLine(),
1030 ... ('1.0', dict(exported_as='bar')),
1031 ... ('2.0', dict(exported_as='foo')))
1032@@ -2042,9 +2010,8 @@
1033 You'll get an error if you define annotations twice for the same
1034 version. This can happen because you repeated the version annotations:
1035
1036- >>> class IDuplicateEntry(Interface):
1037- ... export_as_webservice_entry()
1038- ...
1039+ >>> @exported_as_webservice_entry()
1040+ ... class IDuplicateEntry(Interface):
1041 ... field = exported(TextLine(),
1042 ... ('beta', dict(exported_as='another_beta_name')),
1043 ... ('beta', dict(exported_as='beta_name')))
1044@@ -2059,9 +2026,8 @@
1045 using keyword arguments, and then explicitly defined conflicting
1046 values.
1047
1048- >>> class IDuplicateEntry(Interface):
1049- ... export_as_webservice_entry()
1050- ...
1051+ >>> @exported_as_webservice_entry()
1052+ ... class IDuplicateEntry(Interface):
1053 ... field = exported(TextLine(),
1054 ... ('beta', dict(exported_as='beta_name')),
1055 ... exported_as='earliest_name')
1056@@ -2077,18 +2043,16 @@
1057 You'll get an error if you include an unrecognized key in a field's
1058 version definition.
1059
1060- >>> class InvalidMultiVersionEntry(Interface):
1061- ... export_as_webservice_entry()
1062- ...
1063+ >>> @exported_as_webservice_entry()
1064+ ... class InvalidMultiVersionEntry(Interface):
1065 ... field = exported(TextLine(),
1066 ... ('3.0', dict(not_recognized='this will error')))
1067 Traceback (most recent call last):
1068 ...
1069 ValueError: Unrecognized annotation for version "3.0": "not_recognized"
1070
1071- >>> class InvalidMultiVersionEntry(Interface):
1072- ... export_as_webservice_entry()
1073- ...
1074+ >>> @exported_as_webservice_entry()
1075+ ... class InvalidMultiVersionEntry(Interface):
1076 ... field = exported(TextLine(), not_recognized='this will error')
1077 Traceback (most recent call last):
1078 ...
1079@@ -2098,9 +2062,8 @@
1080 every version, even when an interface does not change at all between
1081 versions. (This could be optimized away.)
1082
1083- >>> class IUnchangingEntry(Interface):
1084- ... export_as_webservice_entry()
1085- ...
1086+ >>> @exported_as_webservice_entry()
1087+ ... class IUnchangingEntry(Interface):
1088 ... field = exported(TextLine(),
1089 ... ('3.0', dict(exported_as='30_name')),
1090 ... ('beta', dict(exported_as='unchanging_name')))
1091@@ -2118,10 +2081,8 @@
1092 2.0, 1.0, and in an unnamed pre-1.0 version.
1093
1094 >>> from lazr.restful.declarations import operation_for_version
1095- >>> class IMultiVersionMethod(Interface):
1096- ... export_as_webservice_entry()
1097- ...
1098- ...
1099+ >>> @exported_as_webservice_entry()
1100+ ... class IMultiVersionMethod(Interface):
1101 ... @cache_for(300)
1102 ... @operation_for_version('3.0')
1103 ...
1104@@ -2298,8 +2259,8 @@
1105 Let's define an operation that's introduced in 1.0 and removed in 2.0.
1106
1107 >>> from lazr.restful.declarations import operation_removed_in_version
1108- >>> class DisappearingMultiversionMethod(Interface):
1109- ... export_as_webservice_entry()
1110+ >>> @exported_as_webservice_entry()
1111+ ... class DisappearingMultiversionMethod(Interface):
1112 ... @operation_removed_in_version(2.0)
1113 ... @operation_parameters(arg=Float())
1114 ... @export_read_operation()
1115@@ -2352,9 +2313,8 @@
1116 In this example, the type of the operation, the type and number of the
1117 arguments, and the return value change in version 1.0.
1118
1119- >>> class ReadOrWriteMethod(Interface):
1120- ... export_as_webservice_entry()
1121- ...
1122+ >>> @exported_as_webservice_entry()
1123+ ... class ReadOrWriteMethod(Interface):
1124 ... @operation_parameters(arg=TextLine(), arg2=TextLine())
1125 ... @export_write_operation()
1126 ... @operation_removed_in_version(1.0)
1127@@ -2403,9 +2363,8 @@
1128
1129 Different versions can define different mutator methods for the same field.
1130
1131- >>> class IDifferentMutators(Interface):
1132- ... export_as_webservice_entry()
1133- ...
1134+ >>> @exported_as_webservice_entry()
1135+ ... class IDifferentMutators(Interface):
1136 ... field = exported(TextLine(readonly=True))
1137 ...
1138 ... @mutator_for(field)
1139@@ -2427,9 +2386,8 @@
1140
1141 But you can't define two mutators for the same field in the same version.
1142
1143- >>> class IDuplicateMutator(Interface):
1144- ... export_as_webservice_entry()
1145- ...
1146+ >>> @exported_as_webservice_entry()
1147+ ... class IDuplicateMutator(Interface):
1148 ... field = exported(TextLine(readonly=True))
1149 ...
1150 ... @mutator_for(field)
1151@@ -2455,9 +2413,8 @@
1152 giving its name), and then define another one explicitly (giving the
1153 name of the earliest version.)
1154
1155- >>> class IImplicitAndExplicitMutator(Interface):
1156- ... export_as_webservice_entry()
1157- ...
1158+ >>> @exported_as_webservice_entry()
1159+ ... class IImplicitAndExplicitMutator(Interface):
1160 ... field = exported(TextLine(readonly=True))
1161 ...
1162 ... @mutator_for(field)
1163@@ -2490,9 +2447,8 @@
1164 because the generate_entry_interfaces call modified the original
1165 class's annotations in place.)
1166
1167- >>> class IImplicitAndExplicitMutator(Interface):
1168- ... export_as_webservice_entry()
1169- ...
1170+ >>> @exported_as_webservice_entry()
1171+ ... class IImplicitAndExplicitMutator(Interface):
1172 ... field = exported(TextLine(readonly=True))
1173 ...
1174 ... @mutator_for(field)
1175@@ -2522,9 +2478,8 @@
1176 earliest version, but not in '1.0'. This is fine: the 1.0 value for
1177 'fixed2' will be inherited from the previous version.
1178
1179- >>> class IGoodDestructorEntry(Interface):
1180- ... export_as_webservice_entry()
1181- ...
1182+ >>> @exported_as_webservice_entry()
1183+ ... class IGoodDestructorEntry(Interface):
1184 ... @call_with(fixed1="value3")
1185 ... @operation_for_version('1.0')
1186 ... @export_destructor_operation()
1187@@ -2542,9 +2497,8 @@
1188 'fixed2' is a problem. The fact that 'fixed2' is fixed in 3.0 doesn't
1189 help; the method is incompletely specified in 2.0.
1190
1191- >>> class IBadDestructorEntry(Interface):
1192- ... export_as_webservice_entry()
1193- ...
1194+ >>> @exported_as_webservice_entry()
1195+ ... class IBadDestructorEntry(Interface):
1196 ... @call_with(fixed2="value4")
1197 ... @operation_for_version('2.0')
1198 ... @export_destructor_operation()
1199@@ -2683,8 +2637,8 @@
1200 that named operation annotations proceed from the top down rather than
1201 the bottom up.)
1202
1203- >>> class WrongOrderVersions(Interface):
1204- ... export_as_webservice_entry()
1205+ >>> @exported_as_webservice_entry()
1206+ ... class WrongOrderVersions(Interface):
1207 ... @export_operation_as('10_name')
1208 ... @operation_for_version("1.0")
1209 ... @operation_parameters(arg=Float())
1210@@ -2707,8 +2661,8 @@
1211 Here's a class in which a named operation is removed in version 1.0
1212 and then annotated without being reinstated.
1213
1214- >>> class AnnotatingARemovedMethod(Interface):
1215- ... export_as_webservice_entry()
1216+ >>> @exported_as_webservice_entry()
1217+ ... class AnnotatingARemovedMethod(Interface):
1218 ... @operation_parameters(arg=TextLine())
1219 ... @export_operation_as('already_been_removed')
1220 ... @operation_removed_in_version("2.0")
1221@@ -2740,9 +2694,8 @@
1222 Let's consider an entry that defines a mutator in the very first
1223 version of the web service and never removes it.
1224
1225- >>> class IBetaMutatorEntry(Interface):
1226- ... export_as_webservice_entry()
1227- ...
1228+ >>> @exported_as_webservice_entry()
1229+ ... class IBetaMutatorEntry(Interface):
1230 ... field = exported(TextLine(readonly=True))
1231 ...
1232 ... @mutator_for(field)
1233@@ -2799,9 +2752,8 @@
1234 Here's an entry that defines a mutator method in version 2.0, after
1235 the cutoff point.
1236
1237- >>> class I20MutatorEntry(Interface):
1238- ... export_as_webservice_entry()
1239- ...
1240+ >>> @exported_as_webservice_entry()
1241+ ... class I20MutatorEntry(Interface):
1242 ... field = exported(TextLine(readonly=True))
1243 ...
1244 ... @mutator_for(field)
1245@@ -2843,9 +2795,8 @@
1246 Here's a named operation that was defined in '1.0' and promoted to a
1247 mutator in '3.0'.
1248
1249- >>> class IOperationPromotedToMutator(Interface):
1250- ... export_as_webservice_entry()
1251- ...
1252+ >>> @exported_as_webservice_entry()
1253+ ... class IOperationPromotedToMutator(Interface):
1254 ... field = exported(TextLine(readonly=True))
1255 ...
1256 ... @mutator_for(field)
1257@@ -2915,9 +2866,8 @@
1258 as 'set_value' in version 2.0, and a third operation to be published as
1259 'set_value' in version 3.0.
1260
1261- >>> class IMutatorPlusNamedOperationEntry(Interface):
1262- ... export_as_webservice_entry()
1263- ...
1264+ >>> @exported_as_webservice_entry()
1265+ ... class IMutatorPlusNamedOperationEntry(Interface):
1266 ... field = exported(TextLine(readonly=True))
1267 ...
1268 ... @mutator_for(field)
1269@@ -2981,9 +2931,8 @@
1270 the 'beta' version of the web service. (We have to redefine the class
1271 to avoid conflicting registrations.)
1272
1273- >>> class IBetaMutatorEntry2(IBetaMutatorEntry):
1274- ... export_as_webservice_entry()
1275- ...
1276+ >>> @exported_as_webservice_entry()
1277+ ... class IBetaMutatorEntry2(IBetaMutatorEntry):
1278 ... field = exported(TextLine(readonly=True))
1279 ...
1280 ... @mutator_for(field)
1281@@ -3026,9 +2975,8 @@
1282 Again, we have to publish a new entry class, to avoid conflicting
1283 registrations.
1284
1285- >>> class IBetaMutatorEntry3(Interface):
1286- ... export_as_webservice_entry()
1287- ...
1288+ >>> @exported_as_webservice_entry()
1289+ ... class IBetaMutatorEntry3(Interface):
1290 ... field = exported(TextLine(readonly=True))
1291 ...
1292 ... @mutator_for(field)
1293
1294=== modified file 'src/lazr/restful/example/base/interfaces.py'
1295--- src/lazr/restful/example/base/interfaces.py 2020-02-04 11:52:59 +0000
1296+++ src/lazr/restful/example/base/interfaces.py 2020-02-19 16:09:23 +0000
1297@@ -29,11 +29,19 @@
1298 from lazr.restful.fields import CollectionField, Reference
1299 from lazr.restful.interfaces import IByteStorage, ITopLevelEntryLink
1300 from lazr.restful.declarations import (
1301- collection_default_content, export_as_webservice_collection,
1302- export_as_webservice_entry, export_destructor_operation,
1303- export_factory_operation, export_read_operation, export_write_operation,
1304- exported, operation_parameters, operation_returns_collection_of,
1305- operation_returns_entry, webservice_error)
1306+ collection_default_content,
1307+ export_destructor_operation,
1308+ export_factory_operation,
1309+ export_read_operation,
1310+ export_write_operation,
1311+ exported,
1312+ exported_as_webservice_collection,
1313+ exported_as_webservice_entry,
1314+ operation_parameters,
1315+ operation_returns_collection_of,
1316+ operation_returns_entry,
1317+ webservice_error,
1318+ )
1319
1320
1321 class AlreadyNew(Exception):
1322@@ -73,9 +81,9 @@
1323 """Traverse to a contained object."""
1324
1325
1326+@exported_as_webservice_entry(plural_name='dishes')
1327 class IDish(ILocation):
1328 """A dish, annotated for export to the web service."""
1329- export_as_webservice_entry(plural_name='dishes')
1330 name = exported(TextLine(title=u"Name", required=True))
1331 recipes = exported(CollectionField(
1332 title=u"Recipes in this cookbook",
1333@@ -85,9 +93,9 @@
1334 """Remove one of this dish's recipes."""
1335
1336
1337+@exported_as_webservice_entry()
1338 class IRecipe(ILocation):
1339 """A recipe, annotated for export to the web service."""
1340- export_as_webservice_entry()
1341 id = exported(Int(title=u"Unique ID", required=True))
1342 dish = exported(Reference(title=u"Dish", schema=IDish))
1343 cookbook = exported(Reference(title=u"Cookbook", schema=Interface))
1344@@ -104,9 +112,9 @@
1345 """Delete the recipe."""
1346
1347
1348+@exported_as_webservice_entry()
1349 class ICookbook(IHasGet, ILocation):
1350 """A cookbook, annotated for export to the web service."""
1351- export_as_webservice_entry()
1352 name = exported(TextLine(title=u"Name", required=True))
1353 copyright_date = exported(
1354 Date(title=u"Copyright Date",
1355@@ -165,6 +173,7 @@
1356 IRecipe['cookbook'].schema = ICookbook
1357
1358
1359+@exported_as_webservice_entry()
1360 class ICookbookSubclass(ICookbook):
1361 """A published subclass of ICookbook.
1362
1363@@ -172,16 +181,15 @@
1364 a published entry interface that subclasses another published
1365 entry interface.
1366 """
1367- export_as_webservice_entry()
1368
1369
1370 class IFeaturedCookbookLink(ITopLevelEntryLink):
1371 """A marker interface."""
1372
1373
1374+@exported_as_webservice_collection(ICookbook)
1375 class ICookbookSet(IHasGet):
1376 """The set of all cookbooks, annotated for export to the web service."""
1377- export_as_webservice_collection(ICookbook)
1378
1379 @collection_default_content()
1380 def getCookbooks():
1381@@ -214,18 +222,18 @@
1382 featured = Attribute("The currently featured cookbook.")
1383
1384
1385+@exported_as_webservice_collection(IDish)
1386 class IDishSet(IHasGet):
1387 """The set of all dishes, annotated for export to the web service."""
1388- export_as_webservice_collection(IDish)
1389
1390 @collection_default_content()
1391 def getDishes():
1392 """Return the list of dishes."""
1393
1394
1395+@exported_as_webservice_collection(IRecipe)
1396 class IRecipeSet(IHasGet):
1397 """The set of all recipes, annotated for export to the web service."""
1398- export_as_webservice_collection(IRecipe)
1399
1400 @collection_default_content()
1401 def getRecipes():
1402
1403=== modified file 'src/lazr/restful/example/base_extended/comments.py'
1404--- src/lazr/restful/example/base_extended/comments.py 2020-02-04 11:52:59 +0000
1405+++ src/lazr/restful/example/base_extended/comments.py 2020-02-19 16:09:23 +0000
1406@@ -7,11 +7,13 @@
1407 from lazr.restful.example.base.interfaces import IRecipe
1408
1409 from lazr.restful.declarations import (
1410- export_as_webservice_entry, exported)
1411-
1412-
1413+ exported,
1414+ exported_as_webservice_entry,
1415+ )
1416+
1417+
1418+@exported_as_webservice_entry(contributes_to=[IRecipe])
1419 class IHasComments(Interface):
1420- export_as_webservice_entry(contributes_to=[IRecipe])
1421 comments = exported(
1422 List(title=u'Comments made by users', value_type=Text()))
1423
1424
1425=== modified file 'src/lazr/restful/example/multiversion/resources.py'
1426--- src/lazr/restful/example/multiversion/resources.py 2020-02-04 11:52:59 +0000
1427+++ src/lazr/restful/example/multiversion/resources.py 2020-02-19 16:09:23 +0000
1428@@ -12,11 +12,20 @@
1429 from zope.location.interfaces import ILocation
1430
1431 from lazr.restful.declarations import (
1432- collection_default_content, export_as_webservice_collection,
1433- export_as_webservice_entry, export_destructor_operation,
1434- export_operation_as, export_read_operation, export_write_operation,
1435- exported, mutator_for, operation_for_version, operation_parameters,
1436- operation_removed_in_version, operation_returns_collection_of)
1437+ collection_default_content,
1438+ export_destructor_operation,
1439+ export_operation_as,
1440+ export_read_operation,
1441+ export_write_operation,
1442+ exported,
1443+ exported_as_webservice_collection,
1444+ exported_as_webservice_entry,
1445+ mutator_for,
1446+ operation_for_version,
1447+ operation_parameters,
1448+ operation_removed_in_version,
1449+ operation_returns_collection_of,
1450+ )
1451
1452 # Our implementations of these classes can be based on the
1453 # implementations from the WSGI example.
1454@@ -26,8 +35,8 @@
1455
1456 # Our interfaces _will_ diverge from the WSGI example interfaces, so
1457 # define them separately.
1458+@exported_as_webservice_entry()
1459 class IKeyValuePair(ILocation):
1460- export_as_webservice_entry()
1461 key = exported(Text(title=u"The key"))
1462 value = exported(Text(title=u"The value"))
1463 a_comment = exported(Text(title=u"A comment on this key-value pair.",
1464@@ -61,9 +70,8 @@
1465 """A destructor that simply sets .deleted to True."""
1466
1467
1468+@exported_as_webservice_collection(IKeyValuePair)
1469 class IPairSet(ILocation):
1470- export_as_webservice_collection(IKeyValuePair)
1471-
1472 # In versions 2.0 and 3.0, the collection of key-value pairs
1473 # includes all pairs.
1474 @collection_default_content("2.0")
1475
1476=== modified file 'src/lazr/restful/example/wsgi/README.txt'
1477--- src/lazr/restful/example/wsgi/README.txt 2009-09-16 16:00:46 +0000
1478+++ src/lazr/restful/example/wsgi/README.txt 2020-02-19 16:09:23 +0000
1479@@ -76,7 +76,7 @@
1480 lazr.restful.example.wsgi.resources. First we define an interface
1481 (IPairSet) that's decorated with lazr.restful decorators:
1482
1483-1. export_as_webservice_collection(IKeyValuePair) tells lazr.restful
1484+1. @exported_as_webservice_collection(IKeyValuePair) tells lazr.restful
1485 that an IPairSet should be published as a collection resource, as
1486 opposed to being published as an entry or not published at all. The
1487 "IKeyValuePair" lets lazr.restful know that this is a collection of
1488@@ -96,7 +96,7 @@
1489 lazr.restful.example.wsgi.resources. First we define an interface
1490 (IKeyValuePair) that's decorated with lazr.restful decorators:
1491
1492-1. export_as_webservice_entry() tells lazr.restful that an
1493+1. @exported_as_webservice_entry() tells lazr.restful that an
1494 IKeyValuePair should be published as an entry, as opposed to being
1495 published as an entry or not published at all.
1496
1497@@ -140,7 +140,7 @@
1498 3. There are no more path fragments, so traversal is complete. The
1499 PairSet object will be published as a collection resource. Why a
1500 collection? Because PairSet implements IPairSet, which is published
1501- as a collection. (Remember export_as_webservice_collection()?)
1502+ as a collection. (Remember @exported_as_webservice_collection()?)
1503
1504 You don't have to write all the traversal code your web service will
1505 ever use. There's a set of default rules that take over once your
1506@@ -158,7 +158,7 @@
1507 object.
1508
1509 4. Because IKeyValuePair is published as an entry (remember
1510- export_as_webservice_entry?), the custom traversal code now
1511+ @exported_as_webservice_entry?), the custom traversal code now
1512 stops. What happens next is entirely controlled by lazr.restful.
1513 Because of this, it doesn't make sense to have an entry subclass
1514 TraverseWithGet or implement IPublishTraverse.
1515
1516=== modified file 'src/lazr/restful/example/wsgi/resources.py'
1517--- src/lazr/restful/example/wsgi/resources.py 2020-02-04 11:52:59 +0000
1518+++ src/lazr/restful/example/wsgi/resources.py 2020-02-19 16:09:23 +0000
1519@@ -13,21 +13,23 @@
1520 from zope.location.interfaces import ILocation
1521
1522 from lazr.restful.declarations import (
1523- collection_default_content, export_as_webservice_collection,
1524- export_as_webservice_entry, exported)
1525+ collection_default_content,
1526+ exported,
1527+ exported_as_webservice_collection,
1528+ exported_as_webservice_entry,
1529+ )
1530 from lazr.restful.interfaces import IServiceRootResource
1531 from lazr.restful.simple import TraverseWithGet
1532
1533
1534+@exported_as_webservice_entry()
1535 class IKeyValuePair(ILocation):
1536- export_as_webservice_entry()
1537 key = exported(Text(title=u"The key"))
1538 value = exported(Text(title=u"The value"))
1539
1540
1541+@exported_as_webservice_collection(IKeyValuePair)
1542 class IPairSet(ILocation):
1543- export_as_webservice_collection(IKeyValuePair)
1544-
1545 @collection_default_content()
1546 def getPairs():
1547 """Return the key-value pairs."""
1548
1549=== modified file 'src/lazr/restful/testing/webservice.py'
1550--- src/lazr/restful/testing/webservice.py 2020-02-04 13:17:32 +0000
1551+++ src/lazr/restful/testing/webservice.py 2020-02-19 16:09:23 +0000
1552@@ -47,7 +47,7 @@
1553
1554 from lazr.restful.declarations import (
1555 collection_default_content, exported,
1556- export_as_webservice_collection, export_as_webservice_entry,
1557+ exported_as_webservice_collection, exported_as_webservice_entry,
1558 export_read_operation, operation_parameters)
1559 from lazr.restful.interfaces import (
1560 IServiceRootResource, IWebServiceClientRequest,
1561@@ -546,12 +546,12 @@
1562 """)
1563
1564
1565+@exported_as_webservice_entry()
1566 class IGenericEntry(Interface):
1567 """A simple, reusable entry interface for use in tests.
1568
1569 The entry publishes one field and one named operation.
1570 """
1571- export_as_webservice_entry()
1572
1573 # pylint: disable-msg=E0213
1574 a_field = exported(
1575@@ -569,9 +569,9 @@
1576 """
1577
1578
1579+@exported_as_webservice_collection(IGenericEntry)
1580 class IGenericCollection(Interface):
1581 """A simple collection containing `IGenericEntry`, for use in tests."""
1582- export_as_webservice_collection(IGenericEntry)
1583
1584 # pylint: disable-msg=E0211
1585 @collection_default_content()
1586
1587=== modified file 'src/lazr/restful/tests/test_declarations.py'
1588--- src/lazr/restful/tests/test_declarations.py 2020-02-13 00:11:04 +0000
1589+++ src/lazr/restful/tests/test_declarations.py 2020-02-19 16:09:23 +0000
1590@@ -4,6 +4,8 @@
1591
1592 from __future__ import absolute_import, print_function
1593
1594+import sys
1595+
1596 import testtools
1597 from zope.component import (
1598 adapter,
1599@@ -34,13 +36,17 @@
1600 from lazr.restful.declarations import (
1601 accessor_for,
1602 call_with,
1603+ collection_default_content,
1604 error_status,
1605+ export_as_webservice_collection,
1606 export_as_webservice_entry,
1607- exported,
1608 export_read_operation,
1609 export_write_operation,
1610+ exported,
1611+ exported_as_webservice_entry,
1612 generate_entry_interfaces,
1613 generate_operation_adapter,
1614+ LAZR_WEBSERVICE_EXPORTED,
1615 LAZR_WEBSERVICE_NAME,
1616 mutator_for,
1617 operation_for_version,
1618@@ -137,8 +143,8 @@
1619 # There can only be one accessor defined for a given attribute
1620 # at a given version.
1621 def declare_too_many_accessors():
1622+ @exported_as_webservice_entry(contributes_to=[IProduct])
1623 class IHasTooManyAccessors(Interface):
1624- export_as_webservice_entry(contributes_to=[IProduct])
1625 needs_an_accessor = exported(
1626 TextLine(title=u'This needs an accessor', readonly=True))
1627
1628@@ -241,8 +247,9 @@
1629 def test_redacted_fields_for_empty_entry(self):
1630 # An entry which doesn't export any fields/operations will have no
1631 # redacted fields.
1632+ @exported_as_webservice_entry()
1633 class IEmpty(Interface):
1634- export_as_webservice_entry()
1635+ pass
1636 @implementer(IEmpty)
1637 class Empty:
1638 pass
1639@@ -300,8 +307,8 @@
1640 # A contributing interface can only contribute to exported interfaces.
1641 class INotExported(Interface):
1642 pass
1643+ @exported_as_webservice_entry(contributes_to=[INotExported])
1644 class IContributor(Interface):
1645- export_as_webservice_entry(contributes_to=[INotExported])
1646 title = exported(TextLine(title=u'The project title'))
1647 self.assertRaises(
1648 AttemptToContributeToNonExportedInterface,
1649@@ -333,6 +340,150 @@
1650 'product', EntryAdapterUtility(adapter.__class__).singular_type)
1651
1652
1653+class TestExportAsWebserviceEntry(testtools.TestCase):
1654+ """Tests for export_as_webservice_entry."""
1655+
1656+ def setUp(self):
1657+ super(TestExportAsWebserviceEntry, self).setUp()
1658+ if sys.version_info[0] >= 3:
1659+ self.skipTest(
1660+ 'export_as_webservice_entry is only supported on Python 2')
1661+
1662+ def test_works_on_interface(self):
1663+ # export_as_webservice_entry works on an interface.
1664+ class IFoo(Interface):
1665+ export_as_webservice_entry()
1666+
1667+ self.assertTrue(
1668+ IFoo.queryTaggedValue(LAZR_WEBSERVICE_EXPORTED)['exported'])
1669+
1670+ def test_requires_interface(self):
1671+ # export_as_webservice_entry can only be used on Interface.
1672+ def export_non_interface():
1673+ class NotAnInterface(object):
1674+ export_as_webservice_entry()
1675+
1676+ exception = self.assertRaises(TypeError, export_non_interface)
1677+ self.assertEqual(
1678+ 'export_as_webservice_entry() can only be used on an interface.',
1679+ str(exception))
1680+
1681+ def test_must_be_within_interface_definition(self):
1682+ # export_as_webservice_entry can only be used from within a
1683+ # interface definition.
1684+ exception = self.assertRaises(TypeError, export_as_webservice_entry)
1685+ self.assertEqual(
1686+ 'export_as_webservice_entry() can only be used from within an '
1687+ 'interface definition.',
1688+ str(exception))
1689+
1690+
1691+class TestExportAsWebserviceCollection(testtools.TestCase):
1692+ """Tests for export_as_webservice_collection."""
1693+
1694+ def setUp(self):
1695+ super(TestExportAsWebserviceCollection, self).setUp()
1696+ if sys.version_info[0] >= 3:
1697+ self.skipTest(
1698+ 'export_as_webservice_collection is only supported on '
1699+ 'Python 2')
1700+
1701+ def test_works_on_interface_with_tagged_method(self):
1702+ # export_as_webservice_collection works on an interface that has an
1703+ # entry schema and a method tagged with @collection_default_content.
1704+ class IFoo(Interface):
1705+ export_as_webservice_collection(Interface)
1706+
1707+ @collection_default_content()
1708+ def getAll():
1709+ pass
1710+
1711+ self.assertEqual(
1712+ {None: ('getAll', {})},
1713+ IFoo.queryTaggedValue(LAZR_WEBSERVICE_EXPORTED)[
1714+ 'collection_default_content'])
1715+
1716+ def test_requires_entry_schema(self):
1717+ # export_as_webservice_collection requires an entry schema.
1718+ def export_without_entry_schema():
1719+ class MissingEntrySchema(Interface):
1720+ export_as_webservice_collection()
1721+
1722+ exception = self.assertRaises(TypeError, export_without_entry_schema)
1723+ self.assertEqual(
1724+ 'export_as_webservice_collection() takes exactly 1 argument (0 '
1725+ 'given)',
1726+ str(exception))
1727+
1728+ def test_requires_entry_schema_as_interface(self):
1729+ # export_as_webservice_collection requires the entry schema to be an
1730+ # interface.
1731+ def export_with_invalid_entry_schema():
1732+ class InvalidEntrySchema(Interface):
1733+ export_as_webservice_collection('not an interface')
1734+
1735+ exception = self.assertRaises(
1736+ TypeError, export_with_invalid_entry_schema)
1737+ self.assertEqual('entry_schema must be an interface.', str(exception))
1738+
1739+ def test_requires_tagged_method(self):
1740+ # export_as_webservice_collection can only be used on a collection
1741+ # that has a method marked as exporting the default content.
1742+ def export_missing_default_content():
1743+ class MissingDefaultContent(Interface):
1744+ export_as_webservice_collection(Interface)
1745+
1746+ exception = self.assertRaises(
1747+ TypeError, export_missing_default_content)
1748+ self.assertEqual(
1749+ 'export_as_webservice_collection() is missing a method tagged '
1750+ 'with @collection_default_content.',
1751+ str(exception))
1752+
1753+ def test_refuses_multiple_tagged_methods(self):
1754+ # export_as_webservice_collection cannot be used on a collection
1755+ # that has multiple methods marked as exporting the default content.
1756+ def export_two_default_content():
1757+ class TwoDefaultContent(Interface):
1758+ export_as_webservice_collection(Interface)
1759+
1760+ @collection_default_content()
1761+ def getAll1():
1762+ """A first getAll()."""
1763+
1764+ @collection_default_content()
1765+ def getAll2():
1766+ """Another getAll()."""
1767+
1768+ exception = self.assertRaises(TypeError, export_two_default_content)
1769+ self.assertEqual(
1770+ "Only one method can be marked with @collection_default_content "
1771+ "for version '(earliest version)'.",
1772+ str(exception))
1773+
1774+ def test_requires_interface(self):
1775+ # export_as_webservice_collection can only be used on Interface.
1776+ def export_non_interface():
1777+ class NotAnInterface(object):
1778+ export_as_webservice_collection(Interface)
1779+
1780+ exception = self.assertRaises(TypeError, export_non_interface)
1781+ self.assertEqual(
1782+ 'export_as_webservice_collection() can only be used on an '
1783+ 'interface.',
1784+ str(exception))
1785+
1786+ def test_must_be_within_interface_definition(self):
1787+ # export_as_webservice_collection can only be used from within an
1788+ # interface definition.
1789+ exception = self.assertRaises(
1790+ TypeError, export_as_webservice_collection, Interface)
1791+ self.assertEqual(
1792+ 'export_as_webservice_collection() can only be used from within '
1793+ 'an interface definition.',
1794+ str(exception))
1795+
1796+
1797 class NotExportedException(Exception):
1798 pass
1799
1800@@ -351,8 +502,9 @@
1801 pass
1802
1803
1804+@exported_as_webservice_entry()
1805 class IExported(Interface):
1806- export_as_webservice_entry()
1807+ pass
1808
1809
1810 class TestFindExportedInterfaces(testtools.TestCase):
1811@@ -394,8 +546,8 @@
1812 self.assertExportsNames(Module, ['ExportedExceptionOne'])
1813
1814
1815+@exported_as_webservice_entry()
1816 class IProduct(Interface):
1817- export_as_webservice_entry()
1818 title = exported(TextLine(title=u'The product title'))
1819 # Need to define the three attributes below because we have a test which
1820 # wraps a Product object with a security proxy and later uses adapters
1821@@ -415,8 +567,8 @@
1822 _bug_target_name = None
1823
1824
1825+@exported_as_webservice_entry()
1826 class IProject(Interface):
1827- export_as_webservice_entry()
1828 title = exported(TextLine(title=u'The project title'))
1829
1830
1831@@ -426,8 +578,8 @@
1832 _bug_count = 0
1833
1834
1835+@exported_as_webservice_entry(contributes_to=[IProduct, IProject])
1836 class IHasBugs(Interface):
1837- export_as_webservice_entry(contributes_to=[IProduct, IProject])
1838 bug_count = exported(Int(title=u'Number of bugs'))
1839 not_exported = TextLine(title=u'Not exported')
1840 bug_target_name = exported(
1841@@ -463,14 +615,14 @@
1842 pass
1843
1844
1845+@exported_as_webservice_entry(contributes_to=[IProduct])
1846 class IHasBugs2(Interface):
1847- export_as_webservice_entry(contributes_to=[IProduct])
1848 bug_count = exported(Int(title=u'Number of bugs'))
1849 not_exported = TextLine(title=u'Not exported')
1850
1851
1852+@exported_as_webservice_entry(contributes_to=[IProduct])
1853 class IHasBugs3(Interface):
1854- export_as_webservice_entry(contributes_to=[IProduct])
1855 not_exported = TextLine(title=u'Not exported')
1856
1857 @export_read_operation()
1858@@ -504,8 +656,8 @@
1859 pass
1860
1861
1862+@exported_as_webservice_entry()
1863 class IBranch(Interface):
1864- export_as_webservice_entry()
1865 name = TextLine(title=u'The branch name')
1866
1867
1868@@ -516,8 +668,8 @@
1869 self.name = name
1870
1871
1872+@exported_as_webservice_entry(contributes_to=[IProduct])
1873 class IHasBranches(Interface):
1874- export_as_webservice_entry(contributes_to=[IProduct])
1875 not_exported = TextLine(title=u'Not exported')
1876 development_branch = exported(
1877 Reference(schema=IBranch, readonly=True),
1878@@ -571,32 +723,36 @@
1879
1880 # Classes for TestEntryMultiversion.
1881
1882+@exported_as_webservice_entry(as_of="2.0")
1883 class INotInitiallyExported(Interface):
1884 # An entry that's not exported in the first version of the web
1885 # service.
1886- export_as_webservice_entry(as_of="2.0")
1887-
1888-
1889+ pass
1890+
1891+
1892+@exported_as_webservice_entry(
1893+ versioned_annotations=[('2.0', dict(exported=False))])
1894 class INotPresentInLaterVersion(Interface):
1895 # An entry that's only exported in the first version of the web
1896 # service.
1897- export_as_webservice_entry(
1898- versioned_annotations=[('2.0', dict(exported=False))])
1899-
1900-
1901+ pass
1902+
1903+
1904+@exported_as_webservice_entry(
1905+ singular_name="octopus", plural_name="octopi",
1906+ versioned_annotations=[
1907+ ('2.0', dict(singular_name="fish", plural_name="fishes"))])
1908 class IHasDifferentNamesInDifferentVersions(Interface):
1909 # An entry that has different names in different versions.
1910- export_as_webservice_entry(
1911- singular_name="octopus", plural_name="octopi",
1912- versioned_annotations=[
1913- ('2.0', dict(singular_name="fish", plural_name="fishes"))])
1914-
1915-
1916+ pass
1917+
1918+
1919+@exported_as_webservice_entry(
1920+ singular_name="frog",
1921+ versioned_annotations=[('2.0', dict(singular_name="toad"))])
1922 class IHasDifferentSingularNamesInDifferentVersions(Interface):
1923 # An entry that has different names in different versions.
1924- export_as_webservice_entry(
1925- singular_name="frog",
1926- versioned_annotations=[('2.0', dict(singular_name="toad"))])
1927+ pass
1928
1929
1930 class TestEntryMultiversion(TestCaseWithWebServiceFixtures):
1931@@ -640,10 +796,11 @@
1932 # You can't define an entry class that includes an unrecognized
1933 # annotation in its versioned_annotations.
1934 try:
1935+ @exported_as_webservice_entry(
1936+ versioned_annotations=[
1937+ ('2.0', dict(no_such_annotation=True))])
1938 class IUsesNonexistentAnnotation(Interface):
1939- export_as_webservice_entry(
1940- versioned_annotations=[
1941- ('2.0', dict(no_such_annotation=True))])
1942+ pass
1943 self.fail("Expected ValueError.")
1944 except ValueError as exception:
1945 self.assertEqual(
1946@@ -671,58 +828,55 @@
1947
1948 # Classes for TestReqireExplicitVersions
1949
1950+@exported_as_webservice_entry()
1951 class IEntryExportedWithoutAsOf(Interface):
1952- export_as_webservice_entry()
1953-
1954-
1955+ pass
1956+
1957+
1958+@exported_as_webservice_entry(as_of="1.0")
1959 class IEntryExportedWithAsOf(Interface):
1960- export_as_webservice_entry(as_of="1.0")
1961-
1962-
1963+ pass
1964+
1965+
1966+@exported_as_webservice_entry(as_of="2.0")
1967 class IEntryExportedAsOfLaterVersion(Interface):
1968- export_as_webservice_entry(as_of="2.0")
1969-
1970-
1971+ pass
1972+
1973+
1974+@exported_as_webservice_entry(as_of="1.0")
1975 class IFieldExportedWithoutAsOf(Interface):
1976- export_as_webservice_entry(as_of="1.0")
1977-
1978 field = exported(TextLine(), exported=True)
1979
1980
1981+@exported_as_webservice_entry(as_of="1.0")
1982 class IFieldExportedToEarliestVersionUsingAsOf(Interface):
1983- export_as_webservice_entry(as_of="1.0")
1984-
1985 field = exported(TextLine(), as_of='1.0')
1986
1987
1988+@exported_as_webservice_entry(as_of="1.0")
1989 class IFieldExportedToLatestVersionUsingAsOf(Interface):
1990- export_as_webservice_entry(as_of="1.0")
1991-
1992 field = exported(TextLine(), as_of='2.0')
1993
1994
1995+@exported_as_webservice_entry(as_of="1.0")
1996 class IFieldDefiningAttributesBeforeAsOf(Interface):
1997- export_as_webservice_entry(as_of="1.0")
1998-
1999 field = exported(TextLine(), ('1.0', dict(exported=True)),
2000 as_of='2.0')
2001
2002+
2003+@exported_as_webservice_entry(as_of="1.0")
2004 class IFieldAsOfNonexistentVersion(Interface):
2005- export_as_webservice_entry(as_of="1.0")
2006-
2007 field = exported(TextLine(), as_of='nosuchversion')
2008
2009
2010+@exported_as_webservice_entry(as_of="1.0")
2011 class IFieldDoubleDefinition(Interface):
2012- export_as_webservice_entry(as_of="1.0")
2013-
2014 field = exported(TextLine(), ('2.0', dict(exported_as='name2')),
2015 exported_as='name2', as_of='2.0')
2016
2017
2018+@exported_as_webservice_entry(as_of="1.0")
2019 class IFieldImplicitOperationDefinition(Interface):
2020- export_as_webservice_entry(as_of="1.0")
2021-
2022 @call_with(value="2.0")
2023 @operation_for_version('2.0')
2024 @call_with(value="1.0")
2025@@ -731,9 +885,8 @@
2026 pass
2027
2028
2029+@exported_as_webservice_entry(as_of="1.0")
2030 class IFieldExplicitOperationDefinition(Interface):
2031- export_as_webservice_entry(as_of="1.0")
2032-
2033 @call_with(value="2.0")
2034 @operation_for_version('2.0')
2035 @call_with(value="1.0")
2036@@ -922,47 +1075,43 @@
2037 pass
2038
2039
2040+@exported_as_webservice_entry()
2041 class IReferencesNotPublished(Interface):
2042- export_as_webservice_entry()
2043 field = exported(Reference(schema=INotPublished))
2044
2045
2046+@exported_as_webservice_entry()
2047 class IReferencesCollectionOfNotPublished(Interface):
2048- export_as_webservice_entry()
2049 field = exported(
2050 CollectionField(value_type=Reference(schema=INotPublished)))
2051
2052
2053+@exported_as_webservice_entry()
2054 class IOperationReturnsNotPublished(Interface):
2055- export_as_webservice_entry()
2056-
2057 @operation_returns_entry(INotPublished)
2058 @export_read_operation()
2059 def get_impossible_object():
2060 pass
2061
2062
2063+@exported_as_webservice_entry()
2064 class IOperationReturnsNotPublishedCollection(Interface):
2065- export_as_webservice_entry()
2066-
2067 @operation_returns_collection_of(INotPublished)
2068 @export_read_operation()
2069 def get_impossible_objects():
2070 pass
2071
2072
2073+@exported_as_webservice_entry()
2074 class IOperationAcceptsNotPublished(Interface):
2075- export_as_webservice_entry()
2076-
2077 @operation_parameters(arg=Reference(schema=INotPublished))
2078 @export_write_operation()
2079 def use_impossible_object(arg):
2080 pass
2081
2082
2083+@exported_as_webservice_entry()
2084 class IOperationAcceptsCollectionOfNotPublished(Interface):
2085- export_as_webservice_entry()
2086-
2087 @operation_parameters(
2088 arg=CollectionField(value_type=Reference(schema=INotPublished)))
2089 @export_write_operation()
2090@@ -970,31 +1119,35 @@
2091 pass
2092
2093
2094+@exported_as_webservice_entry(as_of='2.0')
2095 class IPublishedTooLate(Interface):
2096- export_as_webservice_entry(as_of='2.0')
2097-
2098-
2099+ pass
2100+
2101+
2102+@exported_as_webservice_entry(as_of='1.0')
2103 class IReferencesPublishedTooLate(Interface):
2104- export_as_webservice_entry(as_of='1.0')
2105 field = exported(Reference(schema=IPublishedTooLate))
2106
2107
2108+@exported_as_webservice_entry(as_of='1.0')
2109 class IPublishedEarly(Interface):
2110- export_as_webservice_entry(as_of='1.0')
2111-
2112-
2113+ pass
2114+
2115+
2116+@exported_as_webservice_entry(as_of='2.0')
2117 class IPublishedLate(Interface):
2118- export_as_webservice_entry(as_of='2.0')
2119 field = exported(Reference(schema=IPublishedEarly))
2120
2121
2122+@exported_as_webservice_entry(
2123+ as_of='1.0', versioned_annotations=[
2124+ VersionedObject('2.0', dict(exported=False))])
2125 class IPublishedAndThenRemoved(Interface):
2126- export_as_webservice_entry(
2127- as_of='1.0', versioned_annotations=[
2128- VersionedObject('2.0', dict(exported=False))])
2129-
2130+ pass
2131+
2132+
2133+@exported_as_webservice_entry(as_of='1.0')
2134 class IReferencesPublishedAndThenRemoved(Interface):
2135- export_as_webservice_entry(as_of='1.0')
2136 field = exported(Reference(schema=IPublishedAndThenRemoved))
2137
2138
2139
2140=== modified file 'src/lazr/restful/tests/test_webservice.py'
2141--- src/lazr/restful/tests/test_webservice.py 2020-02-13 00:11:04 +0000
2142+++ src/lazr/restful/tests/test_webservice.py 2020-02-19 16:09:23 +0000
2143@@ -58,7 +58,10 @@
2144 ResourceGETOperation,
2145 )
2146 from lazr.restful.declarations import (
2147- exported, export_as_webservice_entry, LAZR_WEBSERVICE_NAME)
2148+ exported,
2149+ exported_as_webservice_entry,
2150+ LAZR_WEBSERVICE_NAME,
2151+ )
2152 from lazr.restful.testing.webservice import (
2153 create_web_service_request,
2154 DummyAbsoluteURL,
2155@@ -227,9 +230,9 @@
2156 IAbsoluteURL)
2157
2158
2159+@exported_as_webservice_entry()
2160 class IHasOneField(Interface):
2161 """An entry with a single field."""
2162- export_as_webservice_entry()
2163 a_field = exported(TextLine(title=u"A field."))
2164
2165
2166@@ -240,9 +243,9 @@
2167 self.a_field = value
2168
2169
2170+@exported_as_webservice_entry()
2171 class IHasTwoFields(Interface):
2172 """An entry with two fields."""
2173- export_as_webservice_entry()
2174 a_field = exported(TextLine(title=u"A field."))
2175 another_field = exported(TextLine(title=u"Another field."))
2176
2177@@ -317,9 +320,9 @@
2178 self.assertFalse('web_link' in self.wadl)
2179
2180
2181+@exported_as_webservice_entry(publish_web_link=False)
2182 class IHasNoWebLink(Interface):
2183 """An entry that does not publish a web_link."""
2184- export_as_webservice_entry(publish_web_link=False)
2185 a_field = exported(TextLine(title=u"A field."))
2186
2187
2188@@ -361,9 +364,9 @@
2189 return super(InterfaceRestrictedField, self).bind(context)
2190
2191
2192+@exported_as_webservice_entry()
2193 class IHasRestrictedField(Interface):
2194 """An entry with an InterfaceRestrictedField."""
2195- export_as_webservice_entry()
2196 a_field = exported(InterfaceRestrictedField(Interface))
2197
2198
2199@@ -374,9 +377,9 @@
2200 self.a_field = value
2201
2202
2203+@exported_as_webservice_entry()
2204 class IHasFieldExportedAsDifferentName(Interface):
2205 """An entry with a field exported as a different name."""
2206- export_as_webservice_entry()
2207 a_field = exported(TextLine(title=u"A field."), exported_as='field')
2208
2209
2210@@ -616,9 +619,9 @@
2211 UNICODE = Item(u"Uni\u00e7ode", "Uni\u00e7ode choice")
2212
2213
2214+@exported_as_webservice_entry()
2215 class ICanBeSetToUnicodeValue(Interface):
2216 """An entry with an InterfaceRestrictedField."""
2217- export_as_webservice_entry()
2218 a_field = exported(Choice(
2219 vocabulary=UnicodeChoice,
2220 title=u"A value that might be ASCII or Unicode.",
2221@@ -662,9 +665,8 @@
2222 """Test the docstring generation."""
2223
2224 # This one is used to test when docstrings are missing.
2225+ @exported_as_webservice_entry()
2226 class IUndocumentedEntry(Interface):
2227- export_as_webservice_entry()
2228-
2229 a_field = exported(TextLine())
2230
2231 testmodule_objects = [
2232@@ -768,8 +770,9 @@
2233 def make_entry(name):
2234 """Make an entity with some attibutes to expose as a web service."""
2235 code = """
2236+ @exported_as_webservice_entry(singular_name='%(name)s')
2237 class %(name)s(Interface):
2238- export_as_webservice_entry(singular_name='%(name)s')
2239+ pass
2240 """ % locals()
2241
2242 for letter in 'rstuvwxyz':
2243@@ -816,9 +819,9 @@
2244 class DuplicateSingularNameTestCase(DuplicateNameTestCase):
2245 """Test AssertionError when resource types share a singular name."""
2246
2247+ @exported_as_webservice_entry('generic_entry')
2248 class IDuplicate(Interface):
2249 """An entry that reuses the singular name of IGenericEntry."""
2250- export_as_webservice_entry('generic_entry')
2251
2252 testmodule_objects = [IGenericEntry, IDuplicate]
2253
2254@@ -830,9 +833,9 @@
2255 class DuplicatePluralNameTestCase(DuplicateNameTestCase):
2256 """Test AssertionERror when resource types share a plural name."""
2257
2258+ @exported_as_webservice_entry(plural_name='generic_entrys')
2259 class IDuplicate(Interface):
2260 """An entry that reuses the plural name of IGenericEntry."""
2261- export_as_webservice_entry(plural_name='generic_entrys')
2262
2263 testmodule_objects = [IGenericEntry, IDuplicate]
2264
2265@@ -890,9 +893,9 @@
2266 endInteraction()
2267
2268
2269+@exported_as_webservice_entry()
2270 class ITestEntry(IEntry):
2271 """Interface for a test entry."""
2272- export_as_webservice_entry()
2273
2274
2275 @implementer(ITestEntry)

Subscribers

People subscribed via source and target branches