issue #72: WIP

issue72
David Wilson 6 years ago
parent e8ab8d9352
commit 0e98cd4590

@ -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
<mitogen.core.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

@ -248,39 +248,43 @@ class CallError(Error):
:py:meth:`Context.call() <mitogen.parent.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

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

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

Loading…
Cancel
Save