diff --git a/.travis.yml b/.travis.yml index 60d4838..3d8f10d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,18 +3,14 @@ dist: xenial # required for Python >= 3.7 language: python python: - - "3.4" - "3.5" - "3.6" - "3.7" - -env: - # - NUMPY_VERSION='numpy==1.13' - - NUMPY_VERSION='numpy' + - "3.8-dev" # command to install dependencies install: - - pip install -q $NUMPY_VERSION + - pip install .[tests] # command to run tests script: diff --git a/setup.py b/setup.py index 41458df..ff892f9 100644 --- a/setup.py +++ b/setup.py @@ -3,8 +3,15 @@ with open("README.md", "r") as fh: long_description = fh.read() +test_requires = ["pytest", "numpy"] + setuptools.setup( - python_requires=">=3.4", + python_requires=">=3.5", + install_requires=["stack_data"], + test_requires=test_requires, + extras_require={ + 'tests': test_requires, + }, name="stackprinter", version="0.2.3", author="cknd", diff --git a/stackprinter/extraction.py b/stackprinter/extraction.py index 936d324..5995560 100644 --- a/stackprinter/extraction.py +++ b/stackprinter/extraction.py @@ -1,193 +1,8 @@ -import types -import inspect -from collections import OrderedDict, namedtuple -from stackprinter.source_inspection import annotate +from stack_data import FrameInfo, Options -NON_FUNCTION_SCOPES = ['', '', ''] - -_FrameInfo = namedtuple('_FrameInfo', - ['filename', 'function', 'lineno', 'source_map', - 'head_lns', 'line2names', 'name2lines', 'assignments']) - -class FrameInfo(_FrameInfo): - # give this namedtuple type a friendlier string representation - def __str__(self): - return ("" % - (self.filename, self.lineno, self.function)) - - -def get_info(tb_or_frame, lineno=None): - """ - Get a frame representation that's easy to format - - - Params - --- - tb: Traceback object or Frame object - - lineno: int (optional) - Override which source line is treated as the important one. For trace- - back objects this defaults to the last executed line (tb.tb_lineno). - For frame objects, it defaults the currently executed one (fr.f_lineno). - - - Returns - --- - FrameInfo, a named tuple with the following fields: - - filename: Path of the executed source file - - function: Name of the scope - - lineno: Highlighted line (last executed line) - - source_map: OrderedDict - Maps line numbers to a list of tokens. Each token is a (string, type) - tuple. Concatenating the first elements of all tokens of all lines - restores the original source, weird whitespaces/indentations and all - (in contrast to python's built-in `tokenize`). However, multiline - statements (those with a trailing backslash) are secretly collapsed - into their first line. - - head_lns: (int, int) or (None, None) - Line numbers of the beginning and end of the function header - - line2names: dict - Maps each line number to a list of variables names that occur there - - name2lines: dict - Maps each variable name to a list of line numbers where it occurs - - assignments: OrderedDict - Holds current values of all variables that occur in the source and - are found in the given frame's locals or globals. Attribute lookups - with dot notation are treated as one variable, so if `self.foo.zup` - occurs in the source, this dict will get a key 'self.foo.zup' that - holds the fully resolved value. - (TODO: it would be easy to return the whole attribute lookup chain, - so maybe just do that & let formatting decide which parts to show?) - (TODO: Support []-lookups just like . lookups) - """ +def get_info(tb_or_frame): if isinstance(tb_or_frame, FrameInfo): return tb_or_frame - if isinstance(tb_or_frame, types.TracebackType): - tb = tb_or_frame - lineno = tb.tb_lineno if lineno is None else lineno - frame = tb.tb_frame - elif isinstance(tb_or_frame, types.FrameType): - frame = tb_or_frame - lineno = frame.f_lineno if lineno is None else lineno - else: - raise ValueError('Cant inspect this: ' + repr(tb_or_frame)) - - filename = inspect.getsourcefile(frame) or inspect.getfile(frame) - function = frame.f_code.co_name - - try: - source, startline = get_source(frame) - # this can be slow (tens of ms) the first time it is called, since - # inspect.get_source internally calls inspect.getmodule, for no - # other purpose than updating the linecache. seems like a bad tradeoff - # for our case, but this is not the time & place to fork `inspect`. - except: - source = [] - startline = lineno - - source_map, line2names, name2lines, head_lns, lineno = annotate(source, startline, lineno) - - if function in NON_FUNCTION_SCOPES: - head_lns = [] - - names = name2lines.keys() - assignments = get_vars(names, frame.f_locals, frame.f_globals) - - finfo = FrameInfo(filename, function, lineno, source_map, head_lns, - line2names, name2lines, assignments) - return finfo - - -def get_source(frame): - """ - get source lines for this frame - - Params - --- - frame : frame object - - Returns - --- - lines : list of str - - startline : int - location of lines[0] in the original source file - """ - - # TODO find out what's faster: Allowing inspect's getsourcelines - # to tokenize the whole file to find the surrounding code block, - # or getting the whole file quickly via linecache & feeding all - # of it to our own instance of tokenize, then clipping to - # desired context afterwards. - - if frame.f_code.co_name in NON_FUNCTION_SCOPES: - lines, _ = inspect.findsource(frame) - startline = 1 - else: - lines, startline = inspect.getsourcelines(frame) - - return lines, startline - - -def get_vars(names, loc, glob): - assignments = [] - for name in names: - try: - val = lookup(name, loc, glob) - except LookupError: - pass - else: - assignments.append((name, val)) - return OrderedDict(assignments) - - -def lookup(name, scopeA, scopeB): - basename, *attr_path = name.split('.') - if basename in scopeA: - val = scopeA[basename] - elif basename in scopeB: - val = scopeB[basename] - else: - # not all names in the source file will be - # defined (yet) when we get to see the frame - raise LookupError(basename) - - for k, attr in enumerate(attr_path): - try: - val = getattr(val, attr) - except Exception as e: - # return a special value in case of lookup errors - # (note: getattr can raise anything, e.g. if a complex - # @property fails). - return UnresolvedAttribute(basename, attr_path, k, val, - e.__class__.__name__, str(e)) - return val - - -class UnresolvedAttribute(): - """ - Container value for failed dot attribute lookups - """ - def __init__(self, basename, attr_path, failure_idx, value, - exc_type, exc_str): - self.basename = basename - self.attr_path = attr_path - self.first_failed = attr_path[failure_idx] - self.failure_idx = failure_idx - self.last_resolvable_value = value - self.exc_type = exc_type - self.exc_str = exc_str - - @property - def last_resolvable_name(self): - return self.basename + '.'.join([''] + self.attr_path[:self.failure_idx]) + return FrameInfo(tb_or_frame, Options(include_signature=True)) diff --git a/stackprinter/formatting.py b/stackprinter/formatting.py index db543a5..64e5c19 100644 --- a/stackprinter/formatting.py +++ b/stackprinter/formatting.py @@ -4,6 +4,8 @@ import types import traceback +import stack_data + import stackprinter.extraction as ex import stackprinter.colorschemes as colorschemes from stackprinter.utils import match, get_ansi_tpl @@ -71,8 +73,7 @@ def format_stack(frames, style='plaintext', source_lines=5, frame_msgs = [] parent_is_boring = True for frame in frames: - fi = ex.get_info(frame) - is_boring = match(fi.filename, suppressed_paths) + is_boring = match(frame.filename, suppressed_paths) if is_boring: if parent_is_boring: formatter = minimal_formatter @@ -82,7 +83,7 @@ def format_stack(frames, style='plaintext', source_lines=5, formatter = verbose_formatter parent_is_boring = is_boring - frame_msgs.append(formatter(fi)) + frame_msgs.append(formatter(frame)) if reverse: frame_msgs = reversed(frame_msgs) @@ -98,12 +99,7 @@ def format_stack_from_frame(fr, add_summary=False, **kwargs): keyword args like stackprinter.format() """ - stack = [] - while fr is not None: - stack.append(fr) - fr = fr.f_back - stack = reversed(stack) - + stack = stack_data.FrameInfo.stack_data(fr, collapse_repeated_frames=False) return format_stack(stack, **kwargs) diff --git a/stackprinter/frame_formatting.py b/stackprinter/frame_formatting.py index 57506aa..7d30d90 100644 --- a/stackprinter/frame_formatting.py +++ b/stackprinter/frame_formatting.py @@ -1,19 +1,22 @@ -import types import os +import token as token_module +import types +from keyword import kwlist -from collections import OrderedDict -import stackprinter.extraction as ex -import stackprinter.source_inspection as sc -import stackprinter.colorschemes as colorschemes +import stack_data +from asttokens.util import is_non_coding_token +import stackprinter.colorschemes as colorschemes +import stackprinter.extraction as ex from stackprinter.prettyprinting import format_value -from stackprinter.utils import inspect_callable, match, trim_source, get_ansi_tpl +from stackprinter.utils import inspect_callable, match, get_ansi_tpl, ansi_color, ansi_reset + class FrameFormatter(): headline_tpl = 'File "%s", line %s, in %s\n' - sourceline_tpl = " %-3s %s" - single_sourceline_tpl = " %s" - marked_sourceline_tpl = "--> %-3s %s" + sourceline_tpl = " %-3s " + single_sourceline_tpl = " " + marked_sourceline_tpl = "--> %-3s " elipsis_tpl = " (...)\n" var_indent = 5 sep_vars = "%s%s" % ((' ') * 4, ('.' * 50)) @@ -117,7 +120,7 @@ def __call__(self, frame, lineno=None): "%s. Got %r" % (accepted_types, frame)) try: - finfo = ex.get_info(frame, lineno) + finfo = ex.get_info(frame) return self._format_frame(finfo) except Exception as exc: @@ -127,129 +130,127 @@ def __call__(self, frame, lineno=None): raise def _format_frame(self, fi): - msg = self.headline_tpl % (fi.filename, fi.lineno, fi.function) + msg = self.headline_tpl % (fi.code.co_filename, fi.lineno, fi.executing.code_qualname()) - source_map, assignments = self.select_scope(fi) + variables = self.variables(fi) + if 1: + msg += self._format_listing(fi.lines) - if source_map: - source_lines = self._format_source(source_map) - msg += self._format_listing(source_lines, fi.lineno) - if assignments: - msg += self._format_assignments(assignments) + if variables: + msg += self._format_assignments(variables) elif self.lines == 'all' or self.lines > 1 or self.show_signature: msg += '\n' return msg - def _format_source(self, source_map): - lines = OrderedDict() - for ln in sorted(source_map): - lines[ln] = ''.join(st for st, _, in source_map[ln]) - return lines + def _format_listing(self, lines, colormap=None, variables=()): + if colormap: + bold_code = ansi_color(*self.colors['source_bold']) + comment_code = ansi_color(*self.colors['source_comment']) - def _format_listing(self, lines, lineno): - ln_prev = None msg = "" n_lines = len(lines) - for ln in sorted(lines): - line = lines[ln] - if ln_prev and ln_prev != ln - 1: + variables = set(variables) + for line in lines: + if line is stack_data.LINE_GAP: msg += self.elipsis_tpl - ln_prev = ln + continue + + if colormap: + def convert_variable_range(r): + var = r.data[0] + if var in colormap: + return ansi_color(*colormap[var]), ansi_reset + + def convert_token_range(r): + typ = r.data.type + if typ == token_module.OP or typ == token_module.NAME and r.data.string in kwlist: + return bold_code, ansi_reset + + if is_non_coding_token(typ): + return comment_code, ansi_reset + + variable_ranges = [ + rang + for rang in line.variable_ranges + if rang.data[0] in variables + ] + + markers = ( + stack_data.markers_from_ranges(variable_ranges, convert_variable_range) + + stack_data.markers_from_ranges(line.token_ranges, convert_token_range) + ) + else: + markers = [] + + text = line.render(markers) + "\n" if n_lines > 1: - if ln == lineno: + if line.is_current: tpl = self.marked_sourceline_tpl else: tpl = self.sourceline_tpl - msg += tpl % (ln, line) + msg += tpl % line.lineno + text else: - msg += self.single_sourceline_tpl % line + msg += self.single_sourceline_tpl + text msg += self.sep_source_below return msg - def _format_assignments(self, assignments): + def _format_assignments(self, variables, colormap=None): msgs = [] - for name, value in assignments.items(): - val_str = format_value(value, - indent=len(name) + self.var_indent + 3, + for variable in variables: + val_str = format_value(variable.value, + indent=len(variable.name) + self.var_indent + 3, truncation=self.truncate_vals) - assign_str = self.val_tpl % (name, val_str) + assign_str = self.val_tpl % (variable.name, val_str) + if colormap: + hue, sat, val, bold = colormap.get(variable, self.colors['var_invisible']) + assign_str = get_ansi_tpl(hue, sat, val, bold) % assign_str msgs.append(assign_str) if len(msgs) > 0: return self.sep_vars + '\n' + ''.join(msgs) + self.sep_vars + '\n\n' else: return '' - def select_scope(self, fi): - """ - decide which lines of code and which variables will be visible - """ - source_lines = [] - minl, maxl = 0, 0 - if len(fi.source_map) > 0: - minl, maxl = min(fi.source_map), max(fi.source_map) - lineno = fi.lineno - - if self.lines == 0: - source_lines = [] - elif self.lines == 1: - source_lines = [lineno] - elif self.lines == 'all': - source_lines = range(minl, maxl + 1) - elif self.lines > 1 or self.lines_after > 0: - start = max(lineno - (self.lines - 1), 0) - stop = lineno + self.lines_after - start = max(start, minl) - stop = min(stop, maxl) - source_lines = list(range(start, stop + 1)) - - if source_lines and self.show_signature: - source_lines = sorted(set(source_lines) | set(fi.head_lns)) - - if source_lines: - # Report a bit more info about a weird class of bug - # that I can't reproduce locally. - if not set(source_lines).issubset(fi.source_map.keys()): - debug_vals = [source_lines, fi.head_lns, fi.source_map.keys()] - info = ', '.join(str(p) for p in debug_vals) - raise Exception("Picked an invalid source context: %s" % info) - trimmed_source_map = trim_source(fi.source_map, source_lines) - else: - trimmed_source_map = {} - - if self.show_vals: - if self.show_vals == 'all': - val_lines = range(minl, maxl) - elif self.show_vals == 'like_source': - val_lines = source_lines - elif self.show_vals == 'line': - val_lines = [lineno] if source_lines else [] - - # TODO refactor the whole blacklistling mechanism below: - - def hide(name): - value = fi.assignments[name] - if callable(value): - qualified_name, path, *_ = inspect_callable(value) - is_builtin = value.__class__.__name__ == 'builtin_function_or_method' - is_boring = is_builtin or (qualified_name == name) - is_suppressed = match(path, self.suppressed_paths) - return is_boring or is_suppressed - return False - - visible_vars = (name for ln in val_lines - for name in fi.line2names[ln] - if name in fi.assignments) - - visible_assignments = OrderedDict([(n, fi.assignments[n]) - for n in visible_vars - if not hide(n)]) + def variables(self, frame_info): + if not self.show_vals: + return [] + + if self.show_vals == 'all': + variables = frame_info.variables + elif self.show_vals == 'like_source': + variables = frame_info.variables_in_lines + elif self.show_vals == 'line': + variables = frame_info.variables_in_executing_piece else: - visible_assignments = {} + raise ValueError("Unknown option " + self.show_vals) + + variables = sorted( + [ + variable + for variable in variables + if not self.hide_variable(variable.name, variable.value) + ], + key=lambda var: min(node.first_token.start for node in var.nodes) + ) - return trimmed_source_map, visible_assignments + return variables + + def hide_variable(self, name, value): + if not callable(value): + return False + + qualified_name, path, *_ = inspect_callable(value) + + is_builtin = value.__class__.__name__ == 'builtin_function_or_method' + is_boring = is_builtin or qualified_name == name + if is_boring: + return True + + is_suppressed = match(path, self.suppressed_paths) + if is_suppressed: + return True class ColorfulFrameFormatter(FrameFormatter): @@ -278,80 +279,36 @@ def tpl(self, name): return get_ansi_tpl(*self.colors[name]) def _format_frame(self, fi): - basepath, filename = os.path.split(fi.filename) + basepath, filename = os.path.split(fi.code.co_filename) sep = os.sep if basepath else '' - msg = self.headline_tpl % (basepath, sep, filename, fi.lineno, fi.function) - source_map, assignments = self.select_scope(fi) + msg = self.headline_tpl % (basepath, sep, filename, fi.lineno, fi.executing.code_qualname()) - colormap = self._pick_colors(source_map, fi.name2lines, assignments, fi.lineno) + colormap = self._pick_colors(fi.variables) - if source_map: - source_lines = self._format_source(source_map, colormap, fi.lineno) - msg += self._format_listing(source_lines, fi.lineno) + variables = self.variables(fi) + if 1: + msg += self._format_listing(fi.lines, colormap, variables) - if assignments: - msg += self._format_assignments(assignments, colormap) + if variables: + msg += self._format_assignments(variables, colormap) elif self.lines == 'all' or self.lines > 1 or self.show_signature: msg += '\n' return msg - def _format_source(self, source_map, colormap, lineno): - bold_tp = self.tpl('source_bold') - default_tpl = self.tpl('source_default') - comment_tpl = self.tpl('source_comment') - - source_lines = OrderedDict() - for ln in source_map: - line = '' - for snippet, ttype in source_map[ln]: - if ttype in [sc.KEYWORD, sc.OP]: - line += bold_tp % snippet - elif ttype == sc.VAR: - if snippet not in colormap: - line += default_tpl % snippet - else: - hue, sat, val, bold = colormap[snippet] - var_tpl = get_ansi_tpl(hue, sat, val, bold) - line += var_tpl % snippet - elif ttype == sc.CALL: - line += bold_tp % snippet - elif ttype == sc.COMMENT: - line += comment_tpl % snippet - else: - line += default_tpl % snippet - source_lines[ln] = line - - return source_lines - - def _format_assignments(self, assignments, colormap): - msgs = [] - for name, value in assignments.items(): - val_str = format_value(value, - indent=len(name) + self.var_indent + 3, - truncation=self.truncate_vals) - assign_str = self.val_tpl % (name, val_str) - hue, sat, val, bold = colormap.get(name, self.colors['var_invisible']) - clr_str = get_ansi_tpl(hue, sat, val, bold) % assign_str - msgs.append(clr_str) - if len(msgs) > 0: - return self.sep_vars + '\n' + ''.join(msgs) + self.sep_vars + '\n\n' - else: - return '' - - def _pick_colors(self, source_map, name2lines, assignments, lineno): + def _pick_colors(self, variables): # TODO refactor: pick a hash for each name across frames, _then_ color. # Currently, colors are consistent across frames purely because there's # a fixed map from hashes to colors. It's not bijective though. If colors # were picked after hashing across all frames, that could be fixed. - colormap = {} - for line in source_map.values(): - for name, ttype in line: - if name not in colormap and ttype == sc.VAR and name in assignments: - value = assignments[name] - highlight = lineno in name2lines[name] - colormap[name] = self._pick_color(name, value, highlight) - return colormap + return { + variable: self._pick_color( + variable.name, + variable.value, + highlight=False, # TODO + ) + for variable in variables + } def _pick_color(self, name, val, highlight=False, method='id'): if method == 'formatted': diff --git a/stackprinter/prettyprinting.py b/stackprinter/prettyprinting.py index 6784a3b..f6e958d 100644 --- a/stackprinter/prettyprinting.py +++ b/stackprinter/prettyprinting.py @@ -1,7 +1,6 @@ import os import pprint -from stackprinter.extraction import UnresolvedAttribute from stackprinter.utils import inspect_callable try: @@ -50,14 +49,6 @@ def format_value(value, indent=0, truncation=None, wrap=60, if depth > max_depth: return '...' - if isinstance(value, UnresolvedAttribute): - reason = "# %s" % (value.exc_type) - val_tpl = reason + "\n%s = %s" - lastval_str = format_value(value.last_resolvable_value, - truncation=truncation, indent=3, depth=depth+1) - val_str = val_tpl % (value.last_resolvable_name, lastval_str) - indent = 10 - elif isinstance(value, (list, tuple, set)): val_str = format_iterable(value, truncation, max_depth, depth) diff --git a/stackprinter/source_inspection.py b/stackprinter/source_inspection.py deleted file mode 100644 index 862b3bf..0000000 --- a/stackprinter/source_inspection.py +++ /dev/null @@ -1,238 +0,0 @@ -import tokenize -import warnings -from keyword import kwlist -from collections import defaultdict - -RAW = 'RAW' -COMMENT = 'COMM' -VAR = 'VAR' -KEYWORD = 'KW' -CALL = 'CALL' -OP = 'OP' - - -def annotate(source_lines, line_offset=0, lineno=0, max_line=1e4): - """ - Find out where in a piece of code which variables live. - - This tokenizes the source, maps out where the variables occur, and, weirdly, - collapses any multiline continuations (i.e. lines ending with a backslash). - - - Params - --- - line_offset: int - line number of the first element of source_lines in the original file - - lineno: int - A line number you're especially interested in. If this line moves around - while treating multiline statements, the corrected nr will be returned. - Otherwise, the given nr will be returned. - - max_line: int - Stop analysing after this many lines - - Returns - --- - source_map: OrderedDict - Maps line numbers to a list of tokens. Each token is a (string, TYPE) - tuple. Concatenating the first elements of all tokens of all lines - restores the original source, weird whitespaces/indentations and all - (in contrast to python's built-in `tokenize`). However, multiline - statements (those with a trailing backslash) are secretly collapsed - into their first line. - - line2names: dict - Maps each line number to a list of variables names that occur there - - name2lines: dict - Maps each variable name to a list of line numbers where it occurs - - head_lns: (int, int) or (None, None) - Line numbers of the beginning and end of the function header - - lineno: int - identical to the supplied argument lineno, unless that line had to be - moved when collapsing a backslash-continued multiline statement. - """ - if not source_lines: - return {}, {}, {}, [], lineno - - source_lines, lineno_corrections = join_broken_lines(source_lines) - lineno += lineno_corrections[lineno - line_offset] - - max_line_relative = min(len(source_lines), max_line-line_offset) - tokens, head_s, head_e = _tokenize(source_lines[:max_line_relative]) - - tokens_by_line = defaultdict(list) - name2lines = defaultdict(list) - line2names = defaultdict(list) - for ttype, string, (sline, scol), (eline, ecol) in tokens: - ln = sline + line_offset - tokens_by_line[ln].append((ttype, scol, ecol, string)) - if ttype == VAR: - name2lines[string].append(ln) - line2names[ln].append(string) - - source_map = {} - for ln, line in enumerate(source_lines): - ln = ln + line_offset - regions = [] - col = 0 - for ttype, tok_start, tok_end, string in tokens_by_line[ln]: - if tok_start > col: - snippet = line[col:tok_start] - regions.append((snippet, RAW)) - col = tok_start - snippet = line[tok_start:tok_end] - if snippet != string: - msg = ("Token %r doesn't match raw source %r" - " in line %s: %r" % (string, snippet, ln, line)) - warnings.warn(msg) - regions.append((snippet, ttype)) - col = tok_end - - if col < len(line): - snippet = line[col:] - regions.append((snippet, RAW)) - - source_map[ln] = regions - - if head_s is not None and head_e is not None: - head_lines = list(range(head_s + line_offset, 1 + head_e + line_offset)) - else: - head_lines = [] - - return source_map, line2names, name2lines, head_lines, lineno - - -def _tokenize(source_lines): - """ - Split a list of source lines into tokens - - Params - --- - source_lines: list of str - - Returns - --- - - list of tokens, each a list of this format: - [TOKENTYPE, 'string', (startline, startcolumn), (endline, endcol)] - - """ - - tokenizer = tokenize.generate_tokens(iter(source_lines).__next__) - # Dragons! This is a trick from the `inspect` standard lib module: Using the - # undocumented method generate_tokens() instead of the official tokenize(), - # since the latter doesn't accept strings (only `readline()`s). The official - # way would be to repackage our list of strings, something like this.. :( - # source = "".join(source_lines) - # source_bytes = BytesIO(source.encode('utf-8')).readline - # tokenizer = tokenize.tokenize(source_bytes) - - tokens = [] - - dot_continuation = False - was_name = False - open_parens = 0 - - head_s = None - head_e = None - name_end = -2 - for ttype, string, (sline, scol), (eline, ecol), line in tokenizer: - sline -= 1 # we deal in line indices counting from 0 - eline -= 1 - if ttype != tokenize.STRING: - assert sline == eline, "Can't accept non-string multiline tokens" - - if ttype == tokenize.NAME: - if string in kwlist: - tokens.append([KEYWORD, string, (sline, scol), (eline, ecol)]) - if head_s is None and string == 'def': - # while we're here, note the start of the call signature - head_s = sline - - elif not dot_continuation: - tokens.append([VAR, string, (sline, scol), (eline, ecol)]) - else: - # this name seems to be part of an attribute lookup, - # which we want to treat as one long name. - prev = tokens[-1] - extended_name = prev[1] + "." + string - old_eline, old_ecol = prev[3] - end_line = max(old_eline, eline) - end_col = max(old_ecol, ecol) - tokens[-1] = [VAR, extended_name, prev[2], (end_line, end_col)] - dot_continuation = False - was_name = True - name_end = ecol - 1 - else: - if string == '.' and was_name and scol == name_end + 1: - dot_continuation = True - continue - elif string == '(': - open_parens += 1 - elif string == ')': - # while we're here, note the end of the call signature. - # the parens counting is necessary because default keyword - # args can contain '(', ')', e.g. in object instantiations. - open_parens -= 1 - if head_e is None and open_parens == 0 and head_s is not None: - head_e = sline - - if ttype == tokenize.OP: - tokens.append([OP, string, (sline, scol), (eline, ecol)]) - if ttype == tokenize.COMMENT: - tokens.append([COMMENT, string, (sline, scol), (eline, ecol)]) - was_name = False - name_end = -2 - - # TODO: proper handling of keyword argument assignments: left hand sides - # should be treated as variables _only_ in the header of the current - # function, and outside of calls, but not when calling other functions... - # this is getting silly. - return tokens, head_s, head_e - - -def join_broken_lines(source_lines): - """ - Collapse backslash-continued lines into the first (upper) line - """ - - # TODO meditate whether this is a good idea - - n_lines = len(source_lines) - unbroken_lines = [] - k = 0 - lineno_corrections = defaultdict(lambda: 0) - while k < n_lines: - line = source_lines[k] - - gobbled_lines = [] - while (line.endswith('\\\n') - and k + 1 < n_lines - and line.lstrip()[0] != '#'): - k_continued = k - k += 1 - nextline = source_lines[k] - nextline_stripped = nextline.lstrip() - line = line[:-2] + nextline_stripped - - indent = '' - n_raw, n_stripped = len(nextline), len(nextline_stripped) - if n_raw != n_stripped: - white_char = nextline[0] - fudge = 3 if white_char == ' ' else 0 - indent = white_char * max(0, (n_raw - n_stripped - fudge)) - - gobbled_lines.append(indent + "\n" ) - lineno_corrections[k] = k_continued - k - - unbroken_lines.append(line) - unbroken_lines.extend(gobbled_lines) - k += 1 - - return unbroken_lines, lineno_corrections - - diff --git a/stackprinter/utils.py b/stackprinter/utils.py index 65d365f..c4b5cef 100644 --- a/stackprinter/utils.py +++ b/stackprinter/utils.py @@ -80,20 +80,19 @@ def trim_source(source_map, context): return trimmed_source_map - -def get_ansi_tpl(hue, sat, val, bold=False): - - # r_, g_, b_ = colorsys.hls_to_rgb(hue, val, sat) +def ansi_color(hue, sat, val, bold=False): r_, g_, b_ = colorsys.hsv_to_rgb(hue, sat, val) r = int(round(r_*5)) g = int(round(g_*5)) b = int(round(b_*5)) point = 16 + 36 * r + 6 * g + b - # print(r,g,b,point) bold_tp = '1;' if bold else '' - code_tpl = ('\u001b[%s38;5;%dm' % (bold_tp, point)) + '%s\u001b[0m' - return code_tpl + return '\u001b[%s38;5;%dm' % (bold_tp, point) + +ansi_reset = '\u001b[0m' +def get_ansi_tpl(*args): + return ansi_color(*args) + '%s' + ansi_reset diff --git a/tests/source.py b/tests/source.py index cb51833..f347c2f 100644 --- a/tests/source.py +++ b/tests/source.py @@ -1,5 +1,4 @@ import numpy as np -import pytest def spam(thing, @@ -74,14 +73,6 @@ def spam_spam_spam(val): T.T) raise Exception('something happened') -#### - -@pytest.fixture -def sourcelines(): - with open(__file__, 'r') as sf: - lines = sf.readlines() - return lines - if __name__ == '__main__': import stackprinter diff --git a/tests/test_formatting.py b/tests/test_formatting.py index 8e8b0d5..30a9c80 100644 --- a/tests/test_formatting.py +++ b/tests/test_formatting.py @@ -1,29 +1,22 @@ +import re + import stackprinter def test_frame_formatting(): """ pin plaintext output """ msg = stackprinter.format() - lines = msg.split('\n') - - expected = ['File "test_formatting.py", line 6, in test_frame_formatting', - ' 4 def test_frame_formatting():', - ' 5 """ pin plaintext output """', - '--> 6 msg = stackprinter.format()', - " 7 lines = msg.split('\\n')", - ' ..................................................', - " stackprinter.format = ", - ' ..................................................', - '', - ''] - - for k, (our_line, expected_line) in enumerate(zip(lines[-len(expected):], expected)): - if k == 0: - assert our_line[-52:] == expected_line[-52:] - elif k == 6: - assert our_line[:58] == expected_line[:58] - else: - assert our_line == expected_line + lines = msg.strip().split('\n') + + expected = [r'File ".+test_formatting\.py", line 8, in test_frame_formatting$', + ' 6 def test_frame_formatting():', + ' 7 """ pin plaintext output """', + '--> 8 msg = stackprinter.format()', + " 9 lines = msg.strip().split('\\n')"] + + lines = lines[-len(expected):] + assert re.match(expected[0], lines[0]) + assert lines[1:] == expected[1:] # for scheme in stackprinter.colorschemes.__all__: # stackprinter.format(style=scheme, suppressed_paths=[r"lib/python.*"]) diff --git a/tests/test_frame_inspection.py b/tests/test_frame_inspection.py deleted file mode 100644 index 7c20a88..0000000 --- a/tests/test_frame_inspection.py +++ /dev/null @@ -1,15 +0,0 @@ -import sys -import pytest -import stackprinter.extraction as ex - -@pytest.fixture -def frameinfo(): - somevalue = 'spam' - fr = sys._getframe() - return ex.get_info(fr) - -def test_frameinfo(frameinfo): - fi = frameinfo - assert fi.filename.endswith('test_frame_inspection.py') - assert fi.function == 'frameinfo' - assert fi.assignments['somevalue'] == 'spam' diff --git a/tests/test_source_inspection.py b/tests/test_source_inspection.py deleted file mode 100644 index 5449fb2..0000000 --- a/tests/test_source_inspection.py +++ /dev/null @@ -1,35 +0,0 @@ -from source import sourcelines -from stackprinter import source_inspection as si - - -def test_source_annotation(sourcelines): - """ """ - line_offset = 23 - source_map, line2names, name2lines, head_lns, lineno = si.annotate(sourcelines, line_offset, 42) - - # see that we didn't forget or invent any lines - assert len(source_map) == len(sourcelines) - - # reassemble the original source, whitespace and all - # (except when we hit the `\`-line continuations at the bottom of the - # file - parsing collapses continued lines, so those can't be reconstructed.) - for k, (true_line, parsed_line) in enumerate(zip(sourcelines, source_map.values())): - if '\\' in true_line: - assert k >= 50 - break - reconstructed_line = ''.join([snippet for snippet, ttype in parsed_line]) - assert reconstructed_line == true_line - - - # see if we found this known token - assert source_map[17 + line_offset][5] == ('lambda', 'KW') - - # see if we found this name - assert "spam_spam_spam" in line2names[17 + line_offset] - assert 17 + line_offset in name2lines["spam_spam_spam"] - - # see if we found this multiline function header - assert head_lns == [k + line_offset for k in [4,5,6,7]] - - # ... and that lineno survived the roundtrip - assert lineno == 42 \ No newline at end of file diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..7957202 --- /dev/null +++ b/tox.ini @@ -0,0 +1,7 @@ +[tox] +envlist = py{35,36,37,38} + +[testenv] +commands = pytest +deps = + .[tests]