serialization.py 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. # -*- coding: utf-8 -*-
  2. """
  3. celery.utils.serialization
  4. ~~~~~~~~~~~~~~~~~~~~~~~~~~
  5. Utilities for safely pickling exceptions.
  6. """
  7. from __future__ import absolute_import, unicode_literals
  8. from celery.five import bytes_if_py2, python_2_unicode_compatible, string_t
  9. from base64 import b64encode as base64encode, b64decode as base64decode
  10. from inspect import getmro
  11. from itertools import takewhile
  12. try:
  13. import cPickle as pickle
  14. except ImportError:
  15. import pickle # noqa
  16. from kombu.utils.encoding import bytes_to_str, str_to_bytes
  17. from .encoding import safe_repr
  18. __all__ = ['UnpickleableExceptionWrapper', 'subclass_exception',
  19. 'find_pickleable_exception', 'create_exception_cls',
  20. 'get_pickleable_exception', 'get_pickleable_etype',
  21. 'get_pickled_exception', 'strtobool']
  22. #: List of base classes we probably don't want to reduce to.
  23. try:
  24. unwanted_base_classes = (StandardError, Exception, BaseException, object)
  25. except NameError: # pragma: no cover
  26. unwanted_base_classes = (Exception, BaseException, object) # py3k
  27. def subclass_exception(name, parent, module): # noqa
  28. return type(bytes_if_py2(name), (parent,), {'__module__': module})
  29. def find_pickleable_exception(exc, loads=pickle.loads,
  30. dumps=pickle.dumps):
  31. """With an exception instance, iterate over its super classes (by MRO)
  32. and find the first super exception that is pickleable. It does
  33. not go below :exc:`Exception` (i.e. it skips :exc:`Exception`,
  34. :class:`BaseException` and :class:`object`). If that happens
  35. you should use :exc:`UnpickleableException` instead.
  36. :param exc: An exception instance.
  37. Will return the nearest pickleable parent exception class
  38. (except :exc:`Exception` and parents), or if the exception is
  39. pickleable it will return :const:`None`.
  40. :rtype :exc:`Exception`:
  41. """
  42. exc_args = getattr(exc, 'args', [])
  43. for supercls in itermro(exc.__class__, unwanted_base_classes):
  44. try:
  45. superexc = supercls(*exc_args)
  46. loads(dumps(superexc))
  47. except:
  48. pass
  49. else:
  50. return superexc
  51. def itermro(cls, stop):
  52. return takewhile(lambda sup: sup not in stop, getmro(cls))
  53. def create_exception_cls(name, module, parent=None):
  54. """Dynamically create an exception class."""
  55. if not parent:
  56. parent = Exception
  57. return subclass_exception(name, parent, module)
  58. @python_2_unicode_compatible
  59. class UnpickleableExceptionWrapper(Exception):
  60. """Wraps unpickleable exceptions.
  61. :param exc_module: see :attr:`exc_module`.
  62. :param exc_cls_name: see :attr:`exc_cls_name`.
  63. :param exc_args: see :attr:`exc_args`
  64. **Example**
  65. .. code-block:: pycon
  66. >>> def pickle_it(raising_function):
  67. ... try:
  68. ... raising_function()
  69. ... except Exception as e:
  70. ... exc = UnpickleableExceptionWrapper(
  71. ... e.__class__.__module__,
  72. ... e.__class__.__name__,
  73. ... e.args,
  74. ... )
  75. ... pickle.dumps(exc) # Works fine.
  76. """
  77. #: The module of the original exception.
  78. exc_module = None
  79. #: The name of the original exception class.
  80. exc_cls_name = None
  81. #: The arguments for the original exception.
  82. exc_args = None
  83. def __init__(self, exc_module, exc_cls_name, exc_args, text=None):
  84. safe_exc_args = []
  85. for arg in exc_args:
  86. try:
  87. pickle.dumps(arg)
  88. safe_exc_args.append(arg)
  89. except Exception:
  90. safe_exc_args.append(safe_repr(arg))
  91. self.exc_module = exc_module
  92. self.exc_cls_name = exc_cls_name
  93. self.exc_args = safe_exc_args
  94. self.text = text
  95. Exception.__init__(self, exc_module, exc_cls_name, safe_exc_args, text)
  96. def restore(self):
  97. return create_exception_cls(self.exc_cls_name,
  98. self.exc_module)(*self.exc_args)
  99. def __str__(self):
  100. return self.text
  101. @classmethod
  102. def from_exception(cls, exc):
  103. return cls(exc.__class__.__module__,
  104. exc.__class__.__name__,
  105. getattr(exc, 'args', []),
  106. safe_repr(exc))
  107. def get_pickleable_exception(exc):
  108. """Make sure exception is pickleable."""
  109. try:
  110. pickle.loads(pickle.dumps(exc))
  111. except Exception:
  112. pass
  113. else:
  114. return exc
  115. nearest = find_pickleable_exception(exc)
  116. if nearest:
  117. return nearest
  118. return UnpickleableExceptionWrapper.from_exception(exc)
  119. def get_pickleable_etype(cls, loads=pickle.loads, dumps=pickle.dumps):
  120. try:
  121. loads(dumps(cls))
  122. except:
  123. return Exception
  124. else:
  125. return cls
  126. def get_pickled_exception(exc):
  127. """Get original exception from exception pickled using
  128. :meth:`get_pickleable_exception`."""
  129. if isinstance(exc, UnpickleableExceptionWrapper):
  130. return exc.restore()
  131. return exc
  132. def b64encode(s):
  133. return bytes_to_str(base64encode(str_to_bytes(s)))
  134. def b64decode(s):
  135. return base64decode(str_to_bytes(s))
  136. def strtobool(term, table={'false': False, 'no': False, '0': False,
  137. 'true': True, 'yes': True, '1': True,
  138. 'on': True, 'off': False}):
  139. """Convert common terms for true/false to bool
  140. (true/false/yes/no/on/off/1/0)."""
  141. if isinstance(term, string_t):
  142. try:
  143. return table[term.lower()]
  144. except KeyError:
  145. raise TypeError('Cannot coerce {0!r} to type bool'.format(term))
  146. return term