"""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",
)
[docs]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)
[docs] 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
[docs] 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
[docs] 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
[docs] 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}}"
#
# 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,
}
[docs]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)