Source code for xonsh.tracer

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