1"""Implements a xonsh tracer.""" 2import os 3import re 4import sys 5import inspect 6import argparse 7import linecache 8import importlib 9import functools 10 11from xonsh.lazyasd import LazyObject 12from xonsh.platform import HAS_PYGMENTS 13from xonsh.tools import DefaultNotGiven, print_color, normabspath, to_bool 14from xonsh.inspectors import find_file, getouterframes 15from xonsh.lazyimps import pygments, pyghooks 16from xonsh.proc import STDOUT_CAPTURE_KINDS 17import xonsh.prompt.cwd as prompt 18 19terminal = LazyObject( 20 lambda: importlib.import_module("pygments.formatters.terminal"), 21 globals(), 22 "terminal", 23) 24 25 26class TracerType(object): 27 """Represents a xonsh tracer object, which keeps track of all tracing 28 state. This is a singleton. 29 """ 30 31 _inst = None 32 valid_events = frozenset(["line", "call"]) 33 34 def __new__(cls, *args, **kwargs): 35 if cls._inst is None: 36 cls._inst = super(TracerType, cls).__new__(cls, *args, **kwargs) 37 return cls._inst 38 39 def __init__(self): 40 self.prev_tracer = DefaultNotGiven 41 self.files = set() 42 self.usecolor = True 43 self.lexer = pyghooks.XonshLexer() 44 self.formatter = terminal.TerminalFormatter() 45 self._last = ("", -1) # filename, lineno tuple 46 47 def __del__(self): 48 for f in set(self.files): 49 self.stop(f) 50 51 def color_output(self, usecolor): 52 """Specify whether or not the tracer output should be colored.""" 53 # we have to use a function to set usecolor because of the way that 54 # lazyasd works. Namely, it cannot dispatch setattr to the target 55 # object without being unable to access its own __dict__. This makes 56 # setting an attr look like getting a function. 57 self.usecolor = usecolor 58 59 def start(self, filename): 60 """Starts tracing a file.""" 61 files = self.files 62 if len(files) == 0: 63 self.prev_tracer = sys.gettrace() 64 files.add(normabspath(filename)) 65 sys.settrace(self.trace) 66 curr = inspect.currentframe() 67 for frame, fname, *_ in getouterframes(curr, context=0): 68 if normabspath(fname) in files: 69 frame.f_trace = self.trace 70 71 def stop(self, filename): 72 """Stops tracing a file.""" 73 filename = normabspath(filename) 74 self.files.discard(filename) 75 if len(self.files) == 0: 76 sys.settrace(self.prev_tracer) 77 curr = inspect.currentframe() 78 for frame, fname, *_ in getouterframes(curr, context=0): 79 if normabspath(fname) == filename: 80 frame.f_trace = self.prev_tracer 81 self.prev_tracer = DefaultNotGiven 82 83 def trace(self, frame, event, arg): 84 """Implements a line tracing function.""" 85 if event not in self.valid_events: 86 return self.trace 87 fname = find_file(frame) 88 if fname in self.files: 89 lineno = frame.f_lineno 90 curr = (fname, lineno) 91 if curr != self._last: 92 line = linecache.getline(fname, lineno).rstrip() 93 s = tracer_format_line( 94 fname, 95 lineno, 96 line, 97 color=self.usecolor, 98 lexer=self.lexer, 99 formatter=self.formatter, 100 ) 101 print_color(s) 102 self._last = curr 103 return self.trace 104 105 106tracer = LazyObject(TracerType, globals(), "tracer") 107 108COLORLESS_LINE = "{fname}:{lineno}:{line}" 109COLOR_LINE = "{{PURPLE}}{fname}{{BLUE}}:" "{{GREEN}}{lineno}{{BLUE}}:" "{{NO_COLOR}}" 110 111 112def tracer_format_line(fname, lineno, line, color=True, lexer=None, formatter=None): 113 """Formats a trace line suitable for printing.""" 114 fname = min(fname, prompt._replace_home(fname), os.path.relpath(fname), key=len) 115 if not color: 116 return COLORLESS_LINE.format(fname=fname, lineno=lineno, line=line) 117 cline = COLOR_LINE.format(fname=fname, lineno=lineno) 118 if not HAS_PYGMENTS: 119 return cline + line 120 # OK, so we have pygments 121 tokens = pyghooks.partial_color_tokenize(cline) 122 lexer = lexer or pyghooks.XonshLexer() 123 tokens += pygments.lex(line, lexer=lexer) 124 if tokens[-1][1] == "\n": 125 del tokens[-1] 126 elif tokens[-1][1].endswith("\n"): 127 tokens[-1] = (tokens[-1][0], tokens[-1][1].rstrip()) 128 return tokens 129 130 131# 132# Command line interface 133# 134 135 136def _find_caller(args): 137 """Somewhat hacky method of finding the __file__ based on the line executed.""" 138 re_line = re.compile(r"[^;\s|&<>]+\s+" + r"\s+".join(args)) 139 curr = inspect.currentframe() 140 for _, fname, lineno, _, lines, _ in getouterframes(curr, context=1)[3:]: 141 if lines is not None and re_line.search(lines[0]) is not None: 142 return fname 143 elif ( 144 lineno == 1 and re_line.search(linecache.getline(fname, lineno)) is not None 145 ): 146 # There is a bug in CPython such that getouterframes(curr, context=1) 147 # will actually return the 2nd line in the code_context field, even though 148 # line number is itself correct. We manually fix that in this branch. 149 return fname 150 else: 151 msg = ( 152 "xonsh: warning: __file__ name could not be found. You may be " 153 "trying to trace interactively. Please pass in the file names " 154 "you want to trace explicitly." 155 ) 156 print(msg, file=sys.stderr) 157 158 159def _on(ns, args): 160 """Turns on tracing for files.""" 161 for f in ns.files: 162 if f == "__file__": 163 f = _find_caller(args) 164 if f is None: 165 continue 166 tracer.start(f) 167 168 169def _off(ns, args): 170 """Turns off tracing for files.""" 171 for f in ns.files: 172 if f == "__file__": 173 f = _find_caller(args) 174 if f is None: 175 continue 176 tracer.stop(f) 177 178 179def _color(ns, args): 180 """Manages color action for tracer CLI.""" 181 tracer.color_output(ns.toggle) 182 183 184@functools.lru_cache(1) 185def _tracer_create_parser(): 186 """Creates tracer argument parser""" 187 p = argparse.ArgumentParser( 188 prog="trace", description="tool for tracing xonsh code as it runs." 189 ) 190 subp = p.add_subparsers(title="action", dest="action") 191 onp = subp.add_parser( 192 "on", aliases=["start", "add"], help="begins tracing selected files." 193 ) 194 onp.add_argument( 195 "files", 196 nargs="*", 197 default=["__file__"], 198 help=( 199 'file paths to watch, use "__file__" (default) to select ' 200 "the current file." 201 ), 202 ) 203 off = subp.add_parser( 204 "off", aliases=["stop", "del", "rm"], help="removes selected files fom tracing." 205 ) 206 off.add_argument( 207 "files", 208 nargs="*", 209 default=["__file__"], 210 help=( 211 'file paths to stop watching, use "__file__" (default) to ' 212 "select the current file." 213 ), 214 ) 215 col = subp.add_parser("color", help="output color management for tracer.") 216 col.add_argument( 217 "toggle", type=to_bool, help="true/false, y/n, etc. to toggle color usage." 218 ) 219 return p 220 221 222_TRACER_MAIN_ACTIONS = { 223 "on": _on, 224 "add": _on, 225 "start": _on, 226 "rm": _off, 227 "off": _off, 228 "del": _off, 229 "stop": _off, 230 "color": _color, 231} 232 233 234def tracermain(args=None, stdin=None, stdout=None, stderr=None, spec=None): 235 """Main function for tracer command-line interface.""" 236 parser = _tracer_create_parser() 237 ns = parser.parse_args(args) 238 usecolor = (spec.captured not in STDOUT_CAPTURE_KINDS) and sys.stdout.isatty() 239 tracer.color_output(usecolor) 240 return _TRACER_MAIN_ACTIONS[ns.action](ns, args) 241