Source code for richreports.richreports

"""
Library that supports the construction of human-readable, interactive static
analysis reports that consist of decorated concrete syntax representations of
programs.
"""
from __future__ import annotations
from typing import Union, Tuple
import doctest

[docs]class location(Tuple[int, int]): """ Data structure for representing a location within a report as a tuple of two integers: the line number (where the first line in the report has a line number of ``1``) and the column on that line. """ def __getattribute__(self, name): """ Simulate named attributes for the two components of an instance. >>> l = location((13, 24)) >>> l.line 13 >>> l.column 24 Other attributes should not be affected. >>> str(type(l.__hash__)) "<class 'method-wrapper'>" """ if name == 'line': return self[0] if name == 'column': return self[1] return object.__getattribute__(self, name)
[docs]class report: """ Data structure that represents the raw concrete syntax string as a two-dimensional array of two-sided stacks. Each stack holds delimiters (left and right) that may appear before or after that character in the rendered version of the report. >>> r = report( ... 'def f(x, y):\\n' + ... ' return x + y' ... ) The individual lines in the supplied string can be retrieved via the ``lines`` attribute. >>> list(r.lines) ['def f(x, y):', ' return x + y'] Delimiters can be added around a range within the report by specifying the locations corresponding to the endpoints (inclusive) of the range. >>> r.enrich((2, 11), (2, 15), '(', ')') >>> for line in r.render().split('\\n'): ... print(line) def f(x, y): return (x + y) The optional ``enrich_intermediate_lines`` parameter can be used to delimit all complete lines that appear between the supplied endpoints. >>> r.enrich((1, 0), (2, 15), '<b>', '</b>', True) >>> for line in r.render().split('\\n'): ... print(line) <b>def f(x, y):</b> <b> return (x + y)</b> By default, the ``enrich_intermediate_lines`` parameter is set to ``False``. >>> r.enrich((1, 0), (2, 15), '<div>\\n', '\\n</div>') >>> for line in r.render().split('\\n'): ... print(line) <div> <b>def f(x, y):</b> <b> return (x + y)</b> </div> """ def __init__(self: report, string: str): self.string = string self.lines = string.split('\n') self._stacks = ( [[]] + # Allow line numbers to begin at index ``1``. [ [([], c, []) for c in line] + [([], '', [])] # Allow enrichment of empty lines. for line in self.lines ] )
[docs] def enrich( # pylint: disable=too-many-arguments self: report, start: Union[tuple, location], end: Union[tuple, location], left: str, right: str, enrich_intermediate_lines = False ): """ Add a pair of left and right delimiters around a given range within this report instance. >>> r = report( ... 'def f(x, y):\\n' + ... ' return x + y' ... ) >>> r.enrich((1, 0), (2, 15), '<b>', '</b>', True) >>> for line in r.render().split('\\n'): ... print(line) <b>def f(x, y):</b> <b> return x + y</b> """ # Tuples containing two integers are permitted. start = location(start) end = location(end) # Add the delimiters at the specified positions, and around any # intermediate lines. self._stacks[start.line][start.column][0].append(left) if enrich_intermediate_lines: for line in range(start.line, end.line): self._stacks[line][-1][2].append(right) self._stacks[line + 1][0][0].append(left) self._stacks[end.line][end.column][2].append(right)
[docs] def render(self: report) -> str: """ Return the report (including all delimiters) as a string. >>> r = report( ... 'def f(x, y):\\n' + ... ' return x + y' ... ) >>> for line in r.render().split('\\n'): ... print(line) def f(x, y): return x + y """ return '\n'.join([ ''.join([ ''.join(reversed(pres)) + c + ''.join(posts) for (pres, c, posts) in line ]) for line in self._stacks[1:] ])
if __name__ == '__main__': doctest.testmod() # pragma: no cover