reporting proxy porting problem

Terry Reedy tjreedy at udel.edu
Thu Nov 28 17:01:25 EST 2013


On 11/28/2013 6:12 AM, Robin Becker wrote:
> I am in the process of porting reportlab to python3.3, one of the
> contributions is a module that implements a reporting proxy with a
> canvas that records all access calls and attribute settings etc etc.
> This fails under python3 because of differences between old and new
> style classes.

All the transition guides I have seen recommend first updating 2.x code 
(and its tests) to work in 2.x with *all* classes being new-style 
classes. I presume one of the code checker programs will check this for 
you. To some extent, the upgrade can be done by changing one class at a 
time.

Yes, this means abandoning support of 2.1 ;-). It also means giving up 
magical hacks that only work with old-style classes.

> I find that I don't understand exactly how the original works so well,

To me, not being comprehensible is not a good sign. I explain some of 
the behavior below.

> but here is a cut down version

Much more should be cut to highlight the important parts.

> ##########################################################################
> class Canvas:
>      def __init__(self,*args,**kwds):
>          self._fontname = 'Helvetica'

This seems pretty useless, but maybe that is a result of cutting down.

> class PDFAction :
>      """Base class to fake method calls or attributes on Canvas"""
>      def __init__(self, parent, action) :
>          """Saves a pointer to the parent object, and the method name."""
>          self._parent = parent
>          self._action = action
>
>      def __getattr__(self, name) :
>          """Probably a method call on an attribute, returns the real one."""

What if it is not a 'method call on an attribute'?

>          print('PDFAction.__getattr__(%s)' % name)
>          return getattr(getattr(self._parent._underlying, self._action), name)

I snipped several irrelevant methods. The important part is that there 
is no __str__ method!

> class PyCanvas:
>      _name = "c"
>
>      def __init__(self, *args, **kwargs) :
>          self._in = 0
>          self._parent = self     # nice trick, isn't it ?

I call this an ugly code stink. But this is not directly an issue here.

>          self._underlying = Canvas(*args,**kwargs)

Snip irrelevant __bool__

>      def __str__(self) :
>          return 'PyCanvas.__str__()'

Also irrelevant for the example.

>      def __getattr__(self, name) :
>          return PDFAction(self, name)

> if __name__=='__main__':
>      c = PyCanvas('filepath.pdf')
>      print('c._fontname=%s' % c._fontname)
>      print('is it a string? %r type=%s' %
>              (isinstance(c._fontname,str),type(c._fontname))))
> ##########################################################################
>
> when run under python27

> C:\code\hg-repos\reportlab>\python27\python.exe z.py
> PDFAction.__getattr__(__str__)
> c._fontname=Helvetica
> is it a string? False type=<type 'instance'>

When Canvas and PyCanvas are upgraded, but PDFAction is not, the result 
remains the same. This fact shows where the old-new difference makes a 
difference.

> and under python33 I see this

> C:\code\hg-repos\reportlab>\python33\python.exe z.py
> c._fontname=<__main__.PDFAction object at 0x00BF8830>
> is it a string? False type=<class '__main__.PDFAction'>

With 2.7, and PDFAction also upgraded (subclassed from object), the 
result is the same. So this is not a 3.x issue at all, but strictly an 
old -- new class issue, which has existed  since 2.2.

The first difference is that
   print('c._fontname=%s' % c._fontname)
produces, with old-style PDFAction,
   PDFAction.__getattr__(__str__)
   c._fontname=Helvetica
but produces, with new-style PDFAction,
   c._fontname=<__main__.PDFAction object at 0x00BF8830>

The reason is that c._fontname invokes PyCanvas.__getattr__, which 
returns PDFAction(c, '_fontname') (call this p). The % string 
interpolation calls p.__str__. For old p, that method does not exist, so 
p.__getattr__('__str__) is called, and that prints a debug line and 
returns 'Helvetica', which is interpolated and printed in the second 
line. For new p, p.__str__ is inherited from object, and we see the 
familiar default 'object at' string.

The hack is depending of the absence of a special method. The immediate 
fix is to give PDFAction a .__str__ method that returns the same string 
as the current .__attr__.

The second different is that type(c._fontname) is 'instance' versus 
"<class '__main__.PDFAction'>". This is because the type of all 
instances of all user-defined old classes is 'instance'. This is pretty 
useless. Any test that depends on this is equally useless and should be 
upgraded to only pass with an instance of the intended (new-style) class.

-- 
Terry Jan Reedy




More information about the Python-list mailing list