diff --git a/a.py b/a.py new file mode 100644 index 00000000..1c7f02e4 --- /dev/null +++ b/a.py @@ -0,0 +1,31 @@ + +import mitogen.core +import mitogen.error + + +@mitogen.main() +def main(router): + ve = ValueError('eep') + ke = KeyError('eep') + + cve = mitogen.core.CallError.from_exception(ve) + kve = mitogen.core.CallError.from_exception(ke) + + print([cve, type(cve)]) + print([kve]) + + mve = mitogen.error.match(ValueError) + assert isinstance(cve, mve) + assert not isinstance(kve, mve) + + print + print + print + print + + + try: + raise cve + except mve: + pass + diff --git a/mitogen/core.py b/mitogen/core.py index 9a525204..0dfeacd6 100644 --- a/mitogen/core.py +++ b/mitogen/core.py @@ -252,11 +252,11 @@ class CallError(Error): #: the :class:`CallError` was constructed with a string argument. type_name = None - @classmethod - def with_type_name(cls, type_name): - return type('CallError_' + type_name, (cls,), { - 'type_name': type_name, - }) + @staticmethod + def for_type_name(type_name): + # Overridden by mitogen.error on import to dynamically produce + # CallError subclasses reflecting the original exception hierarchy. + return CallError @classmethod def from_exception(cls, e=None): @@ -269,7 +269,7 @@ class CallError(Error): if tb: s += '\n' s += ''.join(traceback.format_tb(tb)) - return cls.with_type_name(type_name)(s) + return cls.for_type_name(type_name)(s) def __reduce__(self): return (_unpickle_call_error, (self.type_name, self.args[0],)) @@ -283,7 +283,7 @@ def _unpickle_call_error(type_name, s): if not (type(s) is UnicodeType and len(s) < 10000): raise TypeError('cannot unpickle CallError: bad message') if type_name: - return CallError.with_type_name(type_name)(s) + return CallError.for_type_name(type_name)(s) return CallError(s) diff --git a/mitogen/error.py b/mitogen/error.py index b4d18252..8ce00c2e 100644 --- a/mitogen/error.py +++ b/mitogen/error.py @@ -34,62 +34,54 @@ parent class in the hierarchy of the the supplied Exception type. """ from __future__ import absolute_import +import sys + import mitogen.core -#: Map Exception class -> Matcher class. -_matcher_by_cls = {} +#: Map Exception class -> dynamic CallError subclass. +_error_by_cls = {} -def get_matching_classes(cls): - """ - Given a class, return a list containing it and any base classes, operating - recursively such that the returned list contains the entire hierarchy of - `cls`. - """ - classes = [cls] - for subcls in cls.__bases__: - classes.extend(get_matching_classes(subcls)) - return classes - - -class MatcherMeta(type): - def __subclasscheck__(matcher_cls, cls): - print('lol2') - return ( - issubclass(cls, mitogen.core.CallError) and - (cls.type_name in matcher_cls.type_names) - ) - def __instancecheck__(matcher_cls, e): - print('lol') - return MatcherMeta.__subclasscheck__(matcher_cls, type(e)) +def find_class_by_qualname(s): + modname, sep, classname = s.rpartition('.') + if not sep: + return None + module = sys.modules.get(modname) + if module is None: + return None -__metaclass__ = MatcherMeta + return getattr(module, classname, None) -class Matcher: - """ - This class serves only as a placeholder for its relationship with - :meth:`MatcherMeta.__instancecheck__` where the magic happens. A - dynamically generated Matcher subclass is returned by :func:`match`, to - served only for use with isinstance() internally by the Python exception - handling implementation:: - - # Create a dynamic subclass of Matcher to match mitogen.core.CallError - # instances according to the type of the original exception. - matcher_cls = mitogen.error.match(ValueError) - try: - context.call(func_raising_some_exc) +def for_type(cls): + try: + return _error_by_cls[cls] + except KeyError: + pass - # Here Python calls type(matcher_class).__instancecheck__(e): - except matcher_cls as e: - # e remains bound to the CallError as before. - pass - """ - #: Overridden by subclasses generated by :func:`match`. - classes = frozenset([]) + bases = tuple(for_type(c) for c in cls.__bases__ + if c is not object) + + type_name = mitogen.core.qualname(cls) + klass = type('CallError_' + type_name, bases, { + 'type_name': type_name, + }) + _error_by_cls[cls] = klass + print [klass, klass.__bases__] + return klass + + + +def for_type_name(type_name): + cls = find_class_by_qualname(type_name) + if cls: + return for_type(cls) + return mitogen.core.CallError + +mitogen.core.CallError.for_type_name = staticmethod(for_type_name) def match(target_cls): @@ -110,6 +102,7 @@ def match(target_cls): :returns: :class:`Matcher` subclass. """ + return for_type(target_cls) try: return _matcher_by_cls[target_cls] except KeyError: diff --git a/tests/error_test.py b/tests/error_test.py new file mode 100644 index 00000000..0459cbb0 --- /dev/null +++ b/tests/error_test.py @@ -0,0 +1,57 @@ + +import unittest2 + +import mitogen.core +import mitogen.error + +import testlib + + +class MagicValueError(ValueError): + pass + + +def func_throws_value_error(*args): + raise ValueError(*args) + + +def func_throws_value_error_subclass(*args): + raise MagicValueError(*args) + + +class MatchTest(testlib.RouterMixin, testlib.TestCase): + def _test_no_match(self): + context = self.router.fork() + try: + context.call(func_throws_value_error_subclass) + except mitogen.error.match(ValueError): + pass + + def test_no_match(self): + self.assertRaises(mitogen.core.CallError, + lambda: self._test_no_match()) + + def test_builtin_match(self): + context = self.router.fork() + try: + context.call(func_throws_value_error) + except mitogen.error.match(ValueError): + pass + + def test_direct_custom_match(self): + context = self.router.fork() + try: + context.call(func_throws_value_error_subclass) + except mitogen.error.match(MagicValueError): + pass + + def test_indirect_match(self): + context = self.router.fork() + try: + context.call(func_throws_value_error_subclass) + except mitogen.error.match(ValueError): + pass + + +if __name__ == '__main__': + unittest2.main()