diff --git a/pedal/core/formatting.py b/pedal/core/formatting.py index f03d808..2373294 100644 --- a/pedal/core/formatting.py +++ b/pedal/core/formatting.py @@ -2,7 +2,8 @@ Utilities and classes related to formatting a Feedback Message. """ from pedal.core.location import Location -from pedal.utilities.text import inject_line +from pedal.utilities.text import inject_line, render_table + def chomp_spec(format_spec, word): """ Return a new version of format_spec without the ``word`` and @@ -126,10 +127,7 @@ def exception(self, exception): return self.pre(exception) def table(self, rows, columns): - result = [" | ".join(columns)] - for row in rows: - result.append(" | ".join(row)) - return "\n" + ("\n".join(result)) + return "\n" + render_table(rows, columns, " | ") def check_mark(self): # TODO: should be ✓, right? diff --git a/pedal/environments/terminal.py b/pedal/environments/terminal.py index 2728d15..3d8ea74 100644 --- a/pedal/environments/terminal.py +++ b/pedal/environments/terminal.py @@ -13,6 +13,11 @@ from pedal.resolvers.simple import resolve from pedal.core.formatting import Formatter +try: + import tabulate +except ImportError: + tabulate = None + RESET = "\033[0;0m" REVERSE = "\033[;7m" UNDERLINE = "\033[4m" @@ -42,6 +47,12 @@ def filename(self, filename): filename = os.path.basename(filename) return f"{UNDERLINE}{filename}{RESET}" + def table(self, rows, columns): + if tabulate: + return self.pre(tabulate.tabulate(rows, headers=columns, tablefmt="plain")) + else: + return super().table(rows, columns) + @make_resolver def resolve_on_terminal(report=MAIN_REPORT): diff --git a/pedal/utilities/text.py b/pedal/utilities/text.py index 1055c78..b7957d7 100644 --- a/pedal/utilities/text.py +++ b/pedal/utilities/text.py @@ -84,3 +84,35 @@ def inject_line(code, line_number, new_line): lines = code.split("\n") lines.insert(line_number, new_line) return "\n".join(lines) + + +def render_table(rows, headers=None, separator=" | ", max_width=120): + """ + Simple function to render table with fixed width. + + TODO: Try to resize table to make it fit comfortably within 120 characters, + e.g., by removing separators, or by wrapping text. + + Based on: https://stackoverflow.com/a/52247284/1718155 + """ + table = [headers] + rows if headers else rows + longest_cols = [ + (max([len(str(row[i])) for row in table])) + for i in range(len(table[0])) + ] + total_width = sum(longest_cols) + (len(longest_cols)-1) * len(separator) + if max_width is not None: + if total_width > max_width: + # Maybe we can trim the separator? + if separator: + shorter_separator = separator.strip() + # Oops, we already tried stripping. What about removing it? + if shorter_separator == separator: + shorter_separator = "" + return render_table(rows, headers, shorter_separator, max_width) + row_format = separator.join(["{:>" + str(longest_col) + "}" + for longest_col in longest_cols]) + result = [row_format.format(*row) for row in table] + if headers: + result.insert(1, "="*total_width) + return "\n".join(result) diff --git a/tests/test_assertion_functions.py b/tests/test_assertion_functions.py index a942847..c04ccf3 100644 --- a/tests/test_assertion_functions.py +++ b/tests/test_assertion_functions.py @@ -78,12 +78,13 @@ def test_unit_test_partial_credit_true(self): You passed 3/5 tests. I ran your function add on some new arguments. - | Arguments | Returned | Expected -× | 1, 2 | 2 | 3 - | 0, 0 | 0 | 0 - | 0, 3 | 3 | 3 - | 0, 5 | 5 | 5 -× | 1, 3 | 3 | 4""") + | Arguments | Returned | Expected +=================================== +× | 1, 2 | 2 | 3 + | 0, 0 | 0 | 0 + | 0, 3 | 3 | 3 + | 0, 5 | 5 | 5 +× | 1, 3 | 3 | 4""") def test_bad_name_error_concatenation(self): with Execution(""" @@ -125,9 +126,10 @@ def add(a, b): You passed 1/2 tests. I ran your function add on some new arguments. - | Arguments | Returned | Expected -× | 1, 2 | -1 | 3 - | 0, 0 | 0 | 0""") + | Arguments | Returned | Expected +=================================== +× | 1, 2 | -1 | 3 + | 0, 0 | 0 | 0""") def test_unit_test_blocked_function(self): with Execution(""" @@ -142,8 +144,9 @@ def summate(numbers: [int]) -> int: You passed 0/1 tests. I ran your function summate on some new arguments. - | Arguments | Returned | Expected -× | [1, 2, 3] | You are not allowed to call 'sum'. | 6""") + | Arguments | Returned | Expected +================================================================= +× | [1, 2, 3] | You are not allowed to call 'sum'. | 6""") def test_unit_test_extra_context(self): with Execution('def add(a, b): return b', run_tifa=False) as e: @@ -163,9 +166,10 @@ def test_unit_test_extra_context(self): data = [5, 3, 4] I ran your function add on some new arguments. - | Arguments | Returned | Expected -× | data, 5 | 5 | 17 -× | [3, 3], 3 | 3 | 9""") + | Arguments | Returned | Expected +======================================= +× | data, 5 | 5 | 17 +× | [3, 3], 3 | 3 | 9""") def test_unit_test_extra_context_run(self): with Execution('def add(a, b): return b', run_tifa=False) as e: @@ -187,9 +191,10 @@ def test_unit_test_extra_context_run(self): data2 = [1,2,3] I ran your function add on some new arguments. - | Arguments | Returned | Expected -× | data, 5 | 5 | 17 -× | [3, 3], 3 | 3 | 9""") + | Arguments | Returned | Expected +======================================= +× | data, 5 | 5 | 17 +× | [3, 3], 3 | 3 | 9""") def test_unit_test_extra_context_calls(self): with Execution('def add(a, b): return b', run_tifa=False) as e: @@ -214,10 +219,11 @@ def test_unit_test_extra_context_calls(self): data2 = [1,2,3] I ran your function add on some new arguments. - | Arguments | Returned | Expected -× | data, 5 | 5 | 17 -× | data2, 5 | 5 | 17 -× | [3, 3], 3 | 3 | 9""") + | Arguments | Returned | Expected +======================================= +× | data, 5 | 5 | 17 +× | data2, 5 | 5 | 17 +× | [3, 3], 3 | 3 | 9""") if __name__ == '__main__': unittest.main(buffer=False) diff --git a/tests/test_assertions_runtime.py b/tests/test_assertions_runtime.py index 55ccf5b..b7c2363 100644 --- a/tests/test_assertions_runtime.py +++ b/tests/test_assertions_runtime.py @@ -256,10 +256,11 @@ def test_assert_group_fails_some_errors(self): You passed 1/3 tests. I ran your function add on some new arguments. - | Arguments | Returned | Expected - | 1, 2 | 3 | 3 -× | 1, 4 | 5 | 6 -× | 1, '2' | unsupported operand type(s) for +: 'int' and 'str' | 3""") + | Arguments | Returned | Expected +============================================================================== + | 1, 2 | 3 | 3 +× | 1, 4 | 5 | 6 +× | 1, '2' | unsupported operand type(s) for +: 'int' and 'str' | 3""") def test_assert_group_fails_all(self): with Execution('def add(a, b): return a+b', run_tifa=False) as e: @@ -272,10 +273,11 @@ def test_assert_group_fails_all(self): You passed 0/3 tests. I ran your function add on some new arguments. - | Arguments | Returned | Expected -× | 1, 3 | 4 | 3 -× | 1, 4 | 5 | 6 -× | 1, 3 | 4 | 3""") + | Arguments | Returned | Expected +=================================== +× | 1, 3 | 4 | 3 +× | 1, 4 | 5 | 6 +× | 1, 3 | 4 | 3""") def test_assert_group_passes(self): with Execution('def add(a, b): return a+b', run_tifa=False) as e: