[Python学习笔记-013] 一种Python脚本运行轨迹的查看方法

本文利用sys.settrace() + atexit.register()和inspect模块实现了一种Python脚本运行轨迹的查看方法。

 

1. 核心模块 - debug.py

  1 #!/usr/bin/python3
  2 """
  3     debug module for Python programming
  4     -----------------------------------
  5     This debug module is for providing ksh-style debugging (by setting a
  6     poweful PS4 and set -x) while doing Python programming.
  7 
  8     Q: How to use this feature?
  9     A: Pretty easy. Add these two-line code to your Python script
 10        e.g.
 11            if __name__ == "__main__":
 12        +       import debug
 13        +       debug.init()
 14            ...
 15 
 16     Note the default debug environment variable is DEBUG, let's just take
 17     it for example,
 18 
 19     1. Turn debug on for all functions
 20        $ export DEBUG=yes
 21 
 22     2. Turn debug off
 23        $ export DEBUG=no
 24 
 25     3. Turn debug on for one function in specific file
 26        $ export DEBUG=foo@foo1.py
 27 
 28     4. Turn debug on for a couple of functions
 29        $ export DEBUG=foo:foo2:foo3
 30 
 31     5. Turn debug on for all functions in some files
 32        $ export DEBUG=@foo1.py:@foo2.py
 33 """
 34 
 35 import sys
 36 import os
 37 import re
 38 import inspect
 39 import atexit
 40 import time
 41 
 42 
 43 class Cstr(str):
 44     """
 45     Cstr(object) -> Colorful string
 46 
 47     Return a colorful string representation of the object.
 48 
 49     0. togray()    : str2gray
 50     1. tored()     : str2red
 51     2. togreen()   : str2green
 52     3. toyellow()  : str2yellow
 53     4. toblue()    : str2blue
 54     5. tomagenta() : str2magenta
 55     6. tocyan()    : str2cyan
 56     7. towhite()   : str2white
 57     8. tonone()    : str2none (color is stripped)
 58 
 59     Examples:
 60     (1) s1 = "Hello World"
 61         s2 = Cstr(s1)
 62         print s2.tored()
 63 
 64     (2) print Cstr("Hello World").tored()
 65 
 66     (3) s1 = Cstr("Hello World").tored()
 67         s2 = Cstr(s1).tonone()
 68     """
 69 
 70     def __init__(self, s=""):
 71         self.s = s
 72 
 73     def setstr(self, s):
 74         self.s = s
 75 
 76     def getstr(self):
 77         return self.s
 78 
 79     def _isatty(self):
 80         s = os.getenv("ISATTY")
 81         if s is None:
 82             s = ""
 83 
 84         if s.upper() == "YES":
 85             return True
 86         if s.upper() == "NO":
 87             return False
 88 
 89         if sys.stdout.isatty() and sys.stderr.isatty():
 90             return True
 91         return False
 92 
 93     def togray(self):
 94         return "\033[1;30m%s\033[m" % self.s if self._isatty() else self.s
 95 
 96     def tored(self):
 97         return "\033[1;31m%s\033[m" % self.s if self._isatty() else self.s
 98 
 99     def togreen(self):
100         return "\033[1;32m%s\033[m" % self.s if self._isatty() else self.s
101 
102     def toyellow(self):
103         return "\033[1;33m%s\033[m" % self.s if self._isatty() else self.s
104 
105     def toblue(self):
106         return "\033[1;34m%s\033[m" % self.s if self._isatty() else self.s
107 
108     def tomagenta(self):
109         return "\033[1;35m%s\033[m" % self.s if self._isatty() else self.s
110 
111     def tocyan(self):
112         return "\033[1;36m%s\033[m" % self.s if self._isatty() else self.s
113 
114     def towhite(self):
115         return "\033[1;37m%s\033[m" % self.s if self._isatty() else self.s
116 
117     def tonone(self):
118         s = self.s
119         s = re.sub(r'\033\[1;3.m', '', s)
120         s = re.sub(r'\033\[m', '', s)
121         s = s.strip('\r\n')
122         return s
123 
124 
125 class Debug(object):
126     """
127     Debug -> Object
128 
129     Customize a callback tracer() which could be used by sys.settrace()
130 
131     NOTE: There are some environment variables which can be reset from shell
132           o DEBUG_UTAG : debug unique tag, default is "__DEBUG__: "
133           o TRACER_RPATH_DEPTH : relative path depth of file, default is 0
134           o TRACER_SLEEP_NSECS : nsecs to sleep befor printing debug info to
135                      stderr, default is 0
136           o TRACER_VERBOSE     : verbose level to print when "return" is
137                      being traced, can be one of 0, 1, 2, and 3,
138                      default is 0
139                      if >= 1, print caller
140                      if >= 2, print caller and locals
141                      if >= 3, print caller, locals and globals
142 
143     Examples:
144     (1) foo = Debug()
145         sys.settrace(foo.tracer)
146 
147     (2) foo = Debug("XXX_DEBUG")
148         sys.settrace(foo.tracer)
149 
150     (3) foo = Debug("DEBUG", True)
151         sys.settrace(foo.tracer)
152     """
153     def __init__(self, env="DEBUG", dumpvars=False):
154         self.debug_envname = env
155         self.debug = "no"
156         self.dumpvars = dumpvars
157 
158         self.debug_utag_envname = "DEBUG_UTAG"
159         self.debug_utag = "__DEBUG__: "
160 
161         self.tracer_rpath_depth_envname = "TRACER_RPATH_DEPTH"
162         self.tracer_sleep_nsecs_envname = "TRACER_SLEEP_NSECS"
163         self.tracer_verbose_envname = "TRACER_VERBOSE"
164         self.tracer_rpath_depth = 0
165         self.tracer_sleep_nsecs = 0
166         self.tracer_verbose = 0
167         self.tracer_outfile = "/tmp/debug.vars"
168         self.tracer_start_time = time.time()
169 
170         s = os.getenv(self.debug_envname)
171         if s is not None and len(s) > 0:
172             self.debug = s
173 
174         s = os.getenv(self.debug_utag_envname)
175         if s is not None and len(s) > 0:
176             self.debug_utag = s
177 
178         s = os.getenv(self.tracer_rpath_depth_envname)
179         if s is not None and len(s) > 0:
180             self.tracer_rpath_depth = int(s.split(':')[-1])
181 
182         s = os.getenv(self.tracer_sleep_nsecs_envname)
183         if s is not None and len(s) > 0:
184             self.tracer_sleep_nsecs = int(s)
185 
186         s = os.getenv(self.tracer_verbose_envname)
187         if s is not None and len(s) > 0:
188             self.tracer_verbose = int(s)
189 
190         if self.dumpvars:
191             with open(self.tracer_outfile, 'w') as fd:
192                 fd.write("%s=%s\n" % (self.debug_envname, self.debug))
193                 fd.write("%s='%s'\n" % (self.debug_utag_envname,
194                                         self.debug_utag))
195                 fd.write("%s=%d\n" % (self.tracer_rpath_depth_envname,
196                                       self.tracer_rpath_depth))
197                 fd.write("%s=%d\n" % (self.tracer_sleep_nsecs_envname,
198                                       self.tracer_sleep_nsecs))
199                 fd.write("%s=%d\n" % (self.tracer_verbose_envname,
200                                       self.tracer_verbose))
201 
202     def _debug_query(self, funcname="", filename=""):
203         """
204         1. If debug is in ['no', 'off', 'none'], return False
205         2. If debug is in ['yes', 'on', 'all'],  return True
206         3. for obj in funcname@filename funcname @filename:
207                if obj is in debug.split(':')
208                return True
209         """
210         s = self.debug.lower()
211         if s in ['no', 'none', 'off']:
212             return False
213         if s in ['yes', 'all', 'on']:
214             return True
215 
216         l_obj = ['', '', '']
217         l_obj[0] = funcname + '@' + filename.split('/')[-1]
218         l_obj[1] = funcname
219         l_obj[2] = '@' + filename.split('/')[-1]
220         for obj in l_obj:
221             if obj in self.debug.split(":"):
222                 return True
223 
224         return False
225 
226     def _get_caller(self, frame):
227         prevframe = frame.f_back
228 
229         try:
230             prevlineno = prevframe.f_lineno
231         except:
232             prevlineno = -1
233 
234         try:
235             prevfuncname = prevframe.f_code.co_name
236         except:
237             prevfuncname = "None"
238 
239         try:
240             prevfilename = prevframe.f_code.co_filename
241         except:
242             prevfilename = "None"
243 
244         s = "%s@%s:%d" % (prevfuncname, prevfilename, prevlineno)
245         return s
246 
247     def tracer(self, frame, event, arg):
248         #print Cstr("1:FRAME: " + str(frame)).tocyan()
249         #print Cstr("2:EVENT: " + event).toblue()
250         #print Cstr("3:ARG:   " + str(arg)).tored()
251 
252         #
253         # Reference: https://pymotw.com/2/sys/tracing.html
254         #
255         # NOTE: c_call, c_return, and c_exception are not supported
256         #   to be traced
257         #
258         if event in ["line", "return", "call", "exception"]:
259             seconds = int(time.time() - self.tracer_start_time)
260             lineno = frame.f_lineno
261             funcname = frame.f_code.co_name
262             filename = frame.f_code.co_filename
263 
264             #
265             # For /usr/lib/python*/*.py, always print
266             # its absolute path
267             #
268             if not filename.startswith("/usr/lib"):
269                 n = self.tracer_rpath_depth
270                 filename = '/'.join(filename.split('/')[n:])
271 
272             flag = self._debug_query(funcname, filename)
273             if not flag:
274                 return None
275 
276             try:
277                 thisframe = inspect.getsourcelines(frame)
278                 lineno_base = thisframe[1]
279                 lineno_offset = lineno - lineno_base
280                 lineco = thisframe[0][lineno_offset].rstrip()
281             except:
282                 errmsg = "-- Oops, source code not found --"
283                 lineco = Cstr(errmsg).tomagenta()
284 
285             s = self.debug_utag
286             s += Cstr('[').togray()
287             s += Cstr(funcname).tomagenta() + '()'
288             s += Cstr('@').togray()
289             s += Cstr(filename).toblue()
290             s += Cstr(':').togray()
291             s += Cstr("%d" % lineno).togray()
292             s += Cstr('|').togray()
293             s += Cstr("%d" % seconds).tomagenta()
294             s += Cstr(']').togray()
295             if event == "line":
296                 s += '+ ' + lineco
297 
298             elif event == "return":
299                 s0 = s
300 
301                 rtag = "<RETURN>"
302                 s += "++ %s ++ %s" % \
303                     (Cstr(rtag).tocyan(),
304                      Cstr(str(arg)).tocyan())
305 
306                 if self.tracer_verbose >= 3:
307                     rtag = "<GLOBALS>"
308                     gvars = str(frame.f_globals)
309                     s += '\n' + s0
310                     s += "++ %s ++ %s" % \
311                         (Cstr(rtag).tocyan(),
312                          Cstr(gvars).tocyan())
313 
314                 if self.tracer_verbose >= 2:
315                     rtag = "<LOCALS>"
316                     try:
317                         arg_info = \
318                             inspect.getargvalues(frame)
319                         lvars = str(arg_info.locals)
320                     except:
321                         lvars = ""
322                     s += '\n' + s0
323                     s += "++ %s ++ %s" % \
324                         (Cstr(rtag).tocyan(),
325                          Cstr(lvars).tocyan())
326 
327                 if self.tracer_verbose >= 1:
328                     rtag = "</CALLER>"
329                     sprev = self._get_caller(frame)
330                     s += '\n' + s0
331                     s += "++ %s ++ %s" % \
332                         (Cstr(rtag).tocyan(),
333                          Cstr(sprev).tocyan())
334 
335             elif event == "call":
336                 s0 = s
337 
338                 if self.tracer_verbose >= 1:
339                     rtag = "<CALLER> "
340                     sprev = self._get_caller(frame)
341                     s += "++ %s ++ %s" % \
342                         (Cstr(rtag).tocyan(),
343                          Cstr(sprev).tocyan())
344                     s += '\n' + s0
345 
346                 try:
347                     arg_info = inspect.getargvalues(frame)
348                     funcargs = str(arg_info.locals)
349                 except:
350                     funcargs = ""
351                 fd4 = 'def ' + funcname + funcargs + ':'
352                 s += '++ ' + Cstr(fd4).tocyan()
353 
354             elif event == "exception":
355                 s += '++ ' + Cstr(str(arg)).tored()
356 
357             else:
358                 pass
359 
360             if self.tracer_sleep_nsecs != 0:
361                 time.sleep(self.tracer_sleep_nsecs)
362 
363             sys.stderr.write("%s\n" % s)
364 
365         return self.tracer
366 
367 
368 def init(env="DEBUG", dumpvars=False):
369     atexit.register(fini)
370 
371     dbg = Debug(env, dumpvars)
372     sys.settrace(dbg.tracer)
373 
374 
375 def fini():
376     #
377     # NOTE: If we don't reset sys.settrace(), such an exception is printed
378     #   to stderr once 'call' is traced
379     #
380     #   Exception TypeError: "'NoneType' object is not callable" in \
381     #   <function _remove at 0xb740ecdc> ignored
382     #
383     #   To trace all functions called, try:
384     #   $ python -m trace --listfuncs <foo.py>
385     #   $ python -m trace --trace     <foo.py>
386     #
387     sys.settrace(None)

 

2 - demo程序 - foo.py

 1 #!/usr/bin/python3
 2 
 3 import sys
 4 
 5 
 6 def foo(a, b):
 7     return (a * 10) if a > b else (b * 100)
 8 
 9 
10 def main(argc, argv):
11     if argc != 3:
12         print("Usage: %s <a> <b>" % argv[0], file=sys.stderr)
13         return 1
14 
15     a = int(argv[1])
16     b = int(argv[2])
17     ret = foo(a, b)
18     print(ret)
19 
20     return 0
21 
22 
23 if __name__ == '__main__':
24     import debug
25     debug.init()
26     sys.exit(main(len(sys.argv), sys.argv))

注意:上述脚本插入了关键的两行:

    import debug
    debug.init()

 

3 - 运行效果

 

4 -  小结

在纯函数式编程中,上述运行轨迹查看方法可以使Python脚本执行过程一目了然,不失为一把Python代码辅助阅读的利器。但是,上述方法在实现时未考虑打印类名和异常,故有待进一步增强。

posted @ 2022-03-04 21:24  veli  阅读(390)  评论(0编辑  收藏  举报