[Zope-dev] The Debugger Is Your Friend - an object Browser would be nice too
Loren Stafford
lstaffor@dynalogic.com
Mon, 28 Feb 2000 16:00:35 -0800
Does anyone know of such a thing as a generic Python class browser that
could be adapted to Zope?
-- Loren
----- Original Message -----
From: Damian Morton <morton@dennisinter.com>
To: Michel Pelletier <michel@digicool.com>
Cc: <zope-dev@zope.org>
Sent: February 28, 2000 03:26 PM
Subject: RE: [Zope-dev] The Debugger Is Your Friend - an object Browser
would be nice too
> An extremely usefull adjunct to the debugger would be an object browser
for
> the running Zope application.
>
> Unfortunately, using the monitor or even the debugger to browse through
the
> running application can be extremely painful, especialy as many of the
> methods and properties of objects arent displayed using dir(object) or
> object.__dict__. What is needed is a browser that understands classes,
> inheritance and acquisition.
>
> Im looking to find the list of properties associated with the object
> StuffMag.
>
> Heres how Im using the monitor to browse the live application:
>
> ### lets bring in something to help format our data
> >>> from pprint import pprint
>
> ### This is the object I want to examine
> >>> app.StuffMag
> <StuffClass instance at 011FF190>
>
> #### Lets see whats inside this object
> >>> pprint(app.StuffMag.__dict__)
> {'BabeSection': <StuffSectionClass instance at 01222720>,
> 'OtherSection': <StuffSectionClass instance at 01222370>,
> 'StuffSection': <StuffSectionClass instance at 012227B0>,
> '__ac_local_roles__': {'damien.morton': ['Owner']},
> '_objects': ({'meta_type': 'Section object', 'id': 'BabeSection'},
> {'meta_type': 'Section object', 'id': 'StuffSection'},
> {'meta_type': 'Section object', 'id': 'OtherSection'},
> {'meta_type': 'Section2 object', 'id': 'section2'},
> {'meta_type': 'Section3 object', 'id': 'section3'},
> {'meta_type': 'Section5 object', 'id': 'section5'},
> {'meta_type': 'Section5 object', 'id': 'section5.2'},
> {'meta_type': 'Section5 object', 'id': 'mysection5'},
> {'meta_type': 'myClass object', 'id': 'myObject'}),
> 'body': '',
> 'heading': 'HI THERE!!!',
> 'id': 'StuffMag',
> 'myObject': <myClass instance at 01222660>,
> 'mysection5': <Section5Class instance at 012222A0>,
> 'section2': <StuffSection2 instance at 012224D0>,
> 'section3': <section3Class instance at 012225A0>,
> 'section5': <Section5Class instance at 012218A0>,
> 'section5.2': <Section5Class instance at 01222400>}
>
> >>> app.StuffMag.propertysheets
> <StuffClass_PropertySheetsClass instance at 011FAA50>
>
> ### How did I know this object had a propertysheets attribute?
> >>> pprint(app.StuffMag.__class__.__dict__)
> {'Section5Class': <MWp instance at 011FACE0>,
> 'Section5Class_add': <MWp instance at 011FA4E0>,
> 'Section5Class_addForm': <MWp instance at 011FA8B0>,
> 'Section5Class_factory': <MWp instance at 011FA4C0>,
> 'StuffSection2': <MWp instance at 011F9E30>,
> 'StuffSection2_add': <MWp instance at 011C57C0>,
> 'StuffSection2_addForm': <MWp instance at 011F9140>,
> 'StuffSection2_factory': <MWp instance at 011FA3C0>,
> 'StuffSectionClass': <MWp instance at 011FAF60>,
> 'StuffSectionClass_add': <MWp instance at 011FABE0>,
> 'StuffSectionClass_addForm': <MWp instance at 011C71C0>,
> 'StuffSectionClass_factory': <MWp instance at 011FA530>,
> '__ac_permissions__': (('Add Section objects', (), ('Manager',)),),
> '__doc__': 'StuffClass',
> '__module__': '*gK7YKWqDX9Dn00XJH1ZJPQ==',
> '_p_changed': None,
> '_p_jar': <ZODB.Connection.Connection instance at 11ec6a0>,
> '_p_oid': '\000\000\000\000\000\000\014Q',
> '_p_serial': '\0032\254\306`\335\272\346',
> '_zclass_method_meta_types': ({'action': 'StuffSectionClass_factory',
> 'name': 'Section object',
> 'permission': 'Add Section objects',
> 'product': 'methods'},
> {'action': 'Section5Class_factory',
> 'name': 'Section5 object',
> 'permission': 'Add Section objects',
> 'product': 'methods'},
> {'action': 'StuffSection2_factory',
> 'name': 'Section2 object',
> 'permission': 'Add Section objects',
> 'product': 'methods'},
> {'action': 'section3Class_factory',
> 'name': 'Section3 object',
> 'permission': 'Add Section objects',
> 'product': 'methods'},
> {'action': 'myClass_factory',
> 'name': 'myClass object',
> 'permission': 'Add myClass objects',
> 'product': 'methods'}),
> 'body': '',
> 'heading': '',
> 'icon': '',
> 'isPrincipiaFolderish': 'Y',
> 'manage_options': ({'action': 'propertysheets/Properties/manage',
> 'label': 'Properties'},
> {'label': 'Contents', 'action': 'manage_main'}),
> 'meta_type': 'Stuff Site object',
> 'meta_types': (),
> 'myClass': <MWp instance at 011F9EA0>,
> 'myClass_add': <MWp instance at 011C8210>,
> 'myClass_addForm': <MWp instance at 011F9430>,
> 'myClass_factory': <MWp instance at 011FA740>,
> 'propertysheets': <StuffClass_PropertySheetsClass instance at 011FAA50>,
> 'section3Class': <MWp instance at 011B2960>,
> 'section3Class_add': <MWp instance at 011FAE60>,
> 'section3Class_addForm': <MWp instance at 011F8230>,
> 'section3Class_factory': <MWp instance at 011F92C0>}
>
> ### delving even deeper
> >>> for b in app.StuffMag.__class__.__bases__:
> ... pprint(b.__dict__)
>
> {'__class_init__': <function __class_init__ at a05bf0>,
> '__module__': 'ZClasses.ZClass'}
> {'__module__': 'ZClasses.ObjectManager',
> '__roles__': <PermissionRole instance at 00986D50>,
> '_zclass_method_meta_types': (),
> 'all_meta_types': <function all_meta_types at a6ca90>,
> 'manage_FTPlist__roles__': <PermissionRole instance at 009D7F30>,
> 'manage_FTPstat__roles__': <PermissionRole instance at 009D7F30>,
> 'manage_delObjects__roles__': <PermissionRole instance at 009D4FF0>,
> 'manage_exportObject__roles__': <PermissionRole instance at 00A4D890>,
> 'manage_importExportForm__roles__': <PermissionRole instance at
00A4D890>,
> 'manage_importObject__roles__': <PermissionRole instance at 00A4D890>,
> 'manage_main__roles__': <PermissionRole instance at 00980DF0>,
> 'manage_menu__roles__': <PermissionRole instance at 00980DF0>,
> 'meta_types': (),
> 'objectIds__roles__': <PermissionRole instance at 00986D50>,
> 'objectItems__roles__': <PermissionRole instance at 00986D50>,
> 'objectValues__roles__': <PermissionRole instance at 00986D50>}
> {'__ac_permissions__': (('View', ()),),
> '__doc__': 'Mix-in class combining the most common set of basic
mix-ins\012
> ',
> '__module__': 'OFS.SimpleItem',
> 'manage_options': ({'label': 'Security', 'action': 'manage_access'},)}
>
> ### Hmm, why does a propertysheet have no attributes or methods?
> >>> pprint(app.StuffMag.propertysheets.__dict__)
> {}
>
> ### Lets look a bit deeper again
> >>> pprint(app.StuffMag.propertysheets.__class__.__dict__)
> {'Properties': <ZInstanceSheet instance at 011FA320>,
> '__doc__': 'StuffClass Property Sheets',
> '__module__': None,
> '__propset_attrs__': ('Properties',),
> '_p_changed': None,
> '_p_jar': <ZODB.Connection.Connection instance at 11ec6a0>,
> '_p_oid': '\000\000\000\000\000\000\014P',
> '_p_serial': '\0032\2441d!Pn',
> 'icon': ''}
>
> ### Properties looks like what we want
> >>> app.StuffMag.propertysheets.Properties
> <ZInstanceSheet instance at 011FA320>
>
> ### Lets take a look inside
> >>> pprint(app.StuffMag.propertysheets.Properties.__dict__)
> {'_md': {}, '_base': <ZCommonSheet instance at 012283F0>, 'id':
> 'Properties'}
>
> ### Hmm, have to look at the class again
> >>> pprint(app.StuffMag.propertysheets.Properties.__class__.__dict__)
> {'_Access_contents_information_Permission': '_View_Permission',
> '_Manage_properties_Permission': '_Manage_properties_Permission',
> '__ac_permissions__': (('Manage properties',
> ('manage_addProperty',
> 'manage_editProperties',
> 'manage_delProperties',
> 'manage_changeProperties',
> 'manage')),
> ('Access contents information',
> ('hasProperty',
> 'propertyIds',
> 'propertyValues',
> 'propertyItems',
> ''))),
> '__doc__': 'Waaa this is too hard',
> '__module__': 'ZClasses.Property',
> '__roles__': <PermissionRole instance at 009E8EF0>,
> 'hasProperty__roles__': <PermissionRole instance at 009E8EF0>,
> 'manage__roles__': <PermissionRole instance at 009E7820>,
> 'manage_addProperty__roles__': <PermissionRole instance at 009E7820>,
> 'manage_changeProperties__roles__': <PermissionRole instance at
009E7820>,
> 'manage_delProperties__roles__': <PermissionRole instance at 009E7820>,
> 'manage_editProperties__roles__': <PermissionRole instance at 009E7820>,
> 'propertyIds__roles__': <PermissionRole instance at 009E8EF0>,
> 'propertyItems__roles__': <PermissionRole instance at 009E8EF0>,
> 'propertyValues__roles__': <PermissionRole instance at 009E8EF0>,
> 'v_self': <function v_self at a021a0>}
>
> ### Hmm, lots of stuff, but I cant see what Im looking for, I'll guess
that
> acquisition plays a role here
> ### propertyItems under the permission 'Access contents information' looks
> awfully good
>
> >>> app.StuffMag.propertysheets.Properties.propertyItems
> <Python Method object at b50970>
>
>
> ### Wahooo - we have arrived,
> >>> app.StuffMag.propertysheets.Properties.propertyItems()
> [('heading', 'HI THERE!!!'), ('body', '')]
>
>
> Hmm, it only took an hour or so to figure this out.
>
>
> > -----Original Message-----
> > From: zope-dev-admin@zope.org [mailto:zope-dev-admin@zope.org]On Behalf
> > Of Michel Pelletier
> > Sent: Monday, February 28, 2000 2:24 PM
> > To: pam@digicool.com; zdp@zope.org
> > Cc: zope-dev@zope.org; support@digicool.com
> > Subject: [Zope-dev] The Debugger Is Your Friend
> >
> >
> >
> > And this document tells you why! Last week James W. Howe expressed pain
> > in now knowing how to debug Zope, or even if it is possible. Well, it
> > is, there is even a Zope debugger! But, alas, until now it was
> > undocumented.
> >
> > So here it is folks, 'The Debugger Is Your Friend'. This is a FIRST
> > DRAFT so please read it and send your comments and criticisms to me.
> >
> > The Debugger Is Your Friend
> >
> > Jim Fulton is fond of the expression 'The debugger is your friend'.
> > And although debugging Zope can sometimes be a bit painful, once you
> > are used to the rythum the debugger truly is your friend, and will
> > save you years off your life.
> >
> > Zope has been designed to work in an integrated way with the Python
> > debugger (pdb). In order to really be able to use this document,
> > you must know how to use pdb. pdb is pretty simple as command line
> > debuggers go, and anyone familiar with other popular command line
> > debuggers (like gdb) will feel right at home in pdb. If you are
> > more familiar with the graphical, bells-and-whistles type debugger,
> > I suggest you read the pdb documentation in the standard Python
> > module documentation on the "Python website":http://www.python.org/.
> >
> > For the purposes of this document, I will refer to the debugger as
> > the 'Zope debugger', even though most of it is actually the Python
> > debugger. This is not to dis the python people, it's just a handy
> > convention. Whenever I refer to just the debugger that is not in
> > the context of debugging Zope, I will say 'pdb'.
> >
> > To debug Zope, you must first shut down the Zope process. It is not
> > possible to debug Zope and run it at the same time, as the debugger
> > stops the entire process dead in its tracks (Note: If you are using
> > ZEO, it is possible to run Zope and debug it simultaneously). Also,
> > starting up the debugger will by default start Zope in single
> > threaded mode. It is not possible to run the debugger and Zope in
> > multi-threaded mode at the same time. This is normally not an issue
> > unless you are doing some pretty complex low level stuff, which you
> > shouldn't be messing with anyway if you have to read this, right?
> >
> > For most Zope developer's purposes, the debugger is needed to debug
> > some sort of application level programming error. A common scenario
> > is when developing a new third party Product for Zope. Products
> > extend Zope's functionality but they also present the same kind of
> > debugging problems that are commonly found in any programming
> > environment. It is useful to have an existing debugging
> > infrastructure to help you jump immediatly to your new object and
> > debug it and play with it directly in pdb. The Zope debugger lets
> > you do this.
> >
> > It is also useful to actually debug Zope itself. For example, if
> > you discover some sort of obscure bug in Zope's object request
> > broker (ZPublisher) it would be useful to tell Zope to stop at a
> > certain point in ZPublisher and bring up the debugger. The Zope
> > debugger also lets you do this.
> >
> > In reality, the 'Zope' part of the Zope debugger is actually just a
> > handy way to start up Zope with some pre-configured break points and
> > to tell the debugger where in Zope you want to start debugging.
> > I'll stop talking now and give some examples.
> >
> > Remember, for this example to work, you MUST shut down Zope.
> > Debugging Zope starts in Zope's 'lib/python' directory. 'lib' is a
> > sub-directory of Zope's top level directory. Go to the 'lib/python'
> > directory and fire up Python 1.5.2::
> >
> > Python 1.5.2 (#0, Apr 13 1999, 10:51:12) [MSC 32 bit (Intel)] on
> > win32
> > Copyright 1991-1995 Stichting Mathematisch Centrum, Amsterdam
> > >>> import Zope, ZPublisher
> > >>>
> >
> > Here we have run the python interpreter (which is where using the
> > debugger takes place) and imported two modules, 'Zope' and
> > 'ZPublisher'. If Python complains about an ImportError and not
> > being able to find either module, then you are probably in the wrong
> > directory, or you have not compiled Zope properly.
> >
> > The Zope module is the main Zope application. Now, in most cases,
> > the term 'application' when used in Zope means some sort of
> > application developed on top of Zope, but in this case, we mean Zope
> > itself is the application. The ZPublisher module is the Zope 'ORB'
> > (object request broker) and is needed to run the debugger.
> >
> > If trying to import these modules results in a 'cannot lock
> > database' error, then another Zope process is using your database.
> > Shut that Zope down if you want to debug it. If you get error about
> > 'cannot open file such-and-such' then you do not have proper
> > permission to read or write one of Zope's files. Get the right
> > permission and try again.
> >
> > At this point, if python did not complain, then all is well and you
> > are on your way to debugging Zope. First, let's try something
> > really neat. Zope is, as you should know, completely protocol
> > agnostic. You do not need a webserver to actually call and use
> > Zope, you can do it right here from this python prompt! To
> > illustrate this, let's call your Zope site and ask it for the very
> > root level URL. If your Zope website's URL is
> > 'http://www.mysite.org/', then the very root level URL is '' (or
> > '/'). Calling this object with the debugger is as easy as, and just
> > like *calling it through the web*. The debugger will set up all
> > kinds of fake environment to make Zope not know the difference
> > between you calling it here with the debugger and accessing your top
> > level root object::
> >
> > >>> ZPublisher.Zope('')
> > Status: 200 OK
> > X-Powered-By: Zope (www.zope.org), Python (www.python.org)
> > Content-Length: 1238
> > Content-Type: text/html
> >
> > <HTML><HEAD><TITLE>Zope</TITLE>
> >
> > ... blah blah...
> >
> > </BODY></HTML>
> > >>>
> >
> > Wow, wasn't that cool? If you look closely, you will see that the
> > content returned is *exactly* that which is returned when you call
> > your root level object through HTTP, including all the HTTP headers.
> >
> > So now let's suppose that you have an object in your Zope's root
> > level folder called 'aFoo'. aFoo is an instance of your Foo class
> > You can call your aFoo instance with the debugger like so::
> >
> > >>> ZPublisher.Zope('aFoo')
> >
> > (or you could say '/aFoo' or '/aFoo/' etc..., the rules are the same
> > as HTTP). This is a handy way to test out your objects without ever
> > leaving python.
> >
> > Now, let's get to actually debugging your object. Debugging,
> > typically, involves running your 'program' up to a point where you
> > think it's failing, and then inspecting the state of your variables
> > and objects. The easy part is the actual inspection, the hard part
> > is getting your program to stop at the right point. For example, if
> > you suspect your logic is failing right at the one hundredth
> > iteration in a large for loop, then you need to cleverly construct a
> > breakpoint to stop your program right before this iteration.
> >
> > The actual techniques of doing things like this, conditional
> > breakpoints and other advanced bebugging, is beyond the scope of
> > this document. Many debugger come with documentation, but few come
> > with an explanation of the 'Zen' of debugging. Your best bet is to
> > play with the debugger as much as possible, and to even use it to
> > help you activly engineer your software.
> >
> > So, for the sake or example, let's say that your 'aFoo' object is
> > defined in a Zope Product called 'FooProduct', and is located in the
> > 'lib/python/Products/FooProduct' directory. The class that defines
> > the 'aFoo' instance is also called 'aFoo', and is defined in the
> > 'aFoo.py' module in your Product. Therefore, from Zope's
> > perspective, your aFoo's classes fully qualified name is
> > 'Products.FooProduct.aFoo.aFoo'. All Zope objects have this
> > kind of fully qualified name. For example, the ZCatalog class can
> > be found in 'Products.ZCatalog.ZCatalog.ZCatalog (The redundancy is
> > because the Product, Module, and class are all named 'ZCatalog').
> >
> > Now, your 'aFoo' class defines a method called 'fooMethod' that is
> > definatly not doing the right thing for you. Here is a possible
> > definition of your class and method::
> >
> > class aFoo:
> >
> > def fooMethod(self, N):
> > """ return the sum from 1 to N """
> > sum = 0
> > for x in range(N):
> > sum = sum + x
> > return sum
> >
> >
> > This method can be called with N=5 through the web by visiting
> > 'http://www.mysite.org/aFoo/fooMethod?N=5'. This will make Zope
> > return the value '15', renderered into an HTML document. There's
> > actually nothing wrong with this method to debug, but for the sake
> > of argument let's suppose that returning the sumation of N is not
> > what we wanted, and therefore we want to debug this method.
> >
> > Well then, let's fire up the debugger! This is done in a very
> > similar way to how we called Zope through the python interpreter
> > before, except that we introduce one new argument to the call to
> > 'Zope'::
> >
> > >>> ZPublisher.Zope('/aFoo/fooMethod?N=5', d=1)
> > * Type "s<cr>c<cr>" to jump to beginning of real publishing process.
> > * Then type c<cr> to jump to the beginning of the URL traversal
> > algorithm.
> > * Then type c<cr> to jump to published object call.
> > > <string>(0)?()
> > pdb>
> >
> >
> > Here, we are calling Zope from the interpreter, just like before,
> > but there are two differences. First, we are specificaly calling
> > our method ('fooMethod') with an argument, and second, we have
> > provided a new argument to the Zope call, 'd=1'. The 'd' argument,
> > when true, causes Zope to fire up in the python debugger, pdb.
> > Notice how the python prompt changed from '>>>' to 'pdb>'. This
> > indicates that we are in the debugger.
> >
> > When you first fire up the debugger, Zope gives you a helpfull
> > message that tells you how to get to your object. To understand
> > this message, it's useful to know how we have set Zope up to be
> > debugged. When Zope fires up in debugger mode, there are three
> > breakpoints set for you automatically (if you don't know what a
> > breakpoint is, you need to read the python debugger documentation).
> > The first breakpoint stops the program at the point that ZPublisher
> > (the Zope ORB) tries to publish the application module (in this
> > case, the application module is 'Zope'). The second breakpoint
> > stops the program right before ZPublisher tries to traverse down the
> > provided URL path (in this case, '/aFoo/fooMethod'). The third
> > breakpoint will stop the program right before ZPublisher calls the
> > object it finds that matches the URL path (in this case, the 'aFoo'
> > object).
> >
> > So, the little blurb that comes up and tells you some keys to press
> > is telling you these things in a terse way. Hitting 's' will 's'tep
> > you into the debugger, and hitting 'c' will 'c'ontinue the execution
> > of the program until it hits a breakpoint.
> >
> > Note however that none of these breakpoints will stop the program at
> > 'fooMethod'. To stop the debugger right there, you need to tell the
> > debugger to set a new breakpoint. This is done quite easily.
> > First, you must import your Python module (in this case, a Zope
> > Product called 'aFoo', note the above discussion on 'fully
> > qualified' names).
> >
> > pdb> import Products
> > pdb> b Products.FooProduct.aFoo.aFoo.fooMethod
> > Breakpoint 5 at C:\Program
> > Files\WebSite\lib\python\Products\FooProduct\aFoo.py:42
> > pdb>
> >
> > First, we import 'Products'. Since your module is a Zope Products,
> > it can be found in the Products Module. Next, we set a new
> > breakpoint with the 'b'reak debugger command (pdb allows you to use
> > single letter commands, but you could have also used the entire word
> > 'break'). The breakpoint we set is
> > 'Products.FooProduct.aFoo.aFoo.fooMethod'. After setting this
> > breakpoint, the debugger will respond that it found the method in
> > question in a certain file, on a certain line (in this case, the
> > ficticious line 42) and return you to the debugger.
> >
> > Whew! Lots of stuff to take in there in a few short paragraphs, but
> > you are now on your way to debugging your method with style. Now,
> > we want to get to our 'fooMethod' so we can start debugging it.
> > But along the way, we must first 'c'ontinue through the various
> > breakpoints that Zope has set for us. Although this may seem like a
> > bit of a burden, it's actually quite good to get a feel for how Zope
> > works internally by getting down the rythum that Zope uses to
> > publish your object. In these next examples, my inline comments
> > will begin with '#". Obviously, you won't see these comments when
> > you are debugging. So let's debug::
> >
> > pdb> s
> > # 's'tep into the actual debuging
> >
> > > <string>(1)?()
> > # this is pdb's response to being steped into, ignore it
> >
> > pdb> c
> > # now, let's 'c'ontinue onto the next breakpoint
> >
> > > C:\Program
> > Files\WebSite\lib\python\ZPublisher\Publish.py(112)publish()
> > -> def publish(request, module_name, after_list, debug=0,
> >
> > # pdb has stoped at the first breakpoint, which is the point where
> > # ZPubisher tries to publish the application module.
> >
> > pdb> c
> > # continuing onto the next breakpoint we get...
> >
> > > C:\Program
> > Files\WebSite\lib\python\ZPublisher\Publish.py(101)call_object()
> > -> def call_object(object, args, request):
> >
> > # Here, ZPublisher (which is now publishing the application) has
> > # found your object and is about to call it. Let's jump out here
> > # and discuss this point...
> >
> > # 'Calling' your object consists of applying the arguments supplied
> > by
> > # ZPublisher against the object. Here, you can see how ZPublisher
> > is
> > # passing three arguments into this process. The first argument is
> > # 'object' and is the actual object you want to call. This can be
> > # verified by 'p'rinting the object::
> >
> > pdb> p object
> > <aFoo instance at 00AFE410>
> >
> > # Neat! So here you can inspect your object (with the 'p'rint
> > # command) and even play with it a bit, but we're not there yet!
> > #
> > # The next argument is 'args'. This is a tuple of arguments that
> > # ZPublisher will apply against your object call.
> > #
> > # The final argument is 'request'. This is the 'request object' and
> > # will eventually be transformed in to the DTML usable object
> > 'REQUEST'.
> >
> > pdb> c
> >
> > # Now let's continue, which breakpoint is next? Yours!
> >
> > > C:\Program
> > Files\WebSite\lib\python\Products\FooProduct\aFoo.py(42)fooMethod()
> > -> def fooMethod(self, N)
> >
> > # and now we are here, at YOUR method. How can we be sure, well,
> > # let's tell the debugger to show us where we are in the code:
> >
> >
> > pdb> l
> > 41 def fooMethod(self, N):
> > 42 """ return the sum from 1 to N """
> > 43-> sum = 0
> > 44 for x in range(N):
> > 45 sum = sum + x
> > 46 return sum
> >
> >
> > And that's it, really. From here, with a little knowledge of the
> > python debugger, you should be able to do any kind of debugging task
> > that is needed.
> >
> > _______________________________________________
> > Zope-Dev maillist - Zope-Dev@zope.org
> > http://lists.zope.org/mailman/listinfo/zope-dev
> > ** No cross posts or HTML encoding! **
> > (Related lists -
> > http://lists.zope.org/mailman/listinfo/zope-announce
> > http://lists.zope.org/mailman/listinfo/zope )
> >
>
>
> _______________________________________________
> Zope-Dev maillist - Zope-Dev@zope.org
> http://lists.zope.org/mailman/listinfo/zope-dev
> ** No cross posts or HTML encoding! **
> (Related lists -
> http://lists.zope.org/mailman/listinfo/zope-announce
> http://lists.zope.org/mailman/listinfo/zope )
>