Pythonのwith構文で例外を補足する実例

以前こういう記事を書きました。

withを出る時に呼ばれる__exit__メソッドでは、引数として内部で生じた例外を受け取る事ができることを一応記載しているのですが、どういうシチュエーションでこの機能が使われるのか思い浮かばなかったので適当にはぐらかしていました。

が、Pythonのunittestモジュールで実際に使っていることが分かりましたので、どういう使い方なのか紹介したいと思います。

unittestモジュールのどこで使われているのか

unittestモジュールは、通常unittest.TestCaseクラスを継承する形で利用されます。継承したクラスでは、TestCaseクラスで定義されている各種assertメソッドを利用することが出来るようになります。そのassertメソッドの1つとして、例外が発生するかをテストできるassertRaisesメソッドがあります。

以下は、assertRaisesの利用例です。恐らく3.xでも同様だと思います。

#!/usr/bin/env python2.7
import unittest

def print_not_empty_string(s):
    if type(s) is not str:
        raise TypeError("not str type!")
    if len(s) == 0:
        raise ValueError("length is 0!")
    print s


class PrintTest(unittest.TestCase):
    
    def test_print(self):
        with self.assertRaises(TypeError):
            print_not_empty_string(100)
            
        with self.assertRaises(ValueError):
            print_not_empty_string("")
        
        print_not_empty_string("no error occured.")


if __name__ == "__main__":
    unittest.main()

assertRaisesの仕組み

最初に述べたとおり、withで例外を捕捉しています。

以下は、Python2.7.6における該当部分のソースコードです。

class _AssertRaisesContext(object):
    """A context manager used to implement TestCase.assertRaises* methods."""

    def __init__(self, expected, test_case, expected_regexp=None):
        self.expected = expected
        self.failureException = test_case.failureException
        self.expected_regexp = expected_regexp

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, tb):
        if exc_type is None:
            try:
                exc_name = self.expected.__name__
            except AttributeError:
                exc_name = str(self.expected)
            raise self.failureException(
                "{0} not raised".format(exc_name))
        if not issubclass(exc_type, self.expected):
            # let unexpected exceptions pass through
            return False
        self.exception = exc_value # store for later retrieval
        if self.expected_regexp is None:
            return True

        expected_regexp = self.expected_regexp
        if isinstance(expected_regexp, basestring):
            expected_regexp = re.compile(expected_regexp)
        if not expected_regexp.search(str(exc_value)):
            raise self.failureException('"%s" does not match "%s"' %
                     (expected_regexp.pattern, str(exc_value)))
        return True


class TestCase(object):
    ...

    def assertRaises(self, excClass, callableObj=None, *args, **kwargs):
        """...
        """
        context = _AssertRaisesContext(excClass, self)
        if callableObj is None:
            return context
        with context:
            callableObj(*args, **kwargs)

__exit__に渡る引数は、exc_typeが例外の型、exc_valueが例外オブジェクトです。

_AssertRaisesContext.__exit__では、まずexc_typeが指定された例外型かをチェックしています。その後、not expected_regexp.search(str(exc_value))で値が正しいかをチェックし、結果がFalseなら、self.failureExceptionが呼ばれてテスト失敗となります。

exc_type, exc_valueを無駄なく使っており、withの良い実例だと思います。