Rick
Rick

Reputation: 45281

Validating attribute names using a decorator

I have a decorator class validatekeys() and a Node3D() class.

The intention is for Node3D to hold coordinate values of x, y, and z which are retrieved using a @property decorator, and can be set using either a @coords.setter decorator (which calls set_coords()) or directly using set_coords() which is itself decorated with validatekeys(). I am using decorators to accomplish this so that I can add other classes later, like Node2D(), for example.

Code:

class validatekeys(object):
    def __init__(self,*keysIterable):
        self.validkeys = []
        for k in keysIterable:
            self.validkeys.append(k)
    def __call__(self,f):
        def wrapped_f(*args,**kwargs):
            for a in kwargs:
                if not a in self.validkeys:
                    raise Exception()
            self.__dict__.update(kwargs)
            return f(self,**kwargs)
        return wrapped_f

class Node3D(object):
    @property
    def coords(self):
        return self.__dict__
    @coords.setter
    def coords(self,Coords):
        self.set_coords(**Coords)
    @validatekeys('x','y','z')
    def set_coords(self,**Coords):
        pass

However, part of the output is not as expected:

n = Node2D()
n.coords               #{} <--expected
n.set_coords(x=1,y=2)
n.coords               #{} <--not expected
n.set_coords(a=1,b=2)  #Exception  <--expected

It looks like the self.__dict__ is not being updated correctly. However, I've been unable to figure out how to fix this. Any suggestions?

Note that although I am certainly interested in alternative formulations/approaches on solving this problem (validating keys input to a setter), this is mostly a learning exercise to understand how decorators, classes, etc etc work.

Upvotes: 0

Views: 126

Answers (2)

Martijn Pieters
Martijn Pieters

Reputation: 1123420

Your decorator is updating the wrong __dict__; self in your decorator __call__ is the decorator object itself.

You need to extract the bound self argument from the called wrapper:

def wrapped_f(*args, **kwargs):
    for a in kwargs:
        if not a in self.validkeys:
            raise Exception()
    instance = args[0]
    instance.__dict__.update(kwargs)
    return f(*args, **kwargs)

You can give your wrapped_f() an explicit first argument too:

def wrapped_f(instance, *args, **kwargs):
    for a in kwargs:
        if not a in self.validkeys:
            raise Exception()
    instance.__dict__.update(kwargs)
    return f(instance, *args, **kwargs)

Here instance is bound to the Node3D instance. Note that there is no hard requirement to name this variable self; that is just a convention.

Upvotes: 2

BrenBarn
BrenBarn

Reputation: 251438

The self in your __call__ refers to the validator, not the Node3D object, so the validator is updating its own __dict__. Try this instead:

class validatekeys(object):
    def __init__(self,*keysIterable):
        self.validkeys = []
        for k in keysIterable:
            self.validkeys.append(k)
    def __call__(validator_self,f):
        def wrapped_f(self, *args,**kwargs):
            for a in kwargs:
                if not a in validator_self.validkeys:
                    raise Exception()
            self.__dict__.update(kwargs)
            return f(self, *args, **kwargs)
        return wrapped_f

Here I've renamed the self in the __call__ to validator_self to make it clear that that self refers to the validator. I added a self to the wrapper function; this self will refer to the "real" self of the Node3D object where the validated method is.

Upvotes: 1

Related Questions