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