Skip to content

Commit 0e2239d

Browse files
authored
Merge pull request #3 from IBM/AppTests
App tests
2 parents 7d90b87 + b40f824 commit 0e2239d

File tree

9 files changed

+493
-43
lines changed

9 files changed

+493
-43
lines changed

.coveragerc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[run]
2+
omit =
3+
scriptit/_version.py

scriptit/app.py

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
class TerminalApp:
3434
__doc__ = __doc__
3535

36+
CONSOLE_START = "== CONSOLE "
37+
3638
## Construction ##############################################################
3739

3840
def __init__(
@@ -77,7 +79,6 @@ def __init__(
7779
self.log_string_output, self.log_file_handle
7880
)
7981
self._wrap_all_logging(preserve_log_handlers)
80-
self._log = logging.getLogger("APP")
8182

8283
# Set up a buffer to store non-log lines in
8384
self.previous_content_entities = []
@@ -111,12 +112,23 @@ def _wrap_all_logging(self, preserve_log_handlers: bool):
111112
HandlerWrapper,
112113
log_stream=self.log_stream,
113114
log_to_wrapped=preserve_log_handlers,
115+
callback=self.refresh,
114116
)
115117

116118
# Update all existing handlers
119+
# NOTE: The choice here to update _all_ handlers is based on the
120+
# assumption that a user will be unlikely to configure multiple
121+
# handlers when running a terminal app. If they do, log lines will end
122+
# up duplicated for each handler. The alternative is to attempt to
123+
# decide _which_ of the multiple handlers should be wrapped, but this
124+
# gets further complicated by needing to handle future handlers, so
125+
# the simpler choice is to just let this be a user problem.
117126
for logger in [logging.root] + list(logging.root.manager.loggerDict.values()):
118-
for i, handler in enumerate(logger.handlers):
119-
logger.handlers[i] = make_wrapped_handler(handler)
127+
if isinstance(logger, logging.PlaceHolder):
128+
continue
129+
if logger.handlers:
130+
for i, handler in enumerate(logger.handlers):
131+
logger.handlers[i] = make_wrapped_handler(handler)
120132

121133
# When new loggers are set up and have handlers directly configured,
122134
# intercept them and wrap the handlers
@@ -151,15 +163,8 @@ def _refresh(self, force, use_previous):
151163
max_log_lines = log_height - 2 # top/bottom frame
152164
content_height = height - log_height
153165

154-
self._log.debug(
155-
"height: %d, log_height: %d, content_height: %d",
156-
height,
157-
log_height,
158-
content_height,
159-
)
160-
161166
# Add the log console
162-
heading = "== CONSOLE "
167+
heading = self.CONSOLE_START
163168
raw_log_lines = filter(
164169
lambda line: bool(line.strip()),
165170
self.log_string_output.getvalue().split("\n"),
@@ -170,6 +175,8 @@ def _refresh(self, force, use_previous):
170175
log_lines.append(line[:width])
171176
line = line[width:]
172177
log_lines.append(line)
178+
# DEBUG
179+
print(log_lines)
173180
self.printer.add(heading + "=" * max(0, width - len(heading)))
174181
for line in log_lines[-max_log_lines:]:
175182
self.printer.add(line)
@@ -226,6 +233,7 @@ def __init__(
226233
wrapped_handler: logging.Handler,
227234
log_stream: TextIO,
228235
log_to_wrapped: bool = False,
236+
callback: Optional[Callable[[], None]] = None,
229237
):
230238
"""Set up with the handler to wrap
231239
@@ -238,6 +246,7 @@ def __init__(
238246
self.wrapped_handler = wrapped_handler
239247
self.log_stream = log_stream
240248
self.log_to_wrapped = log_to_wrapped
249+
self.callback = callback
241250
super().__init__()
242251

243252
# Forward all handler methods to the wrapped handler except those
@@ -265,3 +274,5 @@ def emit(self, record: logging.LogRecord):
265274
self.log_stream.flush()
266275
if self.log_to_wrapped:
267276
self.wrapped_handler.emit(record)
277+
if self.callback:
278+
self.callback()

scriptit/refresh_printer.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@
3838
class RefreshPrinter:
3939
__doc__ = __doc__
4040

41+
UP_LINE = "\033[F"
42+
4143
def __init__(
4244
self,
4345
do_refresh: bool = True,
@@ -90,7 +92,7 @@ def refresh(self, force: bool = False):
9092
width = shutil.get_terminal_size().columns
9193
if force or self.refresh_rate == 1 or self.refreshes % self.refresh_rate == 1:
9294
if self.do_refresh and self.last_report is not None and not self.mute:
93-
line_clear = "\033[F" + " " * width
95+
line_clear = self.UP_LINE + " " * width
9496
self.write_stream.write(
9597
line_clear * (len(self.last_report) + 1) + "\r\n"
9698
)

scriptit/shape.py

Lines changed: 44 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,10 @@ def table(
9898
min_width: Optional[int] = None,
9999
row_dividers: bool = True,
100100
header: bool = True,
101+
hframe_char: str = "-",
102+
vframe_char: str = "|",
103+
corner_char: str = "+",
104+
header_char: str = "=",
101105
) -> str:
102106
"""Encode the given columns as an ascii table
103107
@@ -112,17 +116,32 @@ def table(
112116
computed width based on content
113117
row_dividers (bool): Include dividers between rows
114118
header (bool): Include a special divider between header and rows
119+
hframe_char (str): Single character for horizontal frame lines
120+
vframe_char (str): Single character for vertical frame lines
121+
corner_char (str): Single character for corners
122+
header_char (str): Single character for the header horizontal divider
115123
116124
Returns:
117125
table (str): The formatted table string
118126
"""
127+
# Validate arguments
128+
if any(
129+
len(char) != 1 for char in [hframe_char, vframe_char, corner_char, header_char]
130+
):
131+
raise ValueError("*_char args must be a single character")
132+
if len(set([len(col) for col in columns])) > 1:
133+
raise ValueError("All columns must have equal length")
134+
119135
if max_width is None:
120136
max_width = shutil.get_terminal_size().columns
121137
if min_width is None:
122138
min_width = 2 * len(columns) + 1 if width is None else width
123139

124140
# Stringify all column content
125141
columns = [[str(val) for val in col] for col in columns]
142+
empty_cols = [i for i, col in enumerate(columns) if not any(col)]
143+
if empty_cols:
144+
raise ValueError(f"Found empty column(s) when stringified: {empty_cols}")
126145

127146
# Determine the raw max width of each column
128147
max_col_width = max_width - 3 - 2 * (len(columns) - 1)
@@ -138,55 +157,50 @@ def table(
138157
# For each column, determine the width as a percentage of the total width
139158
pcts = [float(w) / float(total_width) for w in widths]
140159
col_widths = [int(p * usable_table_width) + 3 for p in pcts]
141-
col_widths[-1] = usable_table_width - sum(col_widths[:-1])
142-
143-
# Adjust if possible to compensate for collapsed columns
144-
collapsed = [(i, w) for i, w in enumerate(col_widths) if w - 2 < 2]
145-
extra = sorted(
146-
[(w, i) for i, w in enumerate(col_widths) if (i, w) not in collapsed],
147-
key=lambda x: x[0],
148-
)
149-
for (i, w) in collapsed:
150-
assert len(extra) > 0 and extra[0][0] - w > 2, "No extra to borrow from"
151-
padding = 2 - w
152-
col_widths[extra[0][1]] = extra[0][0] - padding
153-
col_widths[i] = w + padding
154-
extra = sorted(extra, key=lambda x: x[0])
160+
if col_widths:
161+
col_widths[-1] = usable_table_width - sum(col_widths[:-1])
162+
else:
163+
col_widths = [0]
155164

156165
# Prepare the rows
157166
wrapped_cols = []
158167
for i, col in enumerate(columns):
159168
wrapped_cols.append([])
160169
for entry in col:
161-
assert col_widths[i] - 2 > 1, f"Column width collapsed for col {i}"
170+
if col_widths[i] - 2 <= 1:
171+
raise ValueError(f"Column width collapsed for col {i}")
162172
wrapped, _ = _word_wrap_to_len(entry, col_widths[i] - 2)
163173
wrapped_cols[-1].append(wrapped)
164174

165175
# Go row-by-row and add to the output
166-
out = _make_hline(table_width)
167-
n_rows = max([len(col) for col in columns])
176+
out = _make_hline(table_width, char=hframe_char, edge=corner_char)
177+
n_rows = max([len(col) for col in columns]) if columns else 0
178+
if not n_rows:
179+
if header:
180+
out += _make_hline(table_width, char=header_char, edge=vframe_char)
181+
out += _make_hline(table_width, char=hframe_char, edge=corner_char)
168182
for r in range(n_rows):
169183
entries = [col[r] if r < len(col) else [""] for col in wrapped_cols]
170184
most_sublines = max([len(e) for e in entries])
171185
for i in range(most_sublines):
172186
line = ""
173187
for c, entry in enumerate(entries):
174188
val = entry[i] if len(entry) > i else ""
175-
line += "| {}{}".format(
176-
val, " " * (col_widths[c] - _printed_len(val) - 2)
189+
line += "{} {}{}".format(
190+
vframe_char, val, " " * (col_widths[c] - _printed_len(val) - 2)
177191
)
178-
line += "|\n"
192+
line += f"{vframe_char}\n"
179193
out += line
180194
if r == 0:
181195
if header:
182-
out += _make_hline(table_width, char="=", edge="|")
196+
out += _make_hline(table_width, char=header_char, edge=vframe_char)
183197
elif row_dividers:
184-
out += _make_hline(table_width, edge="|")
198+
out += _make_hline(table_width, char=hframe_char, edge=vframe_char)
185199
elif r < n_rows - 1:
186200
if row_dividers:
187-
out += _make_hline(table_width, edge="|")
201+
out += _make_hline(table_width, char=hframe_char, edge=vframe_char)
188202
else:
189-
out += _make_hline(table_width)
203+
out += _make_hline(table_width, char=hframe_char, edge=corner_char)
190204

191205
return out
192206

@@ -211,8 +225,8 @@ def _word_wrap_to_len(line: str, max_len: int) -> Tuple[List[str], int]:
211225
sublines (List[str]): The lines wrapped to the target length
212226
longest (int): The length of the longest wrapped line (<= max_len)
213227
"""
214-
if _printed_len(line) <= max_len:
215-
return [line], _printed_len(line)
228+
if (printed_len := _printed_len(line)) <= max_len:
229+
return [line], printed_len
216230
else:
217231
longest = 0
218232
sublines = []
@@ -231,12 +245,14 @@ def _word_wrap_to_len(line: str, max_len: int) -> Tuple[List[str], int]:
231245
subline += words[0][:cutoff] + "- "
232246
words[0] = words[0][cutoff:]
233247
else:
234-
break
248+
# NOTE: This _is_ covered in tests, but the coverage engine
249+
# doesn't pick it up for some reason!
250+
break # pragma: no cover
235251
subline = subline[:-1]
236252
longest = max(longest, _printed_len(subline))
237253
sublines.append(subline)
238254
return sublines, longest
239255

240256

241-
def _make_hline(table_width: int, char: str = "-", edge: str = "+") -> str:
257+
def _make_hline(table_width: int, char: str, edge: str) -> str:
242258
return "{}{}{}\n".format(edge, char * (table_width - 2), edge)

scripts/run_tests.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ set -e
44
BASE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
55
cd "$BASE_DIR"
66

7-
fail_under=${FAIL_UNDER:-"36.5"}
7+
fail_under=${FAIL_UNDER:-"100"}
88
PYTHONPATH="${BASE_DIR}:$PYTHONPATH" python3 -m pytest \
99
--cov-config=.coveragerc \
1010
--cov=scriptit \

tests/conftest.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
"""
2+
Shared testing utilities
3+
"""
4+
5+
# Standard
6+
import io
7+
8+
9+
class ResettableStringIO(io.StringIO):
10+
"""TextIO that can be reset to an empty buffer for sequential outputs"""
11+
12+
def reset(self):
13+
io.StringIO.__init__(self)

0 commit comments

Comments
 (0)