|
|
8 User(s) Active on Site 233 Wiki Pages Most Recently Modified
Club Resources (edit)
How This Wiki Works
Check them out; they are a great source of technical books at very good prices! If you have shopped at Nerdbooks.com, help them out by reviewing them at ResellerRatings.com. You will need your invoice number to prove you are a real customer, and not just ballot stuffing. |
|||
| Recent Changes Printable View Page History Edit Page | |||
|
Content Last Modified on October 25, 2005, at 01:08 AM CST
Developing a Zope 3 Package of Your Own(Based on the webpage Zope3 in 30 Minutes with credit to Baiju M) Developing for Zope 3 means writing components in Python, collected into a package. It is much more programmer-oriented than Zope 2. The steps toward creating such a package are:
You'll often hear talk of Zope "web components" but not all components you'll create are web-specific. In fact, most of them are non-web data objects that get combined with other adapter components that display them on the web. And from that it should become clear that developers don't create just "one" component in isolation but that a Zope add-on "package" is a set of related components. In the example to follow, there is a component for representing web bookmarks, a component for a special folder to hold them and another component to provide a tabular view of that folder, delivered via HTML and through the web. Where do I put my new package?In building something, it's always good to know where you stand, where to begin storing your work. Zope packages are simply Python packages, typically a directory located somewhere along the Python import path (PYTHONPATH). While placing your package on that path means Python can import it, Zope 3 uses no magical package detection facility and won't know about it until a special descriptor file (<package>-configure.zcml) is dropped into the "etc/package-includes/" directory of a particular instance of Zope. Typically Zope packages are placed underneath a "Zope 3 package root", of which there are several.
In my case my instance directory is "~/zope3", so let's create a directory named "bookmarker":
{jrush]# cd ~/zope3/lib/python
[jrush]# mkdir bookmarker
[jrush]# cd bookmarker
Defining your Interface(s)Components are represented as Python modules located somewhere along the Python import path (PYTHONPATH). A convenient place, and which will be private to a particular Zope instance is the directory "lib/python/" underneath the instance home, in my case "~/zope3/lib/python/". You can place any number of components in the same package. Create a directory to contain your work and a nearly empty "__init__.py" so that Python considers the directory to be an importable package. [jrush]# cd ~/zope/lib/python [jrush]# mkdir boom [jrush]# echo "# A Python Package" > boom/__init__.py Create a file, arbitrarily (??? is it arbitrary ???) named "interfaces.py", that contains Python classes, without implementation bodies, that encode the API of your components. First, import the necessary supporting elements. from zope.interface import Interface from zope.schema import Text, TextLine, Field <:vspace> from zope.app.container.constraints import ContainerTypesConstraint from zope.app.container.constraints import ItemTypePrecondition from zope.app.container.interfaces import IContained, IContainer The sample application being developed here is a folder that contains a set of webpage bookmarks. A bookmark is a non-container (i.e. non-folder) object new to Zope that we decide will possess a "URL string" and a "textual description" that may be multi-lined and word-wrapped. Since this is a new interface not based on anything already in Zope, we subclass it from "Interface". class IMark(Interface): """This is the book mark object.""" <:vspace> url = TextLine( title=u"URL/Link", description=u"URL of the website", default=u"http://www.zope.org", required=True) <:vspace> description = Text( title=u"Description", description=u"Description of the website", default=u"", required=False) Next let's create a new kind of container or folder, specifically to hold all of our bookmarks. Since Zope knows about object containers, we'll subclass it from the existing IContainer interface. And we'll declare that such a folder will have a name attribute. We'll also give it a precondition limiting the types of objects that can be placed into it. Our bookmark folder will contain only bookmark objects. class IBookMarker(IContainer): """This is the container for all book marks.""" <:vspace> name = TextLine( title=u"Name of BookMarker", description=u"A name for BookMarker", default=u"", required=True) <:vspace> def __setitem__(name, obj): pass <:vspace> __setitem__.precondition = ItemTypePrecondition(IMark) And conversely, we'll add a constraint that our bookmark objects can only be placed into a bookmark folder. We want to always be able to find our bookmarks so we limit their placement. Note however that we've not placed any constraint on how many bookmark folders can existing in the system at one time. class IMarkContained(IContained): """A book mark can only contain in a BookMarker""" <:vspace> __parent__ = Field( constraint = ContainerTypesConstraint(IBookMarker)) Implementing Unit Tests against Your Interface(s)Unit tests insure our components behave correctly. In our case we'll put in place an empty unit test suite, to show the concept. Create a file, again arbitrarily named "tests.py". (??? is it arbitrary ???) import unittest from zope.testing.doctestunit import DocTestSuite from zope.app.container.tests.test_icontainer import TestSampleContainer from boom.bookmarker import BookMarker, Mark <:vspace> class BookMarkerContainerTest(TestSampleContainer): def makeBookMarkerObject(self): return BookMarker() <:vspace> def test_suite(): return unittest.TestSuite(( DocTestSuite('boom.bookmarker'), unittest.makeSuite(BookMarkerContainerTest), )) <:vspace> if __name__ == '__main__': unittest.main(defaultTest='test_suite') To run your tests, perform the following, from any directory: [jrush]# cd ~/zope3 [jrush]# bin/test -vpu --dir boom Implement the Internals of your ComponentPlace the actual implementation of your components, in our case, in the file "bookmarker.py". We'll provide 'doctests' in our code, which is where small verifiable tests and their results are intermixed. Doctests are a standard part of the Python library and are fully documented there. This technique is called "example driven unit testing". __docformat__ = 'restructuredtext' <:vspace> from zope.interface import implements from zope.app.container.btree import BTreeContainer from zope.app.container.contained import Contained <:vspace> from boom.interfaces import IMark, IMarkContained, IBookMarker <:vspace> class Mark(Contained): """Implementation of IMark <:vspace> Make sure that the `Mark` implements the `IMark` interface:: <:vspace> >>> from zope.interface.verify import verifyClass >>> verifyClass(IMark, Mark) True <:vspace> Make sure that the `Mark` implements the `IMarkContained` interface:: <:vspace> >>> from zope.interface.verify import verifyClass >>> verifyClass(IMarkContained, Mark) True <:vspace> An example of checking the url of Mark:: <:vspace> >>> mk = Mark() >>> mk.url u'http://www.zope.org' >>> mk.url = u'http://www.python.org' >>> mk.url u'http://www.python.org' <:vspace> An example of checking the description of Mark:: <:vspace> >>> mk = Mark() >>> mk.description u'' >>> mk.description = u'Zope Project Web Site' >>> mk.description u'Zope Project Web Site' """ <:vspace> implements(IMark, IMarkContained) <:vspace> url = u"http://www.zope.org" description = u"" <:vspace> class BookMarker(BTreeContainer): """Implementation of IBookMarker using B-Tree Container <:vspace> Make sure that the `BookMarker` implements the `IBookMarker` interface:: <:vspace> >>> from zope.interface.verify import verifyClass >>> verifyClass(IBookMarker, BookMarker) True <:vspace> An example of changing the name of BookMarker:: <:vspace> >>> bm = BookMarker() >>> bm.name u'' >>> bm.name = u'MyBookMarker' >>> bm.name u'MyBookMarker' """ <:vspace> implements(IBookMarker) <:vspace> name = u"" Provide Configuration Information to Get Our Component into the ZMIBy itself, what we've done still isn't hooked into Zope. To tell Zope how to wire our component into itself, provide a file named "configure.zcml".
<configure
xmlns="http://namespaces.zope.org/zope"
xmlns:browser="http://namespaces.zope.org/browser">
<:vspace>
<interface
interface=".interfaces.IBookMarker"
type="zope.app.content.interfaces.IContentType"
/>
<:vspace>
<content class=".bookmarker.BookMarker">
<implements
interface="zope.app.annotation.interfaces.IAttributeAnnotatable"
/>
<implements
interface="zope.app.container.interfaces.IContentContainer"
/>
<factory
id="boom.bookmarker.BookMarker"
description="Book Marker"
/>
<require
permission="zope.ManageContent"
interface=".interfaces.IBookMarker"
/>
<require
permission="zope.ManageContent"
set_schema=".interfaces.IBookMarker"
/>
</content>
<:vspace>
<interface
interface=".interfaces.IMark"
type="zope.app.content.interfaces.IContentType"
/>
<:vspace>
<content class=".bookmarker.Mark">
<implements
interface="zope.app.annotation.interfaces.IAttributeAnnotatable"
/>
<factory
id="boom.bookmarker.Mark"
description="A book mark."
/>
<require
permission="zope.ManageContent"
interface=".interfaces.IMark"/>
<require
permission="zope.ManageContent"
set_schema=".interfaces.IMark"
/>
</content>
<:vspace>
<browser:addform
label="Add Book Marker"
name="AddBookMarker.html"
schema="boom.interfaces.IBookMarker"
content_factory="boom.bookmarker.BookMarker"
fields="name"
permission="zope.ManageContent"
/>
<:vspace>
<browser:addMenuItem
class=".bookmarker.BookMarker"
title="Book Marker"
permission="zope.ManageContent"
view="AddBookMarker.html"
/>
<:vspace>
<browser:editform
schema="boom.interfaces.IBookMarker"
for="boom.interfaces.IBookMarker"
label="Change Book Marker"
name="edit.html"
permission="zope.ManageContent"
menu="zmi_views" title="Edit"
/>
<:vspace>
<browser:containerViews
for="boom.interfaces.IBookMarker"
index="zope.View"
contents="zope.View"
add="zope.ManageContent"
/>
<:vspace>
<browser:addform
label="Add Mark"
name="AddMark.html"
schema="boom.interfaces.IMark"
content_factory="boom.bookmarker.Mark"
fields="url description"
permission="zope.ManageContent"
/>
<:vspace>
<browser:addMenuItem
class="boom.bookmarker.Mark"
title="Mark"
description="URL of Website"
permission="zope.ManageContent"
view="AddMark.html"
/>
<:vspace>
<browser:editform
schema="boom.interfaces.IMark"
for="boom.interfaces.IMark"
label="Change Mark"
fields="url description"
name="edit.html"
permission="zope.ManageContent"
menu="zmi_views" title="Edit"
/>
<:vspace>
<browser:page
name="marks.html"
for="boom.interfaces.IBookMarker"
class=".browser.BookMarks"
template="marks.pt"
permission="zope.Public"
menu="zmi_views"
title="Marks"
/>
<:vspace>
</configure>
The above file is placed into our project directory, which itself can be anywhere along the Python import path. To make Zope aware of the location of our project, create a file named "boom-configure.zcml" but place it into the Zope instance directory "etc/package-includes". In my case this is "~/zope3/etc/package-includes": <include package="boom"/> Notice that there are two similarly named files, one "configure.zcml" and the other "boom-configure.zcml". They have very different purposes and go in different places. Your package is now registered with the particular Zope instance. You can fire up Zope, open your browser, add a BookMarker folder and inside that a few book marks themselves. Provide a View for Your ComponentWhile your components have an administrative face to them, they lack the necessary HTML to present to the casual webbrowser. To give them a face, we provide an adapter that takes in a context and a request and produces ???. We'll store the Python code for this adapter into the file "browser.py": from boom.interfaces import IMark <:vspace> class BookMarks: <:vspace> def __init__(self, context, request, base_url=''): self.context = context self.request = request self.base_url = base_url <:vspace> def listMarks(self): marks = [] for name, child in self.context.items(): if IMark.providedBy(child): info = {} info['url'] = child.url info['description'] = child.description marks.append(info) return marks And provide a "Page Template" for presenting the content of your bookmarks folder, in the file "marks.pt":
<html metal:use-macro="views/standard_macros/view">
<body>
<div metal:fill-slot="body">
<:vspace>
<div class="row">
<div class="label">Book Marks:</div>
<br/><br/>
<li tal:repeat="item view/listMarks">
<:vspace>
<a href="" tal:attributes="href item/url">
<span tal:content="item/url">Link</span>
</a>
<pre tal:content="item/description">Description</pre>
<br/>
<:vspace>
</li>
</div>
<:vspace>
</div>
</body>
</html>
Run Zope again and you'll see the Python file above gave your bookmarks folder a new tab named "Marks". Clicking on "Marks" tab to see all your bookmarks. Implementing Functional Tests for Your ComponentFunctional tests differ from unit tests in that functional tests require the webserver environment in order to be performed. Unit tests can be run against your components in relative isolation. Since functional tests run in a webserver environment, they are actually testing our "view" we created rather than the underlying abstract components themselves. To add functional tests to your components, create the file "ftests.py": import unittest from zope.app.testing.functional import BrowserTestCase <:vspace> class BookMarksTest(BrowserTestCase): <:vspace> def testAddBookMarker(self): response = self.publish( '/+/AddBookMarker.html=bm', basic='mgr:mgrpw', form={'field.name': u'MyBookMarker', 'UPDATE_SUBMIT': 'Add'}) self.assertEqual(response.getStatus(), 302) self.assertEqual(response.getHeader('Location'), 'http://localhost/@@contents.html') <:vspace> def testAddMark(self): self.testAddBookMarker() response = self.publish( '/bm/+/AddMark.html=mrk', basic='mgr:mgrpw', form={'field.url': u'http://www.zope.org', 'field.description': u'Zope Project Site', 'UPDATE_SUBMIT': 'Add'}) self.assertEqual(response.getStatus(), 302) self.assertEqual(response.getHeader('Location'), 'http://localhost/bm/@@contents.html') <:vspace> def testMarksListing(self): self.testAddMark() response = self.publish( '/bm/@@marks.html', basic='mgr:mgrpw') body = response.getBody() self.checkForBrokenLinks(body, '/bm/@@marks.html', basic='mgr:mgrpw') self.assert_(body.find('Zope Project Site') > 0) <:vspace> def test_suite(): return unittest.TestSuite(( unittest.makeSuite(BookMarksTest), )) <:vspace> if __name__ == '__main__': unittest.main(defaultTest='test_suite') To run the functional test, perform the following: [jrush]# cd ~/zope3 [jrush]# bin/test -vpf --dir boom This will actually bring up the Zope server in order to run. Producing Usage Documentation(missing from article) In conclusion, your project directory should look something like: drwxr-xr-x baiju/baiju 0 2005-10-16 07:36:32 boom/ -rw-r--r-- baiju/baiju 16 2005-10-16 07:36:20 boom/__init__.py -rw-r--r-- baiju/baiju 2376 2005-10-16 07:36:20 boom/ftests.py -rw-r--r-- baiju/baiju 1383 2005-10-16 07:36:20 boom/tests.py -rw-r--r-- baiju/baiju 1327 2005-10-16 07:36:20 boom/browser.py -rw-r--r-- baiju/baiju 2508 2005-10-16 07:36:20 boom/bookmarker.py -rw-r--r-- baiju/baiju 1990 2005-10-16 07:36:20 boom/interfaces.py -rw-r--r-- baiju/baiju 475 2005-10-16 07:36:20 boom/marks.pt -rw-r--r-- baiju/baiju 3108 2005-10-16 07:36:20 boom/configure.zcml -rw-r--r-- baiju/baiju 97 2005-10-16 07:36:20 boom/boom-configure.zcml -rw-r--r-- baiju/baiju 793 2005-10-16 07:36:20 boom/README |
|||
| Recent Changes Printable View Page History Edit Page | |||