issue #72: WIP

issue72
David Wilson 6 years ago
parent e8ab8d9352
commit 0e98cd4590

@ -34,14 +34,12 @@ mitogen.core
.. decorator:: takes_econtext .. decorator:: takes_econtext
Decorator that marks a function or class method to automatically receive a Decorator that marks a function or class method to automatically receive a
kwarg named `econtext`, referencing the kwarg named `econtext`, referencing the :class:`ExternalContext` active in
:class:`mitogen.core.ExternalContext` active in the context in which the the context in which the function is being invoked. The decorator is only
function is being invoked in. The decorator is only meaningful when the meaningful when the function is invoked via :data:`CALL_FUNCTION`.
function is invoked via :data:`CALL_FUNCTION
<mitogen.core.CALL_FUNCTION>`. No special handling occurs when the function is invoked directly.
When the function is invoked directly, `econtext` must still be passed to
it explicitly.
.. currentmodule:: mitogen.core .. currentmodule:: mitogen.core
.. decorator:: takes_router .. decorator:: takes_router

@ -248,39 +248,43 @@ class CallError(Error):
:py:meth:`Context.call() <mitogen.parent.Context.call>` fails. A copy of :py:meth:`Context.call() <mitogen.parent.Context.call>` fails. A copy of
the traceback from the external context is appended to the exception the traceback from the external context is appended to the exception
message.""" message."""
def __init__(self, fmt=None, *args): #: Fully qualified name of the original exception type, or :data:`None` if
if not isinstance(fmt, BaseException): #: the :class:`CallError` was constructed with a string argument.
Error.__init__(self, fmt, *args) type_name = None
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)
@property @classmethod
def type_name(self): def with_type_name(cls, type_name):
""" return type('CallError_' + type_name, (cls,), {
Return the fully qualified name of the original exception type, or 'type_name': type_name,
:data:`None` if the :class:`CallError` was raised explcitly. })
"""
type_name, sep, _ = self.args[0].partition(':') @classmethod
if sep: def from_exception(cls, e=None):
return type_name 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): 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): if not (type(s) is UnicodeType and len(s) < 10000):
raise TypeError('cannot unpickle CallError: bad input') raise TypeError('cannot unpickle CallError: bad message')
inst = CallError.__new__(CallError) if type_name:
Exception.__init__(inst, s) return CallError.with_type_name(type_name)(s)
return inst return CallError(s)
class ChannelError(Error): class ChannelError(Error):
@ -580,8 +584,7 @@ class Message(object):
try: try:
self.data = pickle.dumps(obj, protocol=2) self.data = pickle.dumps(obj, protocol=2)
except pickle.PicklingError: except pickle.PicklingError:
e = sys.exc_info()[1] self.data = pickle.dumps(CallError.from_exception(), protocol=2)
self.data = pickle.dumps(CallError(e), protocol=2)
return self return self
def reply(self, msg, router=None, **kwargs): def reply(self, msg, router=None, **kwargs):
@ -2013,7 +2016,7 @@ class Dispatcher(object):
try: try:
chain_id, fn, args, kwargs = self._parse_request(msg) chain_id, fn, args, kwargs = self._parse_request(msg)
except Exception: except Exception:
return None, CallError(sys.exc_info()[1]) return None, CallError.from_exception()
if chain_id in self._error_by_chain_id: if chain_id in self._error_by_chain_id:
return chain_id, self._error_by_chain_id[chain_id] return chain_id, self._error_by_chain_id[chain_id]
@ -2021,7 +2024,7 @@ class Dispatcher(object):
try: try:
return chain_id, fn(*args, **kwargs) return chain_id, fn(*args, **kwargs)
except Exception: except Exception:
e = CallError(sys.exc_info()[1]) e = CallError.from_exception()
if chain_id is not None: if chain_id is not None:
self._error_by_chain_id[chain_id] = e self._error_by_chain_id[chain_id] = e
return chain_id, e return chain_id, e

@ -234,5 +234,4 @@ class ContextDebugger(object):
method, args, kwargs = msg.unpickle() method, args, kwargs = msg.unpickle()
msg.reply(getattr(cls, method)(*args, **kwargs)) msg.reply(getattr(cls, method)(*args, **kwargs))
except Exception: except Exception:
e = sys.exc_info()[1] msg.reply(mitogen.core.CallError.from_exception())
msg.reply(mitogen.core.CallError(e))

@ -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

@ -365,8 +365,10 @@ class DeduplicatingInvoker(Invoker):
e = sys.exc_info()[1] e = sys.exc_info()[1]
self._produce_response(key, e) self._produce_response(key, e)
except Exception: except Exception:
e = sys.exc_info()[1] self._produce_response(
self._produce_response(key, mitogen.core.CallError(e)) key,
mitogen.core.CallError.from_exception()
)
return Service.NO_REPLY return Service.NO_REPLY
@ -524,8 +526,7 @@ class Pool(object):
except Exception: except Exception:
LOG.exception('%r: while invoking %r of %r', LOG.exception('%r: while invoking %r of %r',
self, method_name, service_name) self, method_name, service_name)
e = sys.exc_info()[1] msg.reply(mitogen.core.CallError.from_exception())
msg.reply(mitogen.core.CallError(e))
def _worker_run(self): def _worker_run(self):
while not self.closed: while not self.closed:
@ -876,9 +877,7 @@ class FileService(Service):
try: try:
fp = open(path, 'rb', self.IO_SIZE) fp = open(path, 'rb', self.IO_SIZE)
except IOError: except IOError:
msg.reply(mitogen.core.CallError( msg.reply(mitogen.core.CallError.from_exception())
sys.exc_info()[1]
))
return return
# Response must arrive first so requestee can begin receive loop, # Response must arrive first so requestee can begin receive loop,

Loading…
Cancel
Save