2

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?

Milano
  • 18,048
  • 37
  • 153
  • 353
  • Just using `self._interest_rate`, if it doesn't exist, would already show warnings at compile time and raise an exception at runtime - what exactly does the decorator add? I get that you would like to add other restrictions as well, but again what is the difference between adding the decorator vs. just adding the check where it is needed, other than a bunch of overhead? – Grismar Jun 18 '22 at 22:37
  • @Grismar This is just a very simplified example and besides that, _interest_rate always exists as it is defined as a `dataclass` field. Every `property` does some calculations and expects `fields` and/or `properties`, that are used, to be valid. That's why I want to validate it this way. In fact, I'll not raise exceptions, rather I'll return Result objects but that's not important in the example I think. – Milano Jun 18 '22 at 22:42
  • @Grismar the point is not to flood every property with X lines of code that just does the same. – Milano Jun 18 '22 at 22:48

1 Answers1

2

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"

Grismar
  • 27,561
  • 4
  • 31
  • 54
  • 1
    You're welcome, make sure you play around with the example a bit, so you understand why the bits need to go in this order. – Grismar Jun 18 '22 at 23:18
  • checking `hasattr` before `getattr` might be a bit wasteful imo, because I believe `hasattr` calls `getattr` anyway and then catches the exception. – rv.kvetch Jun 19 '22 at 23:42