debugutils.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. # -*- coding: utf-8 -*-
  2. # Copyright (c) 2013, Mahmoud Hashemi
  3. #
  4. # Redistribution and use in source and binary forms, with or without
  5. # modification, are permitted provided that the following conditions are
  6. # met:
  7. #
  8. # * Redistributions of source code must retain the above copyright
  9. # notice, this list of conditions and the following disclaimer.
  10. #
  11. # * Redistributions in binary form must reproduce the above
  12. # copyright notice, this list of conditions and the following
  13. # disclaimer in the documentation and/or other materials provided
  14. # with the distribution.
  15. #
  16. # * The names of the contributors may not be used to endorse or
  17. # promote products derived from this software without specific
  18. # prior written permission.
  19. #
  20. # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  21. # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  22. # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  23. # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  24. # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  25. # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  26. # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  27. # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  28. # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  29. # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  30. # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  31. """
  32. A small set of utilities useful for debugging misbehaving
  33. applications. Currently this focuses on ways to use :mod:`pdb`, the
  34. built-in Python debugger.
  35. """
  36. import sys
  37. import time
  38. try:
  39. basestring
  40. from repr import Repr
  41. except NameError:
  42. basestring = (str, bytes) # py3
  43. from reprlib import Repr
  44. try:
  45. from .typeutils import make_sentinel
  46. _UNSET = make_sentinel(var_name='_UNSET')
  47. except ImportError:
  48. _UNSET = object()
  49. __all__ = ['pdb_on_signal', 'pdb_on_exception', 'wrap_trace']
  50. def pdb_on_signal(signalnum=None):
  51. """Installs a signal handler for *signalnum*, which defaults to
  52. ``SIGINT``, or keyboard interrupt/ctrl-c. This signal handler
  53. launches a :mod:`pdb` breakpoint. Results vary in concurrent
  54. systems, but this technique can be useful for debugging infinite
  55. loops, or easily getting into deep call stacks.
  56. Args:
  57. signalnum (int): The signal number of the signal to handle
  58. with pdb. Defaults to :mod:`signal.SIGINT`, see
  59. :mod:`signal` for more information.
  60. """
  61. import pdb
  62. import signal
  63. if not signalnum:
  64. signalnum = signal.SIGINT
  65. old_handler = signal.getsignal(signalnum)
  66. def pdb_int_handler(sig, frame):
  67. signal.signal(signalnum, old_handler)
  68. pdb.set_trace()
  69. pdb_on_signal(signalnum) # use 'u' to find your code and 'h' for help
  70. signal.signal(signalnum, pdb_int_handler)
  71. return
  72. def pdb_on_exception(limit=100):
  73. """Installs a handler which, instead of exiting, attaches a
  74. post-mortem pdb console whenever an unhandled exception is
  75. encountered.
  76. Args:
  77. limit (int): the max number of stack frames to display when
  78. printing the traceback
  79. A similar effect can be achieved from the command-line using the
  80. following command::
  81. python -m pdb your_code.py
  82. But ``pdb_on_exception`` allows you to do this conditionally and within
  83. your application. To restore default behavior, just do::
  84. sys.excepthook = sys.__excepthook__
  85. """
  86. import pdb
  87. import sys
  88. import traceback
  89. def pdb_excepthook(exc_type, exc_val, exc_tb):
  90. traceback.print_tb(exc_tb, limit=limit)
  91. pdb.post_mortem(exc_tb)
  92. sys.excepthook = pdb_excepthook
  93. return
  94. _repr_obj = Repr()
  95. _repr_obj.maxstring = 50
  96. _repr_obj.maxother = 50
  97. brief_repr = _repr_obj.repr
  98. # events: call, return, get, set, del, raise
  99. def trace_print_hook(event, label, obj, attr_name,
  100. args=(), kwargs={}, result=_UNSET):
  101. fargs = (event.ljust(6), time.time(), label.rjust(10),
  102. obj.__class__.__name__, attr_name)
  103. if event == 'get':
  104. tmpl = '%s %s - %s - %s.%s -> %s'
  105. fargs += (brief_repr(result),)
  106. elif event == 'set':
  107. tmpl = '%s %s - %s - %s.%s = %s'
  108. fargs += (brief_repr(args[0]),)
  109. elif event == 'del':
  110. tmpl = '%s %s - %s - %s.%s'
  111. else: # call/return/raise
  112. tmpl = '%s %s - %s - %s.%s(%s)'
  113. fargs += (', '.join([brief_repr(a) for a in args]),)
  114. if kwargs:
  115. tmpl = '%s %s - %s - %s.%s(%s, %s)'
  116. fargs += (', '.join(['%s=%s' % (k, brief_repr(v))
  117. for k, v in kwargs.items()]),)
  118. if result is not _UNSET:
  119. tmpl += ' -> %s'
  120. fargs += (brief_repr(result),)
  121. print(tmpl % fargs)
  122. return
  123. def wrap_trace(obj, hook=trace_print_hook,
  124. which=None, events=None, label=None):
  125. """Monitor an object for interactions. Whenever code calls a method,
  126. gets an attribute, or sets an attribute, an event is called. By
  127. default the trace output is printed, but a custom tracing *hook*
  128. can be passed.
  129. Args:
  130. obj (object): New- or old-style object to be traced. Built-in
  131. objects like lists and dicts also supported.
  132. hook (callable): A function called once for every event. See
  133. below for details.
  134. which (str): One or more attribute names to trace, or a
  135. function accepting attribute name and value, and returning
  136. True/False.
  137. events (str): One or more kinds of events to call *hook*
  138. on. Expected values are ``['get', 'set', 'del', 'call',
  139. 'raise', 'return']``. Defaults to all events.
  140. label (str): A name to associate with the traced object
  141. Defaults to hexadecimal memory address, similar to repr.
  142. The object returned is not the same object as the one passed
  143. in. It will not pass identity checks. However, it will pass
  144. :func:`isinstance` checks, as it is a new instance of a new
  145. subtype of the object passed.
  146. """
  147. # other actions: pdb.set_trace, print, aggregate, aggregate_return
  148. # (like aggregate but with the return value)
  149. # TODO: test classmethod/staticmethod/property
  150. # TODO: wrap __dict__ for old-style classes?
  151. if isinstance(which, basestring):
  152. which_func = lambda attr_name, attr_val: attr_name == which
  153. elif callable(getattr(which, '__contains__', None)):
  154. which_func = lambda attr_name, attr_val: attr_name in which
  155. elif which is None or callable(which):
  156. which_func = which
  157. else:
  158. raise TypeError('expected attr name(s) or callable, not: %r' % which)
  159. label = label or hex(id(obj))
  160. if isinstance(events, basestring):
  161. events = [events]
  162. do_get = not events or 'get' in events
  163. do_set = not events or 'set' in events
  164. do_del = not events or 'del' in events
  165. do_call = not events or 'call' in events
  166. do_raise = not events or 'raise' in events
  167. do_return = not events or 'return' in events
  168. def wrap_method(attr_name, func, _hook=hook, _label=label):
  169. def wrapped(*a, **kw):
  170. a = a[1:]
  171. if do_call:
  172. hook(event='call', label=_label, obj=obj,
  173. attr_name=attr_name, args=a, kwargs=kw)
  174. if do_raise:
  175. try:
  176. ret = func(*a, **kw)
  177. except:
  178. if not hook(event='raise', label=_label, obj=obj,
  179. attr_name=attr_name, args=a, kwargs=kw,
  180. result=sys.exc_info()):
  181. raise
  182. else:
  183. ret = func(*a, **kw)
  184. if do_return:
  185. hook(event='return', label=_label, obj=obj,
  186. attr_name=attr_name, args=a, kwargs=kw, result=ret)
  187. return ret
  188. wrapped.__name__ = func.__name__
  189. wrapped.__doc__ = func.__doc__
  190. try:
  191. wrapped.__module__ = func.__module__
  192. except Exception:
  193. pass
  194. try:
  195. if func.__dict__:
  196. wrapped.__dict__.update(func.__dict__)
  197. except Exception:
  198. pass
  199. return wrapped
  200. def __getattribute__(self, attr_name):
  201. ret = type(obj).__getattribute__(obj, attr_name)
  202. if callable(ret): # wrap any bound methods
  203. ret = type(obj).__getattribute__(self, attr_name)
  204. if do_get:
  205. hook('get', label, obj, attr_name, (), {}, result=ret)
  206. return ret
  207. def __setattr__(self, attr_name, value):
  208. type(obj).__setattr__(obj, attr_name, value)
  209. if do_set:
  210. hook('set', label, obj, attr_name, (value,), {})
  211. return
  212. def __delattr__(self, attr_name):
  213. type(obj).__delattr__(obj, attr_name)
  214. if do_del:
  215. hook('del', label, obj, attr_name, (), {})
  216. return
  217. attrs = {}
  218. for attr_name in dir(obj):
  219. try:
  220. attr_val = getattr(obj, attr_name)
  221. except Exception:
  222. continue
  223. if not callable(attr_val) or attr_name in ('__new__',):
  224. continue
  225. elif which_func and not which_func(attr_name, attr_val):
  226. continue
  227. if attr_name == '__getattribute__':
  228. wrapped_method = __getattribute__
  229. elif attr_name == '__setattr__':
  230. wrapped_method = __setattr__
  231. elif attr_name == '__delattr__':
  232. wrapped_method = __delattr__
  233. else:
  234. wrapped_method = wrap_method(attr_name, attr_val)
  235. attrs[attr_name] = wrapped_method
  236. cls_name = obj.__class__.__name__
  237. if cls_name == cls_name.lower():
  238. type_name = 'traced_' + cls_name
  239. else:
  240. type_name = 'Traced' + cls_name
  241. if hasattr(obj, '__mro__'):
  242. bases = (obj.__class__,)
  243. else:
  244. # need new-style class for even basic wrapping of callables to
  245. # work. getattribute won't work for old-style classes of course.
  246. bases = (obj.__class__, object)
  247. trace_type = type(type_name, bases, attrs)
  248. for cls in trace_type.__mro__:
  249. try:
  250. return cls.__new__(trace_type)
  251. except Exception:
  252. pass
  253. raise TypeError('unable to wrap_trace %r instance %r'
  254. % (obj.__class__, obj))
  255. if __name__ == '__main__':
  256. obj = wrap_trace({})
  257. obj['hi'] = 'hello'
  258. obj.fail
  259. import pdb;pdb.set_trace()