diff --git a/docs/api.rst b/docs/api.rst index 72a6b4db..e07e74d9 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -34,14 +34,12 @@ mitogen.core .. decorator:: takes_econtext Decorator that marks a function or class method to automatically receive a - kwarg named `econtext`, referencing the - :class:`mitogen.core.ExternalContext` active in the context in which the - function is being invoked in. The decorator is only meaningful when the - function is invoked via :data:`CALL_FUNCTION - `. - - When the function is invoked directly, `econtext` must still be passed to - it explicitly. + kwarg named `econtext`, referencing the :class:`ExternalContext` active in + the context in which the function is being invoked. The decorator is only + meaningful when the function is invoked via :data:`CALL_FUNCTION`. + + No special handling occurs when the function is invoked directly. + .. currentmodule:: mitogen.core .. decorator:: takes_router diff --git a/mitogen/core.py b/mitogen/core.py index 597e0d90..9a525204 100644 --- a/mitogen/core.py +++ b/mitogen/core.py @@ -248,39 +248,43 @@ class CallError(Error): :py:meth:`Context.call() ` fails. A copy of the traceback from the external context is appended to the exception message.""" - def __init__(self, fmt=None, *args): - if not isinstance(fmt, BaseException): - Error.__init__(self, fmt, *args) - else: - e = fmt - fmt = '%s: %s' % (qualname(type(e)), e) - args = () - tb = sys.exc_info()[2] - if tb: - fmt += '\n' - fmt += ''.join(traceback.format_tb(tb)) - Error.__init__(self, fmt) + #: Fully qualified name of the original exception type, or :data:`None` if + #: the :class:`CallError` was constructed with a string argument. + type_name = None - @property - def type_name(self): - """ - Return the fully qualified name of the original exception type, or - :data:`None` if the :class:`CallError` was raised explcitly. - """ - type_name, sep, _ = self.args[0].partition(':') - if sep: - return type_name + @classmethod + def with_type_name(cls, type_name): + return type('CallError_' + type_name, (cls,), { + 'type_name': type_name, + }) + + @classmethod + def from_exception(cls, e=None): + e_type, e_val, tb = sys.exc_info() + if e is None: + e = e_val + assert e is not None + type_name = qualname(type(e)) + s = '%s: %s' % (type_name, e) + if tb: + s += '\n' + s += ''.join(traceback.format_tb(tb)) + return cls.with_type_name(type_name)(s) def __reduce__(self): - return (_unpickle_call_error, (self.args[0],)) + return (_unpickle_call_error, (self.type_name, self.args[0],)) -def _unpickle_call_error(s): +def _unpickle_call_error(type_name, s): + if not (type_name is None or + (type(type_name) is UnicodeType and + len(type_name) < 100)): + raise TypeError('cannot unpickle CallError: bad type_name') if not (type(s) is UnicodeType and len(s) < 10000): - raise TypeError('cannot unpickle CallError: bad input') - inst = CallError.__new__(CallError) - Exception.__init__(inst, s) - return inst + raise TypeError('cannot unpickle CallError: bad message') + if type_name: + return CallError.with_type_name(type_name)(s) + return CallError(s) class ChannelError(Error): @@ -580,8 +584,7 @@ class Message(object): try: self.data = pickle.dumps(obj, protocol=2) except pickle.PicklingError: - e = sys.exc_info()[1] - self.data = pickle.dumps(CallError(e), protocol=2) + self.data = pickle.dumps(CallError.from_exception(), protocol=2) return self def reply(self, msg, router=None, **kwargs): @@ -2013,7 +2016,7 @@ class Dispatcher(object): try: chain_id, fn, args, kwargs = self._parse_request(msg) except Exception: - return None, CallError(sys.exc_info()[1]) + return None, CallError.from_exception() if chain_id in self._error_by_chain_id: return chain_id, self._error_by_chain_id[chain_id] @@ -2021,7 +2024,7 @@ class Dispatcher(object): try: return chain_id, fn(*args, **kwargs) except Exception: - e = CallError(sys.exc_info()[1]) + e = CallError.from_exception() if chain_id is not None: self._error_by_chain_id[chain_id] = e return chain_id, e diff --git a/mitogen/debug.py b/mitogen/debug.py index 19cf1a89..1d48d769 100644 --- a/mitogen/debug.py +++ b/mitogen/debug.py @@ -234,5 +234,4 @@ class ContextDebugger(object): method, args, kwargs = msg.unpickle() msg.reply(getattr(cls, method)(*args, **kwargs)) except Exception: - e = sys.exc_info()[1] - msg.reply(mitogen.core.CallError(e)) + msg.reply(mitogen.core.CallError.from_exception()) diff --git a/mitogen/error.py b/mitogen/error.py new file mode 100644 index 00000000..b4d18252 --- /dev/null +++ b/mitogen/error.py @@ -0,0 +1,127 @@ +# Copyright 2017, David Wilson +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +""" +This defines :func:`match` that produces dynamic subclasses of a magical type +whose :func:`isinstance` returns :data:`True` if the instance being checked is +a :class:`mitogen.core.CallError` whose original exception type matches a +parent class in the hierarchy of the the supplied Exception type. +""" + +from __future__ import absolute_import +import mitogen.core + + +#: Map Exception class -> Matcher class. +_matcher_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)) + + +__metaclass__ = MatcherMeta + + +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) + + # 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([]) + + +def match(target_cls): + """ + Return a magic for use in :keyword:`except` statements that matches any + :class:`mitogen.core.CallError` whose original exception type was + `target_cls` or one of its base classes:: + + try: + context.call(func_raising_some_exc) + except mitogen.error.match(ValueError) as e: + # handle ValueError. + pass + + :param type target_cls: + Target class to match. + + :returns: + :class:`Matcher` subclass. + """ + try: + return _matcher_by_cls[target_cls] + except KeyError: + name = '%s{%s}' % ( + mitogen.core.qualname(Matcher), + mitogen.core.qualname(target_cls), + ) + matcher_cls = type(name, (Matcher,), { + 'type_names': frozenset( + mitogen.core.qualname(cls) + for cls in get_matching_classes(target_cls) + ) + }) + _matcher_by_cls[target_cls] = matcher_cls + return matcher_cls diff --git a/mitogen/service.py b/mitogen/service.py index ffb7308e..92a522e5 100644 --- a/mitogen/service.py +++ b/mitogen/service.py @@ -365,8 +365,10 @@ class DeduplicatingInvoker(Invoker): e = sys.exc_info()[1] self._produce_response(key, e) except Exception: - e = sys.exc_info()[1] - self._produce_response(key, mitogen.core.CallError(e)) + self._produce_response( + key, + mitogen.core.CallError.from_exception() + ) return Service.NO_REPLY @@ -524,8 +526,7 @@ class Pool(object): except Exception: LOG.exception('%r: while invoking %r of %r', self, method_name, service_name) - e = sys.exc_info()[1] - msg.reply(mitogen.core.CallError(e)) + msg.reply(mitogen.core.CallError.from_exception()) def _worker_run(self): while not self.closed: @@ -876,9 +877,7 @@ class FileService(Service): try: fp = open(path, 'rb', self.IO_SIZE) except IOError: - msg.reply(mitogen.core.CallError( - sys.exc_info()[1] - )) + msg.reply(mitogen.core.CallError.from_exception()) return # Response must arrive first so requestee can begin receive loop,