serialization.py 8.4 KB

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