Problem 569664 · medium · Level 05 Advanced Algorithms & Graphs

Checked at the Door

py-decorators · py-exceptions · py-api-design · inspect.signature · validation

Several finance functions start with the same lines of checks (if amount <= 0: raise ...), and the checks drift apart as the functions change. Move them into a decorator factory validate(**rules):

@validate(amount=lambda a: a > 0, years=int, rate=lambda r: 0 <= r < 1)
def monthly_payment(amount, years, rate=0.05):
    ...

Each keyword of validate names a parameter of the decorated function, and its value is a rule:

  • a class (such as int or str): the argument must be an instance of it, otherwise the call raises TypeError;
  • any other callable: a predicate; the argument is good when rule(value) is true, otherwise the call raises ValueError.

The message of both errors is "<function name>: bad value for <parameter>: <repr of value>", for example "monthly_payment: bad value for amount: -500". Rules apply to arguments however they are passed (positionally or by keyword), but only to arguments the caller passed: defaults are not checked. When several arguments break their rules, the error names the one whose parameter comes first in the function's signature. The original function runs only if every check passes. A call that does not fit the signature (a missing or unexpected argument) raises TypeError, as a plain call would.

A rule naming a parameter the function does not have raises TypeError when the decorator is applied. The decorated function keeps its __name__ and __doc__.

Setup helpers, available with Run: loan_quotes(cases) (each case is (args, kwargs)), first_bad_in_order(), only_given_arguments(), unknown_rule(), call_mistakes(), many_calls(n), and outcome(call) and raises(fn, *args), which report what a call returned or raised.

Examples

Input:  loan_quotes([((12000, 5), {}), ((-500, 2), {}), ((1000, 2.5), {}), ((1000,), {"years": 1, "rate": 1.2})])
Output: [226.45, "ValueError: monthly_payment: bad value for amount: -500",
         "TypeError: monthly_payment: bad value for years: 2.5",
         "ValueError: monthly_payment: bad value for rate: 1.2"]

Constraints

  • Up to 10**4 calls per test. Rules never raise exceptions themselves.

Goals

  • Write a decorator factory whose settings name the parameters of the decorated function
  • Match positional and keyword arguments to parameter names with `inspect.signature`
  • Fail loudly and precisely on bad input, before the function body runs
Starting Python…