serialization.py 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  1. # -*- coding: utf-8 -*-
  2. """Utilities for safely pickling exceptions."""
  3. from __future__ import absolute_import, unicode_literals
  4. import datetime
  5. import numbers
  6. import sys
  7. from base64 import b64encode as base64encode, b64decode as base64decode
  8. from functools import partial
  9. from inspect import getmro
  10. from itertools import takewhile
  11. from kombu.utils.encoding import bytes_to_str, str_to_bytes
  12. from celery.five import (
  13. bytes_if_py2, python_2_unicode_compatible, items, reraise, string_t,
  14. )
  15. from .encoding import safe_repr
  16. try:
  17. import cPickle as pickle
  18. except ImportError:
  19. import pickle # noqa
  20. PY33 = sys.version_info >= (3, 3)
  21. __all__ = [
  22. 'UnpickleableExceptionWrapper', 'subclass_exception',
  23. 'find_pickleable_exception', 'create_exception_cls',
  24. 'get_pickleable_exception', 'get_pickleable_etype',
  25. 'get_pickled_exception', 'strtobool',
  26. ]
  27. #: List of base classes we probably don't want to reduce to.
  28. try:
  29. unwanted_base_classes = (StandardError, Exception, BaseException, object)
  30. except NameError: # pragma: no cover
  31. unwanted_base_classes = (Exception, BaseException, object) # py3k
  32. def subclass_exception(name, parent, module): # noqa
  33. """Create new exception class."""
  34. return type(bytes_if_py2(name), (parent,), {'__module__': module})
  35. def find_pickleable_exception(exc, loads=pickle.loads,
  36. dumps=pickle.dumps):
  37. """Find first pickleable exception base class.
  38. With an exception instance, iterate over its super classes (by MRO)
  39. and find the first super exception that's pickleable. It does
  40. not go below :exc:`Exception` (i.e., it skips :exc:`Exception`,
  41. :class:`BaseException` and :class:`object`). If that happens
  42. you should use :exc:`UnpickleableException` instead.
  43. Arguments:
  44. exc (BaseException): An exception instance.
  45. Returns:
  46. Exception: Nearest pickleable parent exception class
  47. (except :exc:`Exception` and parents), or if the exception is
  48. pickleable it will return :const:`None`.
  49. """
  50. exc_args = getattr(exc, 'args', [])
  51. for supercls in itermro(exc.__class__, unwanted_base_classes):
  52. try:
  53. superexc = supercls(*exc_args)
  54. loads(dumps(superexc))
  55. except Exception: # pylint: disable=broad-except
  56. pass
  57. else:
  58. return superexc
  59. def itermro(cls, stop):
  60. return takewhile(lambda sup: sup not in stop, getmro(cls))
  61. def create_exception_cls(name, module, parent=None):
  62. """Dynamically create an exception class."""
  63. if not parent:
  64. parent = Exception
  65. return subclass_exception(name, parent, module)
  66. @python_2_unicode_compatible
  67. class UnpickleableExceptionWrapper(Exception):
  68. """Wraps unpickleable exceptions.
  69. Arguments:
  70. exc_module (str): See :attr:`exc_module`.
  71. exc_cls_name (str): See :attr:`exc_cls_name`.
  72. exc_args (Tuple[Any, ...]): See :attr:`exc_args`.
  73. Example:
  74. >>> def pickle_it(raising_function):
  75. ... try:
  76. ... raising_function()
  77. ... except Exception as e:
  78. ... exc = UnpickleableExceptionWrapper(
  79. ... e.__class__.__module__,
  80. ... e.__class__.__name__,
  81. ... e.args,
  82. ... )
  83. ... pickle.dumps(exc) # Works fine.
  84. """
  85. #: The module of the original exception.
  86. exc_module = None
  87. #: The name of the original exception class.
  88. exc_cls_name = None
  89. #: The arguments for the original exception.
  90. exc_args = None
  91. def __init__(self, exc_module, exc_cls_name, exc_args, text=None):
  92. safe_exc_args = []
  93. for arg in exc_args:
  94. try:
  95. pickle.dumps(arg)
  96. safe_exc_args.append(arg)
  97. except Exception: # pylint: disable=broad-except
  98. safe_exc_args.append(safe_repr(arg))
  99. self.exc_module = exc_module
  100. self.exc_cls_name = exc_cls_name
  101. self.exc_args = safe_exc_args
  102. self.text = text
  103. Exception.__init__(self, exc_module, exc_cls_name, safe_exc_args, text)
  104. def restore(self):
  105. return create_exception_cls(self.exc_cls_name,
  106. self.exc_module)(*self.exc_args)
  107. def __str__(self):
  108. return self.text
  109. @classmethod
  110. def from_exception(cls, exc):
  111. return cls(exc.__class__.__module__,
  112. exc.__class__.__name__,
  113. getattr(exc, 'args', []),
  114. safe_repr(exc))
  115. def get_pickleable_exception(exc):
  116. """Make sure exception is pickleable."""
  117. try:
  118. pickle.loads(pickle.dumps(exc))
  119. except Exception: # pylint: disable=broad-except
  120. pass
  121. else:
  122. return exc
  123. nearest = find_pickleable_exception(exc)
  124. if nearest:
  125. return nearest
  126. return UnpickleableExceptionWrapper.from_exception(exc)
  127. def get_pickleable_etype(cls, loads=pickle.loads, dumps=pickle.dumps):
  128. """Get pickleable exception type."""
  129. try:
  130. loads(dumps(cls))
  131. except Exception: # pylint: disable=broad-except
  132. return Exception
  133. else:
  134. return cls
  135. def get_pickled_exception(exc):
  136. """Reverse of :meth:`get_pickleable_exception`."""
  137. if isinstance(exc, UnpickleableExceptionWrapper):
  138. return exc.restore()
  139. return exc
  140. def b64encode(s):
  141. return bytes_to_str(base64encode(str_to_bytes(s)))
  142. def b64decode(s):
  143. return base64decode(str_to_bytes(s))
  144. def strtobool(term, table={'false': False, 'no': False, '0': False,
  145. 'true': True, 'yes': True, '1': True,
  146. 'on': True, 'off': False}):
  147. """Convert common terms for true/false to bool.
  148. Examples (true/false/yes/no/on/off/1/0).
  149. """
  150. if isinstance(term, string_t):
  151. try:
  152. return table[term.lower()]
  153. except KeyError:
  154. raise TypeError('Cannot coerce {0!r} to type bool'.format(term))
  155. return term
  156. def _datetime_to_json(dt):
  157. # See "Date Time String Format" in the ECMA-262 specification.
  158. if isinstance(dt, datetime.datetime):
  159. r = dt.isoformat()
  160. if dt.microsecond:
  161. r = r[:23] + r[26:]
  162. if r.endswith('+00:00'):
  163. r = r[:-6] + 'Z'
  164. return r
  165. elif isinstance(dt, datetime.time):
  166. r = dt.isoformat()
  167. if dt.microsecond:
  168. r = r[:12]
  169. return r
  170. else:
  171. return dt.isoformat()
  172. def jsonify(obj,
  173. builtin_types=(numbers.Real, string_t), key=None,
  174. keyfilter=None,
  175. unknown_type_filter=None):
  176. """Transform object making it suitable for json serialization."""
  177. from kombu.abstract import Object as KombuDictType
  178. _jsonify = partial(jsonify, builtin_types=builtin_types, key=key,
  179. keyfilter=keyfilter,
  180. unknown_type_filter=unknown_type_filter)
  181. if isinstance(obj, KombuDictType):
  182. obj = obj.as_dict(recurse=True)
  183. if obj is None or isinstance(obj, builtin_types):
  184. return obj
  185. elif isinstance(obj, (tuple, list)):
  186. return [_jsonify(v) for v in obj]
  187. elif isinstance(obj, dict):
  188. return {
  189. k: _jsonify(v, key=k) for k, v in items(obj)
  190. if (keyfilter(k) if keyfilter else 1)
  191. }
  192. elif isinstance(obj, (datetime.date, datetime.time)):
  193. return _datetime_to_json(obj)
  194. elif isinstance(obj, datetime.timedelta):
  195. return str(obj)
  196. else:
  197. if unknown_type_filter is None:
  198. raise ValueError(
  199. 'Unsupported type: {0!r} {1!r} (parent: {2})'.format(
  200. type(obj), obj, key))
  201. return unknown_type_filter(obj)
  202. # Since PyPy 3 targets Python 3.2, 'raise exc from None' will
  203. # raise a TypeError so we need to look for Python 3.3 or newer
  204. if PY33: # pragma: no cover
  205. from vine.five import exec_
  206. _raise_with_context = None # for flake8
  207. exec_("""def _raise_with_context(exc, ctx): raise exc from ctx""")
  208. def raise_with_context(exc):
  209. exc_info = sys.exc_info()
  210. if not exc_info:
  211. raise exc
  212. elif exc_info[1] is exc:
  213. raise
  214. _raise_with_context(exc, exc_info[1])
  215. else:
  216. def raise_with_context(exc):
  217. exc_info = sys.exc_info()
  218. if not exc_info:
  219. raise exc
  220. if exc_info[1] is exc:
  221. raise
  222. elif exc_info[2]:
  223. reraise(type(exc), exc, exc_info[2])
  224. raise exc