Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1''' 

2Debugging and profiling tools for Gramex 

3''' 

4import os 

5import gc 

6import six 

7import sys 

8import pprint 

9import timeit 

10import inspect 

11import logging 

12import functools 

13from trace import Trace 

14try: 

15 import line_profiler 

16except ImportError: 

17 line_profiler = None 

18from gramex.config import app_log 

19 

20 

21def _indent(text, prefix, predicate=None): 

22 '''Backport of textwrap.indent for Python 2.7''' 

23 if predicate is None: 

24 def predicate(line): 

25 return line.strip() 

26 

27 def prefixed_lines(): 

28 for line in text.splitlines(True): 

29 yield (prefix + line if predicate(line) else line) 

30 return ''.join(prefixed_lines()) 

31 

32 

33def _caller(): 

34 '''_caller() returns the "file:function:line" of the calling function''' 

35 parent = inspect.getouterframes(inspect.currentframe())[2] 

36 return '[%s:%s:%d]' % (parent[1], parent[3], parent[2]) 

37 

38 

39class Timer(object): 

40 ''' 

41 Find how long a code blocks takes to execute. Wrap any code block like this:: 

42 

43 >>> from gramex.debug import Timer 

44 >>> with Timer('optional message'): 

45 >>> slow_running_code() 

46 WARNING:gramex:1.000s optional message [<file>:<func>:line] 

47 ''' 

48 def __init__(self, msg='', level=logging.WARNING): 

49 self.msg = msg 

50 self.level = logging.WARNING 

51 

52 def __enter__(self): 

53 self.start = timeit.default_timer() 

54 self.gc_old = gc.isenabled() 

55 

56 def __exit__(self, type, value, traceback): 

57 end = timeit.default_timer() 

58 if self.gc_old: 

59 gc.enable() 

60 app_log.log(self.level, '%0.3fs %s %s', end - self.start, self.msg, _caller()) 

61 

62 

63def _write(obj, prefix=None, stream=sys.stdout): 

64 text = pprint.pformat(obj, indent=4) 

65 if prefix is None: 

66 stream.write(_indent(text, ' .. ')) 

67 else: 

68 text = _indent(text, ' .. ' + ' ' * len(prefix) + ' ') 

69 stream.write(' .. ' + prefix + ' = ' + text[7 + len(prefix):]) 

70 stream.write('\n') 

71 

72 

73def print(*args, **kwargs): # noqa 

74 ''' 

75 A replacement for the ``print`` function that also logs the (file, function, 

76 line, msg) from where it is called. For example:: 

77 

78 >>> from gramex.debug import print # import print function 

79 >>> print('hello world') # It works like the print function 

80 <file>(line).<function>: hello world 

81 >>> print(x=1, y=2) # Use kwargs to print variable names 

82 <file>(line).<function>: 

83 .. x = 1 

84 .. y = 2 

85 

86 It automatically pretty-prints complex variables. 

87 ''' 

88 stream = kwargs.pop('stream', sys.stdout) 

89 parent = inspect.getouterframes(inspect.currentframe())[1] 

90 file, line, function = parent[1:4] 

91 if len(args) == 1 and not kwargs: 

92 stream.write('{}({}).{}: {}\n'.format(file, line, function, args[0])) 

93 else: 

94 stream.write('\n{}({}).{}:\n'.format(file, line, function)) 

95 for val in args: 

96 _write(val, stream=stream) 

97 for key, val in kwargs.items(): 

98 _write(val, key, stream=stream) 

99 stream.write('\n') 

100 

101 

102def trace(trace=True, exclude=None, **kwargs): 

103 ''' 

104 Decorator to trace line execution. Usage:: 

105 

106 @trace() 

107 def method(...): 

108 ... 

109 

110 When ``method()`` is called, every line of execution is traced. 

111 ''' 

112 if exclude is None: 

113 ignoredirs = (sys.prefix, ) 

114 elif isinstance(exclude, six.string_types): 

115 ignoredirs = (sys.prefix, os.path.abspath(exclude)) 

116 elif isinstance(exclude, (list, tuple)): 

117 ignoredirs = [sys.prefix] + [os.path.abspath(path) for path in exclude] 

118 tracer = Trace(trace=trace, ignoredirs=ignoredirs, **kwargs) 

119 

120 def decorator(func): 

121 @functools.wraps(func) 

122 def wrapper(*args, **kwargs): 

123 return tracer.runfunc(func, *args, **kwargs) 

124 return wrapper 

125 

126 return decorator 

127 

128 

129if line_profiler is None: 129 ↛ 130line 129 didn't jump to line 130, because the condition on line 129 was never true

130 def lineprofile(func): 

131 app_log.warning('@lineprofile requires line_profiler module') 

132 return func 

133else: 

134 def lineprofile(func): 

135 ''' 

136 A decorator that prints the time taken for each line of a function every 

137 time it is called. This example prints each line's performance:: 

138 

139 >>> from gramex.debug import lineprofile 

140 >>> @lineprofile 

141 >>> def calc(): 

142 >>> ... 

143 ''' 

144 profile = line_profiler.LineProfiler(func) 

145 

146 @functools.wraps(func) 

147 def wrapper(*args, **kwds): 

148 profile.enable_by_count() 

149 try: 

150 result = func(*args, **kwds) 

151 finally: 

152 profile.disable_by_count() 

153 profile.print_stats(stripzeros=True) 

154 return result 

155 return wrapper 

156 

157 

158# Windows 

159if os.name == 'nt': 159 ↛ 172line 159 didn't jump to line 172, because the condition on line 159 was never false

160 import msvcrt 

161 

162 def getch(): 

163 ''' 

164 Return character if something was typed on the console, else None. 

165 Used internally by Gramex on the command line. 

166 ''' 

167 # TODO: flush the buffer 

168 return msvcrt.getch() if msvcrt.kbhit() else None 

169 

170# Posix (Linux, OS X) 

171else: 

172 import sys 

173 import termios 

174 import atexit 

175 from select import select 

176 

177 if sys.__stdin__.isatty(): 

178 def _init_non_blocking_terminal(): 

179 fd = sys.__stdin__.fileno() 

180 old_term = termios.tcgetattr(fd) 

181 # Support normal-terminal reset at exit 

182 atexit.register(lambda: termios.tcsetattr(fd, termios.TCSAFLUSH, old_term)) 

183 

184 # New terminal setting unbuffered 

185 new_term = termios.tcgetattr(fd) 

186 new_term[3] = (new_term[3] & ~termios.ICANON & ~termios.ECHO) 

187 termios.tcsetattr(fd, termios.TCSAFLUSH, new_term) 

188 

189 def getch(): 

190 ''' 

191 Return character if something was typed on the console, else None. 

192 TODO: flush the buffer 

193 ''' 

194 dr, dw, de = select([sys.stdin], [], [], 0) 

195 if dr != []: 

196 return sys.stdin.read(1) 

197 else: 

198 return None 

199 

200 _init_non_blocking_terminal() 

201 

202 else: 

203 def getch(): 

204 return None 

205 

206 

207def _make_timer(): 

208 ''' 

209 ``timer("msg")`` prints the time elapsed since the last timer call:: 

210 

211 >>> from gramex.debug import timer 

212 >>> gramex.debug.timer('abc') 

213 WARNING:gramex:7.583s abc [<file>:<function>:1] # Time since Gramex start 

214 >>> gramex.debug.timer('def') 

215 WARNING:gramex:3.707s def [<file>:<function>:1] # Time since last call 

216 ''' 

217 class Context: 

218 start = timeit.default_timer() 

219 

220 def timer(msg, level=logging.WARNING): 

221 end = timeit.default_timer() 

222 app_log.log(level, '%0.3fs %s %s', end - Context.start, msg, _caller()) 

223 Context.start = end 

224 

225 timer.__doc__ = _make_timer.__doc__ 

226 return timer 

227 

228 

229timer = _make_timer()