Search code examples
pythoninheritancepygamegetattrsetattr

extending a class that uses __getattr__ (pygame.Rect)


I am trying to create my own version of a pygame.Rect rectangle, but with the added feature that when a square is out of certain worldbounds, it appears on the other side.

This means I had to rewrite a lot of functions of pygame.Rect in my extension, and i succeeded in that. No problems here.

The problems start when I try to change __getattr__ and __setattr__. pygame.Rect makes heavy use of these functions, so that for example asking 'top' or 'bottom' refer to 'y' and 'y'+'width' respectively. I have changed these functions to accomodate my feature, but there is one more thing i need to do: In the __init__ function I need to create the worldwidth and worldheight variables. But I can't, since the __setattr__ function does not allow it.

Here is what I have right now:

class Rectinworld(pygame.Rect):
    '''hides the fact that the world is round, when it comes to collisions and such'''

    __slots__ = ['_r']

    def __init__(self,(worldwidth,worldheight),*args):
        object.__setattr__(self, "worldwidth",worldwidth)
        if len(args)==0:
            super(Rectinworld, self).__init__((0,0,0,0))
        else:
            super(Rectinworld, self).__init__(*args)

def __getattr__(self, name):
    if name == 'top':
        return self._r.y
    #etc etc etc
    elif name == 'height':
        return self._r.h
    else:
        raise AttributeError(name)

def __setattr__(self, name, value):
    if name == 'top' or name == 'y':
        self._r.y = value % worldheight
    #etc etc etc
    elif name == 'height':
        if int(value) < 0:
            self._ensure_proxy()
        self._r.h = int(value)
    else:
        raise AttributeError(name)

I left some code out at the #etc etc etc comments for clarity. The code for pygame.Rect is similar: the __setattr__ and __getattr__ do not reference worldwidth or worldheight, but are otherwise the same, the pygame.Rect.__init__ function is extremely long, but i think the following snippet covers the most important:

def __init__(self, *args):
#etc etc etc
if len(args) == 4:
            if args[2] < 0 or args[3] < 0:
                object.__setattr__(self, '_r', _RectProxy((int(args[0]),
                                                          int(args[1]),
                                                          int(args[2]),
int(args[3]))))

The complete code can be found at https://github.com/brython-dev/brython-pygame/blob/master/pygame/rect.py

The error I get now is:

line 10, in __init__
    object.__setattr__(self, "worldwidth",0)
AttributeError: 'Rectinworld' object has no attribute 'worldwidth'

An obvious fix seems to be adding worldwidth and worldheight to __slots__ and go from there. That gave even weirder errors. It would give the following message when trying to set any variable:

line 65, in __getattr__
    raise AttributeError(name)
AttributeError: _r

So in short my question boils down to: why can I not create new variables, and what do I need to do, so that I can?


Solution

  • I'm not sure if it is correct but it works for me (on Python 2 and 3)
    and it uses slots as you wish.

    import pygame
    
    class Rectinworld(object):
    
        '''hides the fact that the world is round, when it comes to collisions and such'''
    
        __slots__ = ['_r', '_worldwidth', '_worldheight']
    
        def __init__(self, worldwidth, worldheight, *args):
            if not args:
                args = (0,0,0,0)
    
            super(Rectinworld, self).__setattr__("_r", pygame.Rect(args))
            super(Rectinworld, self).__setattr__("_worldwidth", worldwidth)
            super(Rectinworld, self).__setattr__("_worldheight", worldheight)
    
        def __getattr__(self, name):
            if name == 'top':
                return self._r.y
            #etc etc etc
            elif name == 'height':
                return self._r.h
            else:
                raise AttributeError(name)
    
        def __setattr__(self, name, value):
            if name == 'top' or name == 'y':
                self._r.y = value % self._worldwidth
            #etc etc etc
            elif name == 'height':
                if int(value) < 0:
                    self._ensure_proxy()
                self._r.h = int(value)
            else:
                raise AttributeError(name)
    
    # --- test ---
    
    r = Rectinworld(100, 100)
    r.top = 120
    print(r.top) # gives 20