Search code examples
pythonpython-decoratorspython-class

decorator that decorates @property and take arguments


My class (dataclass) has many properties that are calculations based on other properties or dataclass fields.

I'm trying to create a decorator that takes a list of required fields or properties. That means they can't be None and can't return ValueError. If any of them is None then I want to do something - for sake of simplicity let's raise ValueError(f'Missing {fieldname}').

def required_fields(required_fields):
    def _required_fields(f):
        def wrapper(self, *args, **kwargs):
            for field in required_fields:
                if getattr(self, field) is None:
                    raise ValueError(f"Missing {field}")
            return f
        return wrapper
    return _required_fields

EDIT - another try

def required_fields(required_fields):
    def _required_fields(f):
        @functools.wraps(f)
        def wrapper(self, *args, **kwargs):
            for field in required_fields:
                if getattr(self, field) is None:
                    raise ValueError(f"Missing {field}")
            return f(self, *args, **kwargs)
        return wrapper

Usage

@dataclasses.dataclass
class LoanCalculator:
    _amount: typing.Optional[M] = None
    _interest_rate: typing.Optional[M] = None
    _years: typing.Optional[M] = None
    _balance: typing.Optional[M] = None
    _payment_day: typing.Optional[int] = None
    _start_date: typing.Optional[datetime.date] = None

    class MissingDataError(Exception):
        pass

    @required_fields(['_interest_rate'])
    @property
    def monthly_interest_rate(self):
        return self._interest_rate / 12

I want to get ValueError(f'Missing _interest_rate') when it's None and I call the monthly_interest_rate.

The problem is that wrapper is not called at all and I don't know how to proceed. Can you give me some hints?


Solution

  • It seems like this is what you're after:

    from dataclasses import dataclass
    
    
    def required_fields(fields):
        def wrapper(fun):
            def checker(self):
                for field in fields:
                    if not hasattr(self, field):
                        raise AttributeError(f'Missing field {field}')
                    if getattr(self, field) is None:
                        raise ValueError(f'Field {field} is `None`')
                return fun(self)
            return checker
        return wrapper
    
    
    @dataclass
    class LoanCalculator:
        _interest_rate: int = None
    
        def set_interest_rate(self, value):
            self._interest_rate = value
    
        @property
        @required_fields(['_interest_rate'])
        def monthly_interest_rate(self):
            return self._interest_rate / 12
    
    
    lc = LoanCalculator()
    try:
        print(lc.monthly_interest_rate)
    except ValueError:
        print('This exception is expected')
    
    lc.set_interest_rate(.5)  # after this, lc._intereste_rate is no longer None
    print(lc.monthly_interest_rate)
    print('No exception here')
    

    This decorator checks that the object passed to the method (which happens to be a property setter) has the required attribute, and that its value is not None.

    Output:

    This exception is expected
    0.041666666666666664
    No exception here
    

    The likely answer to your question here may have been: "you should put @property before the @required_fields decorator, not after it"