4

I have an ABC BaseAbstract class with several getter/setter properties defined.

I want to require that the value to be set is an int and from 0 - 15.

@luminance.setter
@abstractproperty
@ValidateProperty(Exception, types=(int,), valid=lambda x: True if 0 <= x <= 15 else False)
def luminance(self, value):
    """
    Set a value that indicate the level of light emitted from the block

    :param value: (int): 0 (darkest) - 15 (brightest)
    :return:
    """
    pass

Can someone help me figure out what my ValidateProperty class/method should look like. I started with a class and called the accepts method but this is causing an error:

function object has no attribute 'func_code'

current source:

class ValidateProperty(object):
    @staticmethod
    def accepts(exception, *types, **kwargs):
        def check_accepts(f, **kwargs):
            assert len(types) == f.func_code.co_argcount

            def new_f(*args, **kwds):
                for i, v in enumerate(args):
                    if f.func_code.co_varnames[i] in types and\
                            not isinstance(v, types[f.func_code.co_varnames[i]]):
                        arg = f.func_code.co_varnames[i]
                        exp = types[f.func_code.co_varnames[i]]
                        raise exception("arg '{arg}'={r} does not match {exp}".format(arg=arg,
                                                                                      r=v,
                                                                                      exp=exp))
                        # del exp       (unreachable)

                    for k,v in kwds.__iter__():
                        if k in types and not isinstance(v, types[k]):
                            raise exception("arg '{arg}'={r} does not match {exp}".format(arg=k,
                                                                                          r=v,
                                                                                          exp=types[k]))

                    return f(*args, **kwds)

            new_f.func_name = f.func_name
            return new_f

        return check_accepts
IAbstract
  • 19,551
  • 15
  • 98
  • 146

1 Answers1

1

One of us is confused about how decorators, descriptors (e.g. properties), and abstracts work -- I hope it's not me. ;)

Here is a rough working example:

from abc import ABCMeta, abstractproperty

class ValidateProperty:
    def __init__(inst, exception, arg_type, valid):
        # called on the @ValidateProperty(...) line
        #
        # save the exception to raise, the expected argument type, and
        # the validator code for later use
        inst.exception = exception
        inst.arg_type = arg_type
        inst.validator = valid
    def __call__(inst, func):
        # called after the def has finished, but before it is stored
        #
        # func is the def'd function, save it for later to be called
        # after validating the argument
        def check_accepts(self, value):
            if not inst.validator(value):
                raise inst.exception('value %s is not valid' % value)
            func(self, value)
        return check_accepts

class AbstractTestClass(metaclass=ABCMeta):
    @abstractproperty
    def luminance(self):
        # abstract property
        return
    @luminance.setter
    @ValidateProperty(Exception, int, lambda x: 0 <= x <= 15)
    def luminance(self, value):
        # abstract property with validator
        return

class TestClass(AbstractTestClass):
    # concrete class
    val = 7
    @property
    def luminance(self):
        # concrete property
        return self.val
    @luminance.setter
    def luminance(self, value):
        # concrete property setter
        # call base class first to activate the validator
        AbstractTestClass.__dict__['luminance'].__set__(self, value)
        self.val = value

tc = TestClass()
print(tc.luminance)
tc.luminance = 10
print(tc.luminance)
tc.luminance = 25
print(tc.luminance)

Which results in:

7
10
Traceback (most recent call last):
  File "abstract.py", line 47, in <module>
    tc.luminance = 25
  File "abstract.py", line 40, in luminance
    AbstractTestClass.__dict__['luminance'].__set__(self, value)
  File "abstract.py", line 14, in check_accepts
    raise inst.exception('value %s is not valid' % value)
Exception: value 25 is not valid

A few points to think about:

  • The ValidateProperty is much simpler because a property setter only takes two parameters: self and the new_value

  • When using a class for a decorator, and the decorator takes arguments, then you will need __init__ to save the parameters, and __call__ to actually deal with the defd function

  • Calling a base class property setter is ugly, but you could hide that in a helper function

  • you might want to use a custom metaclass to ensure the validation code is run (which would also avoid the ugly base-class property call)


I suggested a metaclass above to eliminate the need for a direct call to the base class's abstractproperty, and here is an example of such:

from abc import ABCMeta, abstractproperty

class AbstractTestClassMeta(ABCMeta):

    def __new__(metacls, cls, bases, clsdict):
        # create new class
        new_cls = super().__new__(metacls, cls, bases, clsdict)
        # collect all base class dictionaries
        base_dicts = [b.__dict__ for b in bases]
        if not base_dicts:
            return new_cls
        # iterate through clsdict looking for properties
        for name, obj in clsdict.items():
            if not isinstance(obj, (property)):
                continue
            prop_set = getattr(obj, 'fset')
            # found one, now look in bases for validation code
            validators = []
            for d in base_dicts:
                b_obj = d.get(name)
                if (
                        b_obj is not None and
                        isinstance(b_obj.fset, ValidateProperty)
                        ):
                    validators.append(b_obj.fset)
            if validators:
                def check_validators(self, new_val):
                    for func in validators:
                        func(new_val)
                    prop_set(self, new_val)
                new_prop = obj.setter(check_validators)
                setattr(new_cls, name, new_prop)

        return new_cls

This subclasses ABCMeta, and has ABCMeta do all of its work first, then does some additional processing. Namely:

  • go through the created class and look for properties
  • check the base classes to see if they have a matching abstractproperty
  • check the abstractproperty's fset code to see if it is an instance of ValidateProperty
  • if so, save it in a list of validators
  • if the list of validators is not empty
    • make a wrapper that will call each validator before calling the actual property's fset code
    • replace the found property with a new one that uses the wrapper as the setter code

ValidateProperty is a little different as well:

class ValidateProperty:

    def __init__(self, exception, arg_type):
        # called on the @ValidateProperty(...) line
        #
        # save the exception to raise and the expected argument type
        self.exception = exception
        self.arg_type = arg_type
        self.validator = None

    def __call__(self, func_or_value):
        # on the first call, func_or_value is the function to use
        # as the validator
        if self.validator is None:
            self.validator = func_or_value
            return self
        # every subsequent call will be to do the validation
        if (
                not isinstance(func_or_value, self.arg_type) or
                not self.validator(None, func_or_value)
                ):
            raise self.exception(
                '%r is either not a type of %r or is outside '
                'argument range' %
                (func_or_value, type(func_or_value))
                )

The base AbstractTestClass now uses the new AbstractTestClassMeta, and has the validator code directly in the abstractproperty:

class AbstractTestClass(metaclass=AbstractTestClassMeta):

    @abstractproperty
    def luminance(self):
        # abstract property
        pass

    @luminance.setter
    @ValidateProperty(Exception, int)
    def luminance(self, value):
        # abstract property validator
        return 0 <= value <= 15

The final class is the same:

class TestClass(AbstractTestClass):
    # concrete class

    val = 7

    @property
    def luminance(self):
        # concrete property
        return self.val

    @luminance.setter
    def luminance(self, value):
        # concrete property setter
        # call base class first to activate the validator
        # AbstractTestClass.__dict__['luminance'].__set__(self, value)
        self.val = value
Community
  • 1
  • 1
Ethan Furman
  • 63,992
  • 20
  • 159
  • 237
  • The confusion probably rests with me. I am venturing into the more esoteric and advanced areas of Python (which is where I'm having the most fun). I'll work with your solution. I am interested in the suggestion to use a custom metaclass. Can you provide a link on that bullet for a good starting point? I would like to avoid the call to the base class property setter. – IAbstract Feb 16 '16 at 16:45
  • BTW, I am wondering what raised a red flag regarding any confusion on the topic so I know where I might be confused. :) Can you edit w/ a short explanation? Much appreciated. – IAbstract Feb 16 '16 at 16:51
  • @IAbstract: It was your `ValidateProperty` decorator, which wasn't a correct decorator for a regular function, let alone for a method in a class (and a property, at that! ;). Check out [this answer](http://stackoverflow.com/a/1594484/208880) for a very good explanation on decorators in general. – Ethan Furman Feb 16 '16 at 17:02
  • @IAbstract: Here's [an answer of mine](http://stackoverflow.com/a/32151625/208880) that deals lightly with metaclasses (see the bottom half). I'll try to sketch out a validating abstract meta class here in the next few days. – Ethan Furman Feb 16 '16 at 17:22
  • @IAbstract: Just noticed you had this tagged as Python 3 -- I updated the answer (had been using Python 2 syntax). – Ethan Furman Feb 16 '16 at 20:11
  • Okay, so I don't need `(object)` when defining a class? – IAbstract Feb 16 '16 at 22:19
  • @IAbstract: No, because in Python 3 *everything* is based on `object`. The other change was specifying the metaclass in the header instead of in the body. – Ethan Furman Feb 16 '16 at 22:24
  • Wow!!! Okay, I'm really seeing the power of the abstract metaclass. Wonderful work! – IAbstract Feb 22 '16 at 06:16