[dabo-dev] dabo.lib.reporting refactoring

Author: Stefano Masini

Posted: 2005-03-29 at 10:26:30

Hi Paul and Ed!

I'm finally back home after quite a long trip back from PyCon. During

the non-sleeping flight I finally had the time to look better into

Paul's code for generating the pdf. I concentrated on reportWriter.py,

and ignored reportDesigner.py for now.

I took the chance and did a complete refactoring of that, in order to

show you an example of what I was talking to you at PyCon, regarding

xml serialization.

Very little of the original code structure remains, but I don't want

this to look like throwing away Paul's effort. On the contrary, I used

all the reportlab specific code as is. I just tried to refactor away

everything that was not tightly related to it. Take this as a proof of

concept of my ideas on how things should be done, basing on my

experience. Whether to go this way or another is your decision, since

you guys are the two main developers.

I'll try to highlight the advantages of this design. First of all some

numbers: reportWriter.py is 1018 lines. After refacting a total of 755

lines remained, of which only 467 regard the report generation logic.

The other 288 have been refactored away inside a

dabo.lib.serialization package.

The code is laid out this way:

dabo.lib.reporting:

report.py Pages and bands

objects.py Drawable objects (Rect, String and Image)

run.py if __name__ == '__main__'

util.py A few lines of utility functions

dabo.lib.serialization:

__init__.py

serialization.py

attributes.py

children.py

xmlserializer.py

The dabo.lib.serialization package contains all the code involved into

dealing with xml. For now I only did deserialization from xml (i.e.

xml->python objects), the other way (objects->xml) is missing.

The modules serialization.py, attributes.py and children.py provide

the support for serialization, but the actual operation is done by

xmlserializer.py, therefore any other kind of serialization can be

supported, by simply having another such module like, say,

pickleserializer.py, or socketserializer.py.

Here's an example of how the deserialization works. Every tag in the

xml either represents an object, or represents an attribute of that

object. In the case of attributes, the tag name equals the attribute

name, and the tag's cdata contains the value.

If a tag represents an object, it can either be stored as an attribute

of another object, or it could be contained in a list, that in turn is

stored as an attribute of another object. Dictionaries can be

supported too, but I didn't need them yet.

Example:

from dabo.lib.serialization import *

class Foo(Serializable):

marginRight = LengthAttr(36)

size = PagesizesAttr('letter')

background = ArributeChild('report.Band')

objects = ObjectListChild(['objects.String', 'objects.Rect'])

fooInstance = deserialize(xmldata, Foo)

<foo>

<marginRight>"18 mm"</marginRight>

<size>"A4"</size>

<background>

[...]

</background>

<string>

[...]

</string>

<rect>

[...]

</rect>

</foo>

The function deserialize() takes the xml data and the class

representing the root tag, and returns an instance of that class.

The parsing mechanism goes like this:

- open <foo> tag: the tag name (foo) matches the name of the class

being provided to deserialize(), so instantiate it (and later return

it to the caller)

- open <marginRight> tag: since in Foo class there is a definition for

an attribute called marginRight, so this tag must represent it. Go

ahead, parse its cdata, validate it with the LengthAttr.validate()

method, get back an instance of that attribute and attach it to the

Foo class instance.

- open <background> tag: since in Foo class there is a definition for

a child object named background, this must be it. Recursively parse

the xml according to the rules defined in the report.Band class (not

shown in the example) and stick that class's instance to the Foo

object as an attribute.

- open <string> and <rect> tags: same recursive parsing as for

<background>, getting back instances of the classes object.String and

object.Rect. But this time store these objects inside a list, and

stick that list inside Foo's instance.

This mechanism is implemented by using metaclasses. This approach is

very much like how they do it in Struts (you haven't been at Struts

talk though, if I remember).

A serialize() function can easily be implemented that takes an

instance of such Serializable classes and spits out xml.

This way I was able to factor away all the code dealing with

dictionaries, because all the class instantiation is done for me by

the deserialize() call.

The remaining code, doing the actual interaction with reportlab, is

divided into two modules: report.py and objects.py. The former

contains the code for generating pages and drawing bands. I extended

the functionality to support detail bands spanning across multiple

pages. The latter module contains the drawable objects, that is Rect,

String and Image. For now I commented out Frameset for two reasons:

I'm lazy :), and I'm not sure it's a good idea to have paragraph

specification statically stored in the rfxml template. I rather think

that reportlab framesets should be used when some given text (i.e. out

of a database field) contains carriage returns, and is very long. In

that case a String drawable object cannot be used, and something else

(maybe called like "Paragraph"?) must be used instead, that will make

use of framesets... we can discuss this issue sometime.

Another big change that I've done regards the eval() calls. By simply

calling eval(exprCode), exprCode is evaluated in the local namespace,

therefore 'self' is accessible and potentially modifiable by the mean

user. Alright... statements cannot really be used because eval() takes

only expressions, but I think we should rather user exec, because one

may want to write an if/then/else statement in order to implement some

more interesting functionality. Therefore, at that time, being able to

access 'self' could be dangerous. And even if not used maliciously, it

exposes way too many underlying details. I thought that explicitly

passing a hand crafted environment could be more effective, and the

code also becomes self commenting. If one sees that the call to eval

is passing an environment like {'record':record, 'page':page} it

appears clear that the evaluated code is supposed to make use of these

objects, and nothing more.

Last thing: I changed a little bit the structure of the rfxml, by

moving the band tags inside the <page> tag. Really, I think that

<page> (corresponding to class Page) should probably be called with a

different name, maybe something like BandReportPageGenerator.

Alright, enough talking. I hope you guys have some time to look at my

code. I'm eager to hear your comments about it.

Cheers,

stefano

--- StripMime Report -- processed MIME parts ---

multipart/mixed

text/plain (text body -- kept)

application/x-zip-compressed

---

©2005 Stefano Masini