Merge lp:~leonardr/lazr.restful/double-your-enjoyment into lp:lazr.restful

Proposed by Leonard Richardson
Status: Merged
Approved by: Edwin Grubbs
Approved revision: not available
Merged at revision: not available
Proposed branch: lp:~leonardr/lazr.restful/double-your-enjoyment
Merge into: lp:lazr.restful
Diff against target: 539 lines (+330/-55)
2 files modified
src/lazr/restful/docs/multiversion.txt (+292/-0)
src/lazr/restful/docs/webservice.txt (+38/-55)
To merge this branch: bzr merge lp:~leonardr/lazr.restful/double-your-enjoyment
Reviewer Review Type Date Requested Status
Edwin Grubbs (community) code Approve
Review via email: mp+14967@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Leonard Richardson (leonardr) wrote :

This branch creates a new doctest, multiversion.txt, which defines a web service that serves three distinct services ("beta", "1.0", and "dev") from a single underlying data model. Currently there is no code that uses this data model with lazr.restful, which is good because as soon as I add that code lazr.restful is going to explode. I'm submitting this branch as a natural stopping point before I have to start tearing up lazr.restful and putting it back together.

I'm using webservice.txt as a model for multiversion.txt. This is a little tricky because webservice.txt is a very old service that reimplements many of the niceties found in lazr.restful.simple. So I'm trying to improve webservice.txt as I copy it. For instance, I removed the Traversable class from webservice.txt and replaced it with the standard TraverseWithGet class. I also got rid of an XXX that said to make a certain change once we upgraded to zope 3.4.

I also tried to get rid of webservice.txt's BaseAbsoluteURL class, but it turned out to be way too much work. I'll just use ILocation in the new service and change webservice.txt later.

97. By Leonard Richardson

Fixed the prose.

Revision history for this message
Edwin Grubbs (edwin-grubbs) wrote :

Hi Leonard,

This branch looks good. I have some very minor comments below.

merge-approved

-Edwin

>=== added file 'src/lazr/restful/docs/multiversion.txt'
>+ >>> from lazr.restful.simple import TraverseWithGet
>+ >>> from zope.publisher.interfaces.browser import IBrowserRequest
>+ >>> class ContactSet(TraverseWithGet):
>+ ... implements(IContactSet)
>+ ... path = "contact"

Please fix the path variable as discussed on IRC.

>+ ...
>+ ... def __init__(self):
>+ ... self.contacts = CONTACTS
>+ ...
>+ ... def get(self, name):
>+ ... contacts = [contact for contacts in self.contacts
>+ ... if pair.name == name]
>+ ... if len(contacts) == 1:
>+ ... return contacts[0]
>+ ... return None
>+ >>> sm.registerAdapter(TraverseWithGet, [ITestDataObject, IBrowserRequest])

Line too long.

>=== modified file 'src/lazr/restful/docs/webservice.txt'
>@@ -417,16 +406,15 @@
> ... def getAllAuthors(self):
> ... return self.authors
> ...
>- ... def get(self, name):
>+ ... def get(self, request, name):
> ... match = [p for p in self.authors if p.name == name]
> ... if len(match) > 0:
> ... return match[0]
> ... return None
>
>+ >>> sm.registerAdapter(TraverseWithGet, [ITestDataObject, IBrowserRequest])

Line too long.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'src/lazr/restful/docs/multiversion.txt'
2--- src/lazr/restful/docs/multiversion.txt 1970-01-01 00:00:00 +0000
3+++ src/lazr/restful/docs/multiversion.txt 2009-11-18 16:46:09 +0000
4@@ -0,0 +1,292 @@
5+Publishing multiple versions of a web service
6+*********************************************
7+
8+A single data model can yield many different lazr.restful web
9+services. Typically these different services represent successive
10+versions of a single web service, improved over time.
11+
12+Example model objects
13+=====================
14+
15+First let's define the data model. The model in webservice.txt is
16+pretty complicated; this model will be just complicated enough to
17+illustrate how to publish multiple versions of a web service.
18+
19+ # All classes defined in this test are new-style classes.
20+ >>> __metaclass__ = type
21+
22+ >>> from zope.interface import Interface, Attribute
23+ >>> from zope.schema import Bool, Bytes, Int, Text, TextLine, Object
24+
25+ >>> class ITestDataObject(Interface):
26+ ... """A marker interface for data objects."""
27+ ... path = Attribute("The path portion of this object's URL. "
28+ ... "Defined here for simplicity of testing.")
29+
30+ >>> class IContact(ITestDataObject):
31+ ... name = TextLine(title=u"Name", required=True)
32+ ... phone = TextLine(title=u"Phone number", required=True)
33+ ... fax = TextLine(title=u"Fax number", required=False)
34+
35+Here's the interface for the 'set' object that manages the contacts.
36+
37+ >>> from lazr.restful.interfaces import ITraverseWithGet
38+ >>> class IContactSet(ITestDataObject, ITraverseWithGet):
39+ ... def getAll(self):
40+ ... "Get all contacts."
41+ ...
42+ ... def get(self, name):
43+ ... "Retrieve a single contact by name."
44+
45+Before we can define any classes, a bit of web service setup. Let's
46+make all component lookups use the global site manager.
47+
48+ >>> from zope.component import getSiteManager
49+ >>> sm = getSiteManager()
50+
51+ >>> from zope.component import adapter
52+ >>> from zope.component.interfaces import IComponentLookup
53+ >>> from zope.interface import implementer, Interface
54+ >>> @implementer(IComponentLookup)
55+ ... @adapter(Interface)
56+ ... def everything_uses_the_global_site_manager(context):
57+ ... return sm
58+ >>> sm.registerAdapter(everything_uses_the_global_site_manager)
59+
60+Here's a simple implementation of IContact.
61+
62+ >>> from urllib import quote
63+ >>> from zope.interface import implements
64+ >>> from lazr.restful.security import protect_schema
65+ >>> class Contact:
66+ ... implements(IContact)
67+ ... def __init__(self, name, phone, fax):
68+ ... self.name = name
69+ ... self.phone = phone
70+ ... self.fax = fax
71+ ...
72+ ... @property
73+ ... def path(self):
74+ ... return 'contacts/' + quote(self.name)
75+ >>> protect_schema(Contact, IContact)
76+
77+Here's a simple ContactSet with a predefined list of contacts.
78+
79+ >>> from lazr.restful.simple import TraverseWithGet
80+ >>> from zope.publisher.interfaces.browser import IBrowserRequest
81+ >>> class ContactSet(TraverseWithGet):
82+ ... implements(IContactSet)
83+ ... path = "contact"
84+ ...
85+ ... def __init__(self):
86+ ... self.contacts = CONTACTS
87+ ...
88+ ... def get(self, name):
89+ ... contacts = [contact for contacts in self.contacts
90+ ... if pair.name == name]
91+ ... if len(contacts) == 1:
92+ ... return contacts[0]
93+ ... return None
94+ >>> sm.registerAdapter(TraverseWithGet, [ITestDataObject, IBrowserRequest])
95+
96+Here are the "model objects" themselves:
97+
98+ >>> C1 = Contact("Cleo Python", "555-1212", "111-2121")
99+ >>> C2 = Contact("Oliver Bluth", "10-1000000", "22-2222222")
100+ >>> CONTACTS = [C1, C2]
101+
102+Web service infrastructure initialization
103+=========================================
104+
105+The lazr.restful package contains a set of default adapters and
106+definitions to implement the web service.
107+
108+ >>> from zope.configuration import xmlconfig
109+ >>> zcmlcontext = xmlconfig.string("""
110+ ... <configure xmlns="http://namespaces.zope.org/zope">
111+ ... <include package="lazr.restful" file="basic-site.zcml"/>
112+ ... <utility
113+ ... factory="lazr.restful.example.base.filemanager.FileManager" />
114+ ... </configure>
115+ ... """)
116+
117+A IWebServiceConfiguration utility is also expected to be defined which
118+defines common configuration option for the webservice.
119+
120+ >>> from lazr.restful import directives
121+ >>> from lazr.restful.interfaces import IWebServiceConfiguration
122+ >>> from lazr.restful.simple import BaseWebServiceConfiguration
123+ >>> from lazr.restful.testing.webservice import WebServiceTestPublication
124+
125+ >>> class WebServiceConfiguration(BaseWebServiceConfiguration):
126+ ... use_https = False
127+ ... active_versions = ['beta', '1.0']
128+ ... latest_version_uri_prefix = 'dev'
129+ ... code_revision = 'test'
130+ ... max_batch_size = 100
131+ ... directives.publication_class(WebServiceTestPublication)
132+
133+ >>> from grokcore.component.testing import grok_component
134+ >>> ignore = grok_component(
135+ ... 'WebServiceConfiguration', WebServiceConfiguration)
136+
137+ >>> from zope.component import getUtility
138+ >>> webservice_configuration = getUtility(IWebServiceConfiguration)
139+
140+Defining the web service data model
141+===================================
142+
143+We've defined an underlying data model (IContact), and now we're going
144+to define the evolution of a web service through three versions, by
145+defining three derivative data models. In a real application, these
146+IEntry subclasses would be generated from lazr.restful decorators
147+present in IContact but for testing purposes we're going to just
148+define the three IEntry subclasses manually.
149+
150+The "beta" version of the web service publishes the IContact interface
151+exactly as it is defined.
152+
153+ >>> from lazr.restful.interfaces import IEntry
154+ >>> class IContactEntry(IEntry):
155+ ... """Marker for a contact published through the web service."""
156+
157+ >>> from zope.interface import taggedValue
158+ >>> from lazr.restful.interfaces import IEntry, LAZR_WEBSERVICE_NAME
159+ >>> class IContactEntryBeta(IContactEntry, IContact):
160+ ... """The part of an author we expose through the web service."""
161+ ... taggedValue(LAZR_WEBSERVICE_NAME,
162+ ... dict(singular="contact", plural="contacts"))
163+
164+The "1.0" version publishes the IContact interface as is, but renames
165+two of the fields.
166+
167+ >>> class IContactEntry10(IContactEntry):
168+ ... name = TextLine(title=u"Name", required=True)
169+ ... phone_number = TextLine(title=u"Phone number", required=True)
170+ ... fax_number = TextLine(title=u"Fax number", required=False)
171+ ... taggedValue(LAZR_WEBSERVICE_NAME,
172+ ... dict(singular="contact", plural="contacts"))
173+
174+The "dev" version drops the "fax_number" field because fax machines
175+are obsolete.
176+
177+ >>> class IContactEntryDev(IContactEntry):
178+ ... name = TextLine(title=u"Name", required=True)
179+ ... phone_number = TextLine(title=u"Phone number", required=True)
180+ ... taggedValue(LAZR_WEBSERVICE_NAME,
181+ ... dict(singular="contact", plural="contacts"))
182+
183+Implementing the resources
184+==========================
185+
186+The Contact class defined above implements the IContact interface, but
187+IContact just describes the data model, not any particular web
188+service. The IContactEntry subclasses above -- IContactEntryBeta,
189+IContactEntry10, IContactEntryDev -- describe the three versions of
190+the web service. Each of these interfaces must be implemented by a
191+subclass of Entry.
192+
193+In a real application, the Entry subclasses would be generated from
194+lazr.restful decorators present in IContact (just like the IEntry
195+subclasses), but for testing purposes we're going to define the three
196+Entry subclasses manually.
197+
198+ >>> from zope.component import adapts
199+ >>> from zope.interface import implements
200+ >>> from lazr.delegates import delegates
201+ >>> from lazr.restful import Entry
202+
203+ >>> class ContactEntryBeta(Entry):
204+ ... """A contact, as exposed through the 'beta' web service."""
205+ ... adapts(IContact)
206+ ... implements(IContactEntryBeta)
207+ ... delegates(IContactEntryBeta)
208+ ... schema = IContactEntryBeta
209+ >>> sm.registerAdapter(
210+ ... ContactEntryBeta, provided=IContactEntry, name="beta")
211+
212+By wrapping one of our predefined Contacts in a ContactEntryBeta
213+object, we can verify that it implements IContactEntryBeta and
214+IContactEntry.
215+
216+ >>> entry = ContactEntryBeta(C1)
217+ >>> IContactEntry.validateInvariants(entry)
218+ >>> IContactEntryBeta.validateInvariants(entry)
219+
220+Here's the implemenation of IContactEntry10, which defines Python
221+properties to implement the different field names.
222+
223+ >>> class ContactEntry10(Entry):
224+ ... adapts(IContact)
225+ ... implements(IContactEntry10)
226+ ... schema = IContactEntry10
227+ ...
228+ ... def __init__(self, contact):
229+ ... self.contact = contact
230+ ...
231+ ... @property
232+ ... def phone_number(self):
233+ ... return self.contact.phone
234+ ...
235+ ... @property
236+ ... def fax_number(self):
237+ ... return self.contact.fax
238+ >>> sm.registerAdapter(
239+ ... ContactEntry10, provided=IContactEntry, name="1.0")
240+
241+ >>> entry = ContactEntry10(C1)
242+ >>> IContactEntry.validateInvariants(entry)
243+ >>> IContactEntry10.validateInvariants(entry)
244+
245+Finally, here's the implementation of IContactEntry for the "dev" version of
246+the web service.
247+
248+ >>> class ContactEntryDev(Entry):
249+ ... adapts(IContact)
250+ ... implements(IContactEntryDev)
251+ ... schema = IContactEntryDev
252+ ...
253+ ... def __init__(self, contact):
254+ ... self.contact = contact
255+ ...
256+ ... @property
257+ ... def phone_number(self):
258+ ... return self.contact.phone
259+ >>> sm.registerAdapter(
260+ ... ContactEntryDev, provided=IContactEntry, name="dev")
261+
262+ >>> entry = ContactEntryDev(C1)
263+ >>> IContactEntry.validateInvariants(entry)
264+ >>> IContactEntryDev.validateInvariants(entry)
265+
266+Looking up the appropriate implementation
267+=========================================
268+
269+Because there is no single IEntry implementation for Contact objects,
270+you can't just adapt Contact to IEntry.
271+
272+ >>> from zope.component import getAdapter
273+
274+ >>> getAdapter(C1, IEntry)
275+ Traceback (most recent call last):
276+ ...
277+ ComponentLookupError: ...
278+
279+When adapting Contact to IEntry you must specify a version number as
280+the name of the adapter. The object you get back will implement the
281+appropriate version of the web service.
282+
283+ >>> beta_entry = getAdapter(C1, IEntry, name="beta")
284+ >>> print beta_entry.fax
285+ 111-2121
286+
287+ >>> one_oh_entry = getAdapter(C1, IEntry, name="1.0")
288+ >>> print one_oh_entry.fax_number
289+ 111-2121
290+
291+ >>> dev_entry = getAdapter(C1, IEntry, name="dev")
292+ >>> print dev_entry.fax
293+ Traceback (most recent call last):
294+ ...
295+ AttributeError: 'ContactEntryDev' object has no attribute 'fax'
296+
297
298=== modified file 'src/lazr/restful/docs/webservice.txt'
299--- src/lazr/restful/docs/webservice.txt 2009-11-12 19:08:10 +0000
300+++ src/lazr/restful/docs/webservice.txt 2009-11-18 16:46:09 +0000
301@@ -74,28 +74,32 @@
302 Here's the interface for the 'set' objects that manage the authors,
303 cookbooks, and dishes. The inconsistent naming is intentional.
304
305- >>> class IAuthorSet(ITestDataObject):
306+ >>> from lazr.restful.interfaces import ITraverseWithGet
307+ >>> class ITestDataSet(ITestDataObject, ITraverseWithGet):
308+ ... """A marker interface."""
309+
310+ >>> class IAuthorSet(ITestDataSet):
311 ... def getAllAuthors(self):
312 ... "Get all authors."
313 ...
314- ... def get(self, name):
315+ ... def get(self, request, name):
316 ... "Retrieve a single author by name."
317
318- >>> class ICookbookSet(ITestDataObject):
319+ >>> class ICookbookSet(ITestDataSet):
320 ... def getAll(self):
321 ... "Get all cookbooks."
322 ...
323- ... def get(self, name):
324+ ... def get(self, request, name):
325 ... "Retrieve a single cookbook by name."
326 ...
327 ... def findRecipes(self, name):
328 ... "Find recipes with a given name, across cookbooks."
329
330- >>> class IDishSet(ITestDataObject):
331+ >>> class IDishSet(ITestDataSet):
332 ... def getAll(self):
333 ... "Get all dishes."
334 ...
335- ... def get(self, name):
336+ ... def get(self, request, name):
337 ... "Retrieve a single dish by name."
338
339
340@@ -143,23 +147,6 @@
341 ... BaseAbsoluteURL, [ITestDataObject, IBrowserRequest],
342 ... IAbsoluteURL)
343
344- >>> from urllib import unquote
345- >>> class Traversable:
346- ... """A default IPublishTraverse that uses the get() method."""
347- ... implements(IPublishTraverse)
348- ... adapts(ITestDataObject, IBrowserRequest)
349- ...
350- ... def __init__(self, context, request):
351- ... self.context = context
352- ...
353- ... def publishTraverse(self, request, name):
354- ... name = unquote(name)
355- ... value = self.context.get(name)
356- ... if value is None:
357- ... raise NotFound(self, name)
358- ... return value
359- >>> sm.registerAdapter(Traversable)
360-
361 >>> class Author:
362 ... implements(IAuthor)
363 ... def __init__(self, name):
364@@ -219,6 +206,7 @@
365
366 >>> protect_schema(Cookbook, ICookbook, write_permission=CheckerPublic)
367
368+ >>> from urllib import unquote
369 >>> class CookbookTraversal:
370 ... implements(IPublishTraverse)
371 ... adapts(ICookbook, IBrowserRequest)
372@@ -358,7 +346,8 @@
373
374 Here's a simple CookbookSet with a predefined list of cookbooks.
375
376- >>> class CookbookSet(BaseAbsoluteURL):
377+ >>> from lazr.restful.simple import TraverseWithGet
378+ >>> class CookbookSet(BaseAbsoluteURL, TraverseWithGet):
379 ... implements(ICookbookSet)
380 ... path = 'cookbooks'
381 ...
382@@ -367,7 +356,7 @@
383 ...
384 ... def newCookbook(self, author_name, title, cuisine):
385 ... authors = AuthorSet()
386- ... author = authors.get(author_name)
387+ ... author = authors.get(None, author_name)
388 ... if author is None:
389 ... author = authors.newAuthor(author_name)
390 ... cookbook = Cookbook(title, author, cuisine)
391@@ -377,7 +366,7 @@
392 ... def getAll(self):
393 ... return self.cookbooks
394 ...
395- ... def get(self, name):
396+ ... def get(self, request, name):
397 ... match = [c for c in self.cookbooks if c.name == name]
398 ... if len(match) > 0:
399 ... return match[0]
400@@ -402,7 +391,7 @@
401
402 Here's a simple AuthorSet with predefined authors.
403
404- >>> class AuthorSet(BaseAbsoluteURL):
405+ >>> class AuthorSet(BaseAbsoluteURL, TraverseWithGet):
406 ... implements(IAuthorSet)
407 ... path = 'authors'
408 ...
409@@ -417,16 +406,15 @@
410 ... def getAllAuthors(self):
411 ... return self.authors
412 ...
413- ... def get(self, name):
414+ ... def get(self, request, name):
415 ... match = [p for p in self.authors if p.name == name]
416 ... if len(match) > 0:
417 ... return match[0]
418 ... return None
419
420+ >>> sm.registerAdapter(TraverseWithGet, [ITestDataObject, IBrowserRequest])
421 >>> protect_schema(AuthorSet, IAuthorSet)
422 >>> sm.registerUtility(AuthorSet(), IAuthorSet)
423- >>> sm.registerAdapter(
424- ... Traversable, [IAuthorSet, IBrowserRequest])
425
426 Here's a vocabulary of authors, for a field that presents a Choice
427 among authors.
428@@ -448,7 +436,7 @@
429
430 Finally, a simple DishSet with predefined dishes.
431
432- >>> class DishSet(BaseAbsoluteURL):
433+ >>> class DishSet(BaseAbsoluteURL, TraverseWithGet):
434 ... implements(IDishSet)
435 ... path = 'dishes'
436 ... def __init__(self):
437@@ -457,7 +445,7 @@
438 ... def getAll(self):
439 ... return self.dishes
440 ...
441- ... def get(self, name):
442+ ... def get(self, request, name):
443 ... match = [d for d in self.dishes if d.name == name]
444 ... if len(match) > 0:
445 ... return match[0]
446@@ -546,27 +534,20 @@
447 interface and ``IEntry``. Since ``IAuthor`` and ``IComment`` are so simple, we
448 can define ``IAuthorEntry`` and ``ICommentEntry`` very simply.
449
450- >>> from lazr.restful.interfaces import IEntry
451+The only extra and unusual step we have to take is to annotate the interfaces
452+with human-readable names for the objects we're exposing.
453+
454+ >>> from zope.interface import taggedValue
455+ >>> from lazr.restful.interfaces import IEntry, LAZR_WEBSERVICE_NAME
456 >>> class IAuthorEntry(IAuthor, IEntry):
457 ... """The part of an author we expose through the web service."""
458+ ... taggedValue(LAZR_WEBSERVICE_NAME, dict(singular="author",
459+ ... plural="authors"))
460
461 >>> class ICommentEntry(IComment, IEntry):
462 ... """The part of a comment we expose through the web service."""
463-
464-The only extra and unusual step we have to take is to annotate the interfaces
465-with human-readable names for the objects we're exposing.
466-
467- >>> from lazr.restful.interfaces import LAZR_WEBSERVICE_NAME
468-
469- # XXX leonardr 2008-07-09 bug=145746
470- # Once we're using Zope 3.4 we can call
471- # zope.interface.taggedValue(LAZR_WEBSERVICE_NAME, dict(...))
472- # from within the interface declaration.
473- >>> def tag_with_names(interface, singular, plural):
474- ... interface.setTaggedValue(
475- ... LAZR_WEBSERVICE_NAME, dict(singular=singular, plural=plural))
476- >>> tag_with_names(IAuthorEntry, 'author', 'authors')
477- >>> tag_with_names(ICommentEntry, 'comment', 'comments')
478+ ... taggedValue(LAZR_WEBSERVICE_NAME,
479+ ... dict(singular="comment", plural="comments"))
480
481 Most of the time, it doesn't work to expose to the web service the same data
482 model we expose internally. Usually there are fields we don't want to expose,
483@@ -586,7 +567,8 @@
484 >>> class IDishEntry(IEntry):
485 ... "The part of a dish that we expose through the web service."
486 ... recipes = CollectionField(value_type=Object(schema=IRecipe))
487- >>> tag_with_names(IDishEntry, 'dish', 'dishes')
488+ ... taggedValue(LAZR_WEBSERVICE_NAME,
489+ ... dict(singular="dish", plural="dishes"))
490
491 In the following code block we define an interface that exposes the underlying
492 ``Recipe``'s name but not its ID. References to associated objects (like the
493@@ -599,7 +581,8 @@
494 ... dish = Object(schema=IDish)
495 ... instructions = Text(title=u"Name", required=True)
496 ... comments = CollectionField(value_type=Object(schema=IComment))
497- >>> tag_with_names(IRecipeEntry, 'recipe', 'recipes')
498+ ... taggedValue(LAZR_WEBSERVICE_NAME,
499+ ... dict(singular="recipe", plural="recipes"))
500
501 >>> from lazr.restful.fields import ReferenceChoice
502 >>> class ICookbookEntry(IEntry):
503@@ -610,7 +593,8 @@
504 ... recipes = CollectionField(value_type=Object(schema=IRecipe))
505 ... comments = CollectionField(value_type=Object(schema=IComment))
506 ... cover = Bytes(0, 5000, title=u"An image of the cookbook's cover.")
507- >>> tag_with_names(ICookbookEntry, 'cookbook', 'cookbooks')
508+ ... taggedValue(LAZR_WEBSERVICE_NAME,
509+ ... dict(singular="cookbook", plural="cookbooks"))
510
511 The ``author`` field is a choice between ``Author`` objects. To make sure
512 that the ``Author`` objects are properly marshalled to JSON, we need to
513@@ -947,7 +931,7 @@
514 >>> from lazr.restful import ServiceRootResource
515 >>> from zope.traversing.browser.interfaces import IAbsoluteURL
516
517- >>> class MyServiceRootResource(ServiceRootResource):
518+ >>> class MyServiceRootResource(ServiceRootResource, TraverseWithGet):
519 ... implements(IAbsoluteURL)
520 ... path = ''
521 ...
522@@ -956,7 +940,7 @@
523 ... 'cookbooks': CookbookSet(),
524 ... 'authors': AuthorSet()}
525 ...
526- ... def get(self, name):
527+ ... def get(self, request, name):
528 ... return self.top_level_names.get(name)
529
530 It's the responsibility of each web service to provide an implementation of
531@@ -964,8 +948,7 @@
532
533 >>> sm.registerAdapter(
534 ... BaseAbsoluteURL, [MyServiceRootResource, IBrowserRequest])
535- >>> sm.registerAdapter(
536- ... Traversable, [IServiceRootResource, IBrowserRequest])
537+
538 >>> app = MyServiceRootResource()
539 >>> sm.registerUtility(app, IServiceRootResource)
540

Subscribers

People subscribed via source and target branches