Milano
Milano

Reputation: 18725

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?

Upvotes: 2

Views: 818

Answers (1)

Grismar
Grismar

Reputation: 31319

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"

Upvotes: 2

Related Questions