Skip to content

Commit

Permalink
Add location operators, whitespace skipping, and adjustability of bas…
Browse files Browse the repository at this point in the history
…e indices.
  • Loading branch information
lapets committed Mar 31, 2024
1 parent 57f4954 commit 0b58147
Show file tree
Hide file tree
Showing 3 changed files with 259 additions and 23 deletions.
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ Examples
^^^^^^^^

.. |report| replace:: ``report``
.. _report: https://richreports.readthedocs.io/en/0.1.0/_source/richreports.html#richreports.richreports.report
.. _report: https://richreports.readthedocs.io/en/0.2.0/_source/richreports.html#richreports.richreports.report

This library supports the enrichment of concrete syntax strings with delimiters. A |report|_ instance can be created from a concrete string and then enriched:

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "richreports"
version = "0.1.0"
version = "0.2.0"
description = """\
Library that supports the construction of human-readable, interactive \
static analysis reports that consist of decorated concrete syntax \
Expand Down
278 changes: 257 additions & 21 deletions src/richreports/richreports.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,32 @@
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.
two integers: the line number and the column on that line.
Because this class is derived from the :obj:`tuple` type, relational
operators can be used to determine whether one location appears before
or after another.
>>> location((12, 24)) < location((13, 24))
True
>>> location((13, 23)) < location((13, 24))
True
>>> location((13, 24)) < location((13, 24))
False
>>> location((14, 0)) < location((13, 0))
False
>>> location((14, 23)) < location((13, 41))
False
>>> location((12, 24)) <= location((13, 24))
True
>>> location((13, 23)) <= location((13, 24))
True
>>> location((13, 24)) <= location((13, 24))
True
>>> location((14, 0)) <= location((13, 0))
False
>>> location((14, 23)) <= location((13, 41))
False
"""
def __getattribute__(self, name):
"""
Expand All @@ -35,6 +59,39 @@ def __getattribute__(self, name):

return object.__getattribute__(self, name)

def __add__(self: location, other: Tuple[int, int]):
"""
Return a later location according to the supplied pair of integers.
The first component must be an integer indicating the amount by which
the line component value should increase. The second component must be
an integerF indicating the amount by which the column component value
should increase.
>>> l = location((13, 24))
>>> l + (3, 0)
(16, 24)
>>> l + (1, -17)
(14, 7)
"""
return location((self[0] + other[0], self[1] + other[1]))

def __sub__(self: location, other: Tuple[int, int]):
"""
Return an earlier location according to the supplied pair of integers.
The first component must be an integer indicating the amount by which
the line component value should decrease. The second component must be
an integer indicating the amount by which the column component value
should decrease.
>>> l = location((13, 24))
>>> l - (3, 0)
(10, 24)
>>> l - (7, 6)
(6, 18)
"""
return location((self[0] - other[0], self[1] - other[1]))

class report:
"""
Data structure that represents the raw concrete syntax string as a
Expand Down Expand Up @@ -65,7 +122,7 @@ def f(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)
>>> r.enrich((1, 0), (2, 15), '<b>', '</b>', enrich_intermediate_lines=True)
>>> for line in r.render().split('\\n'):
... print(line)
<b>def f(x, y):</b>
Expand All @@ -80,8 +137,102 @@ def f(x, y):
<b>def f(x, y):</b>
<b> return (x + y)</b>
</div>
The optional ``skip_whitespace`` parameter (which is set to ``False``
by default) can be used to ensure that left-hand delimiters skip over
whitespace (moving to the right and down) and, likewise, that right-hand
delimiters skip over whitespace (moving to the left and up).
>>> r = report(
... ' \\n' +
... '\\n' +
... ' \\n' +
... ' def f(x, y):\\n' +
... ' return x + y \\n' +
... ' \\n' +
... ' \\n' +
... ' '
... )
>>> r.enrich((2, 0), (5, 20), '<b>', '</b>', skip_whitespace=True)
>>> for line in r.render().split('\\n'):
... print(line)
<BLANKLINE>
<BLANKLINE>
<BLANKLINE>
<b>def f(x, y):
return x + y</b>
<BLANKLINE>
<BLANKLINE>
<BLANKLINE>
If the delimited text consists of whitespace and ``skip_whitespace`` is
``True``, no delimiters are added.
>>> r.enrich((6, 0), (6, 20), '<i>', '</i>', skip_whitespace=True)
>>> r.enrich((1, 0), (1, 3), '<i>', '</i>', skip_whitespace=True)
>>> r.enrich((2, 0), (3, 3), '<i>', '</i>', skip_whitespace=True)
>>> for line in r.render().split('\\n'):
... print(line)
<BLANKLINE>
<BLANKLINE>
<BLANKLINE>
<b>def f(x, y):
return x + y</b>
<BLANKLINE>
<BLANKLINE>
<BLANKLINE>
If ``enrich_intermediate_lines`` and ``skip_whitespace`` are both ``True``,
then individual lines between the first occurrence of a left-hand delimiter
and the last occurrence of a right-hand delimiter are delimited as if each
line was being enriched individually with ``skip_whitespace`` set to
``True``.
>>> r = report(
... ' \\n' +
... '\\n' +
... ' def f(x, y):\\n' +
... ' \\n' +
... '\\n' +
... ' \\n' +
... ' return x + y \\n' +
... '\\n' +
... ' \\n' +
... ' '
... )
>>> r.enrich(
... (1, 3), (10, 20),
... '<b>', '</b>',
... enrich_intermediate_lines=True, skip_whitespace=True
... )
>>> for line in r.render().split('\\n'):
... print(line)
<BLANKLINE>
<BLANKLINE>
<b>def f(x, y):</b>
<BLANKLINE>
<BLANKLINE>
<BLANKLINE>
<b>return x + y</b>
<BLANKLINE>
<BLANKLINE>
<BLANKLINE>
It is possible to specify at what value the line and column numbering
schemes begin by supplying the optional ``line`` and ``column`` arguments
to the instance constructor.
>>> r = report(' def f(x, y):\\n return x + y', line=1, column=0)
>>> r.enrich((1, 0), (2, 20), '<b>', '</b>', skip_whitespace=True)
>>> list(r.render().split('\\n'))
[' <b>def f(x, y):', ' return x + y</b>']
>>> r = report(' def f(x, y):\\n return x + y', line=0, column=0)
>>> r.enrich((0, 0), (1, 20), '<b>', '</b>', skip_whitespace=True)
>>> list(r.render().split('\\n'))
[' <b>def f(x, y):', ' return x + y</b>']
"""
def __init__(self: report, string: str):
def __init__(self: report, string: str, line: int = 1, column: int = 0):
self.string = string
self.lines = string.split('\n')
self._stacks = (
Expand All @@ -92,17 +243,65 @@ def __init__(self: report, string: str):
for line in self.lines
]
)
self.line = line
self.column = column
self._base = location((self.line, self.column))

def _skip_whitespace_left(self: report, location_: location) -> location:
"""
Find the first location (starting from the supplied location and moving
to the right and down) that is not a whitespace character.
"""
(line, column) = location_

if column == 0:
while len(self.lines[line - 1]) == 0:
line += 1

while self._stacks[line][column][1] in (' ', ''):
column += 1
if column == len(self._stacks[line]) - 1:
if line == len(self._stacks) - 1:
break
line += 1
column = 0

if column == 0:
while len(self.lines[line - 1]) == 0:
line += 1

return location((line, column))

def _skip_whitespace_right(self: report, location_: location) -> location:
"""
Find the first location (starting from the supplied location and moving
left and up) that is not a whitespace character.
"""
(line, column) = location_
while self._stacks[line][column][1] in (' ', ''):
column -= 1
if column == -1:
if line == 1:
column = 0
break
line -= 1
column = len(self._stacks[line]) - 1

return location((line, column))

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
start: Union[Tuple[int, int], location],
end: Union[Tuple[int, int], location],
left: str,
right: str,
enrich_intermediate_lines = False,
skip_whitespace = False
):
"""
Add a pair of left and right delimiters around a given range within this
report instance.
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'
Expand All @@ -113,31 +312,68 @@ def enrich( # pylint: disable=too-many-arguments
<b>def f(x, y):</b>
<b> return x + y</b>
"""
# Tuples containing two integers are permitted.
start = location(start)
end = location(end)
# Tuples containing exactly two integers are permitted.
start = location(start) - self._base + location((1, 0))
end = location(end) - self._base + location((1, 0))

if skip_whitespace:
start = self._skip_whitespace_left(start)
end = self._skip_whitespace_right(end)

if start > end:
return

# Add the delimiters at the specified positions, and around any
# intermediate lines.
# Add the delimiters at the specified positions, and (if directed
# to do so) 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)
line = start.line
while line < end.line:
empty = self.lines[line - 1].strip() == ''
if (not skip_whitespace) or (not empty):
column = len(self._stacks[line]) - 1
if skip_whitespace:
while (
self._stacks[line][column][1] in (' ', '')
and
column > 0
and
(line > start.line or column > start.column)
):
column -= 1
self._stacks[line][column][2].append(right)

line += 1

empty = self.lines[line - 1].strip() == ''
if (not skip_whitespace) or (not empty):
column = 0
if skip_whitespace:
while (
self._stacks[line][column][1] in (' ', '')
and
column < len(self._stacks[line]) - 1
and
(line < end.line or column < end.column)
):
column += 1
self._stacks[line][column][0].append(left)

self._stacks[end.line][end.column][2].append(right)

def render(self: report) -> str:
"""
Return the report (including all delimiters) as a string.
Return the report (incorporating all delimiters) as a string.
>>> r = report(
... 'def f(x, y):\\n' +
... ' return x + y'
... )
>>> r.enrich((1, 0), (2, 16), '<b>', '</b>')
>>> for line in r.render().split('\\n'):
... print(line)
def f(x, y):
return x + y
<b>def f(x, y):
return x + y</b>
"""
return '\n'.join([
''.join([
Expand Down

0 comments on commit 0b58147

Please sign in to comment.