python3の例外のfromキーワード(前回の続き)

前回の反省を踏まえて書き直しました。

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
関数の引数型を検証します。
"""
from functools import wraps
from inspect import getfullargspec, getmembers, isfunction


class ArgumentError(Exception):
    def __init__(self, arg_name, val, typ):
        err = '引数 {} の {} は {} と型が違います。'.format(arg_name, val, typ)
        super().__init__(err)


class RetValError(Exception):
    def __init__(self, retval, typ):
        err = '戻り値 {} は型が {} ではありません。'.format(retval, typ)
        super().__init__(err)


class UndefinedArgTypeError(KeyError):
    def __init__(self):
        err = '引数の型が未定義です。'
        super().__init__(err)


class UndefinedRetvalTypeError(KeyError):
    def __init__(self):
        err = '戻り値の型が未定義です。'
        super().__init__(err)


def validate(func):
    """
    >>> @validate
    ... def test(a: int, b: str) -> str:
    ...     val = str(a) + b
    ...     return val
    ...
    >>> test(1, "3")
    '13'
    """
    anno = func.__annotations__
    def check_type(key, value):
        try:
            obj_type = anno[key]
        except KeyError as err:
            if key == "return":
                raise UndefinedRetvalTypeError from err
            else:
                raise UndefinedArgTypeError from err

        def raise_error():
            if key == "return":
                raise RetValError(value, obj_type)
            else:
                raise ArgumentError(key, value, obj_type)

        if type(obj_type) in (list, tuple, set):
            if type(value) not in obj_type:
                raise_error()
        else:
            if obj_type is None:
                obj_type = type(obj_type)
            if type(value) is not obj_type:
                raise_error()

    @wraps(func)
    def _validate(*args, **kwargs):
        argSpec = getfullargspec(func)
        for name, value in zip(argSpec.args, args):
            if name == "self":
                continue
            check_type(name, value)
        result = func(*args, **kwargs)
        if func.__name__ != "__init__":
            check_type("return", result)
        return result
    return _validate


def validate_method(cls):
    """
    >>> @validate_method
    ... class Hoge:
    ...     def fuga(self, a: int, b: float) -> float:
    ...         return a + b
    >>> h = Hoge()
    >>> h.fuga(7, 0.777)
    7.777
    """
    for name, func in getmembers(cls, isfunction):
        func.__name__ = name
        func.__doc__ = getattr(cls, name).__doc__
        setattr(cls, name, validate(func))
    return cls


def test():
    import doctest
    doctest.testmod()


if __name__ == "__main__":
    test()

前回の問題点はクラスデコレータ使うとデコったクラスがオブジェクトになってしまうところでこれは何かと問題があります。デコったクラスがクラスでは無くなってしまうので。

ということで普通に関数でデコレートしてクラスはそのまま返すことに。
デコレータの中でメソッドを上書きしてやることにしました。

表題の件はpython3から例外キャッチしたときにfrom句が使えるみたいで、実際例外を出してみるとどうなるかわかります。

>>> from validation import validate
>>> @validate
... def a(b):
...   pass
... 
>>> a(1)
Traceback (most recent call last):
  File "./validation.py", line 47, in check_type
    obj_type = anno[key]
KeyError: 'b'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "./validation.py", line 75, in _validate
    check_type(name, value)
  File "./validation.py", line 52, in check_type
    raise UndefinedArgTypeError from err
validation.UndefinedArgTypeError: '引数の型が未定義です。'

超適当ですがこんな感じです。

The above exception was the direct cause of the following exception:

って出てますね。自作の例外クラス作った時なんかは追いやすくなりそうです。

ちなみにfrom使わないと

During handling of the above exception, another exception occurred:

って出ます。python2ではこのような表示は出ないのでpython3のが便利になっているのがわかります。