From cb380b423ebd23c1899a63c349f6b0764558e9c0 Mon Sep 17 00:00:00 2001 From: "Paul Gauthier (aider)" Date: Thu, 8 May 2025 09:34:31 -0700 Subject: [PATCH 1/8] fix: Use rich Text for filenames in prompt header --- aider/io.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/aider/io.py b/aider/io.py index 90f581aab..f28a1c86d 100644 --- a/aider/io.py +++ b/aider/io.py @@ -1144,18 +1144,19 @@ class InputOutput: ro_paths = [] for rel_path in read_only_files: abs_path = os.path.abspath(os.path.join(self.root, rel_path)) - ro_paths.append(abs_path if len(abs_path) < len(rel_path) else rel_path) + ro_paths.append(Text(abs_path if len(abs_path) < len(rel_path) else rel_path)) - files_with_label = ["Readonly:"] + ro_paths + files_with_label = [Text("Readonly:")] + ro_paths read_only_output = StringIO() Console(file=read_only_output, force_terminal=False).print(Columns(files_with_label)) read_only_lines = read_only_output.getvalue().splitlines() console.print(Columns(files_with_label)) if editable_files: - files_with_label = editable_files + text_editable_files = [Text(f) for f in editable_files] + files_with_label = text_editable_files if read_only_files: - files_with_label = ["Editable:"] + editable_files + files_with_label = [Text("Editable:")] + text_editable_files editable_output = StringIO() Console(file=editable_output, force_terminal=False).print(Columns(files_with_label)) editable_lines = editable_output.getvalue().splitlines() From 1307215b8fdea01b249473bf4c314a6da48d2252 Mon Sep 17 00:00:00 2001 From: "Paul Gauthier (aider)" Date: Thu, 8 May 2025 09:36:15 -0700 Subject: [PATCH 2/8] test: Test io.format_files_for_input pretty output --- tests/basic/test_io.py | 119 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) diff --git a/tests/basic/test_io.py b/tests/basic/test_io.py index e925ef66d..f590194ab 100644 --- a/tests/basic/test_io.py +++ b/tests/basic/test_io.py @@ -7,6 +7,7 @@ from prompt_toolkit.completion import CompleteEvent from prompt_toolkit.document import Document from aider.dump import dump # noqa: F401 +from rich.text import Text from aider.io import AutoCompleter, ConfirmGroup, InputOutput from aider.utils import ChdirTemporaryDirectory @@ -476,5 +477,123 @@ class TestInputOutputMultilineMode(unittest.TestCase): mock_print.assert_called_once() +class TestInputOutputFormatFiles(unittest.TestCase): + def test_format_files_for_input_pretty_false(self): + io = InputOutput(pretty=False, fancy_input=False) + rel_fnames = ["file1.txt", "file[markup].txt", "ro_file.txt"] + rel_read_only_fnames = ["ro_file.txt"] + + expected_output = ( + "file1.txt\n" + "file[markup].txt\n" + "ro_file.txt (read only)\n" + ) + # Sort the expected lines because the order of editable vs read-only might vary + # depending on internal sorting, but the content should be the same. + # The method sorts editable_files and read_only_files separately. + # The final output joins sorted(read_only_files) + sorted(editable_files) + + # Based on current implementation: + # read_only_files = ["ro_file.txt (read only)"] + # editable_files = ["file1.txt", "file[markup].txt"] + # output = "\n".join(read_only_files + editable_files) + "\n" + + # Correct expected output based on implementation: + expected_output_lines = sorted([ + "ro_file.txt (read only)", + "file1.txt", + "file[markup].txt", + ]) + expected_output = "\n".join(expected_output_lines) + "\n" + + actual_output = io.format_files_for_input(rel_fnames, rel_read_only_fnames) + + # Normalizing actual output by splitting, sorting, and rejoining + actual_output_lines = sorted(filter(None, actual_output.splitlines())) + normalized_actual_output = "\n".join(actual_output_lines) + "\n" + + self.assertEqual(normalized_actual_output, expected_output) + + @patch("rich.columns.Columns") + @patch("os.path.abspath") + @patch("os.path.join") + def test_format_files_for_input_pretty_true_no_files(self, mock_join, mock_abspath, mock_columns): + io = InputOutput(pretty=True, root="test_root") + io.format_files_for_input([], []) + mock_columns.assert_not_called() + + @patch("rich.columns.Columns") + @patch("os.path.abspath") + @patch("os.path.join") + def test_format_files_for_input_pretty_true_editable_only(self, mock_join, mock_abspath, mock_columns): + io = InputOutput(pretty=True, root="test_root") + rel_fnames = ["edit1.txt", "edit[markup].txt"] + + io.format_files_for_input(rel_fnames, []) + + mock_columns.assert_called_once() + args, _ = mock_columns.call_args + renderables = args[0] + + self.assertEqual(len(renderables), 2) + self.assertIsInstance(renderables[0], Text) + self.assertEqual(renderables[0].plain, "edit1.txt") + self.assertIsInstance(renderables[1], Text) + self.assertEqual(renderables[1].plain, "edit[markup].txt") + + @patch("rich.columns.Columns") + @patch("os.path.abspath") + @patch("os.path.join") + def test_format_files_for_input_pretty_true_readonly_only(self, mock_join, mock_abspath, mock_columns): + io = InputOutput(pretty=True, root="test_root") + + # Mock path functions to ensure rel_path is chosen by the shortener logic + mock_join.side_effect = lambda *args: "/".join(args) + mock_abspath.side_effect = lambda p: "/ABS_PREFIX_VERY_LONG/" + os.path.normpath(p) + + rel_read_only_fnames = ["ro1.txt", "ro[markup].txt"] + # When all files in chat are read-only + rel_fnames = list(rel_read_only_fnames) + + io.format_files_for_input(rel_fnames, rel_read_only_fnames) + + mock_columns.assert_called_once() + args, _ = mock_columns.call_args + renderables = args[0] + + self.assertEqual(len(renderables), 3) # Readonly: + 2 files + self.assertIsInstance(renderables[0], Text) + self.assertEqual(renderables[0].plain, "Readonly:") + self.assertIsInstance(renderables[1], Text) + self.assertEqual(renderables[1].plain, "ro1.txt") + self.assertIsInstance(renderables[2], Text) + self.assertEqual(renderables[2].plain, "ro[markup].txt") + + @patch("rich.columns.Columns") + @patch("os.path.abspath") + @patch("os.path.join") + def test_format_files_for_input_pretty_true_mixed_files(self, mock_join, mock_abspath, mock_columns): + io = InputOutput(pretty=True, root="test_root") + + mock_join.side_effect = lambda *args: "/".join(args) + mock_abspath.side_effect = lambda p: "/ABS_PREFIX_VERY_LONG/" + os.path.normpath(p) + + rel_fnames = ["edit1.txt", "edit[markup].txt", "ro1.txt", "ro[markup].txt"] + rel_read_only_fnames = ["ro1.txt", "ro[markup].txt"] + + io.format_files_for_input(rel_fnames, rel_read_only_fnames) + + self.assertEqual(mock_columns.call_count, 2) + + # First call for read-only files + args_ro, _ = mock_columns.call_args_list[0] + renderables_ro = args_ro[0] + self.assertEqual(renderables_ro, [Text("Readonly:"), Text("ro1.txt"), Text("ro[markup].txt")]) + + # Second call for editable files + args_ed, _ = mock_columns.call_args_list[1] + renderables_ed = args_ed[0] + self.assertEqual(renderables_ed, [Text("Editable:"), Text("edit1.txt"), Text("edit[markup].txt")]) + if __name__ == "__main__": unittest.main() From f31128706d135caaf50e786b3cfdd605b2daf7d0 Mon Sep 17 00:00:00 2001 From: "Paul Gauthier (aider)" Date: Thu, 8 May 2025 09:36:20 -0700 Subject: [PATCH 3/8] style: Format test_io.py --- tests/basic/test_io.py | 49 ++++++++++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/tests/basic/test_io.py b/tests/basic/test_io.py index f590194ab..e831fef10 100644 --- a/tests/basic/test_io.py +++ b/tests/basic/test_io.py @@ -5,9 +5,9 @@ from unittest.mock import MagicMock, patch from prompt_toolkit.completion import CompleteEvent from prompt_toolkit.document import Document +from rich.text import Text from aider.dump import dump # noqa: F401 -from rich.text import Text from aider.io import AutoCompleter, ConfirmGroup, InputOutput from aider.utils import ChdirTemporaryDirectory @@ -483,11 +483,7 @@ class TestInputOutputFormatFiles(unittest.TestCase): rel_fnames = ["file1.txt", "file[markup].txt", "ro_file.txt"] rel_read_only_fnames = ["ro_file.txt"] - expected_output = ( - "file1.txt\n" - "file[markup].txt\n" - "ro_file.txt (read only)\n" - ) + expected_output = "file1.txt\nfile[markup].txt\nro_file.txt (read only)\n" # Sort the expected lines because the order of editable vs read-only might vary # depending on internal sorting, but the content should be the same. # The method sorts editable_files and read_only_files separately. @@ -499,11 +495,13 @@ class TestInputOutputFormatFiles(unittest.TestCase): # output = "\n".join(read_only_files + editable_files) + "\n" # Correct expected output based on implementation: - expected_output_lines = sorted([ - "ro_file.txt (read only)", - "file1.txt", - "file[markup].txt", - ]) + expected_output_lines = sorted( + [ + "ro_file.txt (read only)", + "file1.txt", + "file[markup].txt", + ] + ) expected_output = "\n".join(expected_output_lines) + "\n" actual_output = io.format_files_for_input(rel_fnames, rel_read_only_fnames) @@ -517,7 +515,9 @@ class TestInputOutputFormatFiles(unittest.TestCase): @patch("rich.columns.Columns") @patch("os.path.abspath") @patch("os.path.join") - def test_format_files_for_input_pretty_true_no_files(self, mock_join, mock_abspath, mock_columns): + def test_format_files_for_input_pretty_true_no_files( + self, mock_join, mock_abspath, mock_columns + ): io = InputOutput(pretty=True, root="test_root") io.format_files_for_input([], []) mock_columns.assert_not_called() @@ -525,7 +525,9 @@ class TestInputOutputFormatFiles(unittest.TestCase): @patch("rich.columns.Columns") @patch("os.path.abspath") @patch("os.path.join") - def test_format_files_for_input_pretty_true_editable_only(self, mock_join, mock_abspath, mock_columns): + def test_format_files_for_input_pretty_true_editable_only( + self, mock_join, mock_abspath, mock_columns + ): io = InputOutput(pretty=True, root="test_root") rel_fnames = ["edit1.txt", "edit[markup].txt"] @@ -544,7 +546,9 @@ class TestInputOutputFormatFiles(unittest.TestCase): @patch("rich.columns.Columns") @patch("os.path.abspath") @patch("os.path.join") - def test_format_files_for_input_pretty_true_readonly_only(self, mock_join, mock_abspath, mock_columns): + def test_format_files_for_input_pretty_true_readonly_only( + self, mock_join, mock_abspath, mock_columns + ): io = InputOutput(pretty=True, root="test_root") # Mock path functions to ensure rel_path is chosen by the shortener logic @@ -553,7 +557,7 @@ class TestInputOutputFormatFiles(unittest.TestCase): rel_read_only_fnames = ["ro1.txt", "ro[markup].txt"] # When all files in chat are read-only - rel_fnames = list(rel_read_only_fnames) + rel_fnames = list(rel_read_only_fnames) io.format_files_for_input(rel_fnames, rel_read_only_fnames) @@ -561,7 +565,7 @@ class TestInputOutputFormatFiles(unittest.TestCase): args, _ = mock_columns.call_args renderables = args[0] - self.assertEqual(len(renderables), 3) # Readonly: + 2 files + self.assertEqual(len(renderables), 3) # Readonly: + 2 files self.assertIsInstance(renderables[0], Text) self.assertEqual(renderables[0].plain, "Readonly:") self.assertIsInstance(renderables[1], Text) @@ -572,7 +576,9 @@ class TestInputOutputFormatFiles(unittest.TestCase): @patch("rich.columns.Columns") @patch("os.path.abspath") @patch("os.path.join") - def test_format_files_for_input_pretty_true_mixed_files(self, mock_join, mock_abspath, mock_columns): + def test_format_files_for_input_pretty_true_mixed_files( + self, mock_join, mock_abspath, mock_columns + ): io = InputOutput(pretty=True, root="test_root") mock_join.side_effect = lambda *args: "/".join(args) @@ -588,12 +594,17 @@ class TestInputOutputFormatFiles(unittest.TestCase): # First call for read-only files args_ro, _ = mock_columns.call_args_list[0] renderables_ro = args_ro[0] - self.assertEqual(renderables_ro, [Text("Readonly:"), Text("ro1.txt"), Text("ro[markup].txt")]) + self.assertEqual( + renderables_ro, [Text("Readonly:"), Text("ro1.txt"), Text("ro[markup].txt")] + ) # Second call for editable files args_ed, _ = mock_columns.call_args_list[1] renderables_ed = args_ed[0] - self.assertEqual(renderables_ed, [Text("Editable:"), Text("edit1.txt"), Text("edit[markup].txt")]) + self.assertEqual( + renderables_ed, [Text("Editable:"), Text("edit1.txt"), Text("edit[markup].txt")] + ) + if __name__ == "__main__": unittest.main() From aef3863c4a1448d3a4ffb03415a46d794ec2278a Mon Sep 17 00:00:00 2001 From: "Paul Gauthier (aider)" Date: Thu, 8 May 2025 09:36:33 -0700 Subject: [PATCH 4/8] fix: Remove unused import in test_io.py --- tests/basic/test_io.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/basic/test_io.py b/tests/basic/test_io.py index e831fef10..80cf50cb7 100644 --- a/tests/basic/test_io.py +++ b/tests/basic/test_io.py @@ -452,8 +452,6 @@ class TestInputOutputMultilineMode(unittest.TestCase): """Test that tool_output correctly handles hex colors without # prefix""" from unittest.mock import patch - from rich.text import Text - # Create IO with hex color without # for tool_output_color io = InputOutput(tool_output_color="FFA500", pretty=True) From d18a9f32bc1d3f2d30ff2982a199e83c76f3b08a Mon Sep 17 00:00:00 2001 From: "Paul Gauthier (aider)" Date: Thu, 8 May 2025 09:38:21 -0700 Subject: [PATCH 5/8] fix: Fix flaky tests for pretty file list formatting --- tests/basic/test_io.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/basic/test_io.py b/tests/basic/test_io.py index 80cf50cb7..476b0a59a 100644 --- a/tests/basic/test_io.py +++ b/tests/basic/test_io.py @@ -475,6 +475,8 @@ class TestInputOutputMultilineMode(unittest.TestCase): mock_print.assert_called_once() +@patch("aider.io.is_dumb_terminal", return_value=False) +@patch.dict(os.environ, {"NO_COLOR": ""}) class TestInputOutputFormatFiles(unittest.TestCase): def test_format_files_for_input_pretty_false(self): io = InputOutput(pretty=False, fancy_input=False) From 4124cee722e940b7b7684cdf51e4e0cf8e2df012 Mon Sep 17 00:00:00 2001 From: "Paul Gauthier (aider)" Date: Thu, 8 May 2025 09:39:22 -0700 Subject: [PATCH 6/8] test: Fix TestInputOutputFormatFiles method signatures --- tests/basic/test_io.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/basic/test_io.py b/tests/basic/test_io.py index 476b0a59a..d1d1f9686 100644 --- a/tests/basic/test_io.py +++ b/tests/basic/test_io.py @@ -478,7 +478,7 @@ class TestInputOutputMultilineMode(unittest.TestCase): @patch("aider.io.is_dumb_terminal", return_value=False) @patch.dict(os.environ, {"NO_COLOR": ""}) class TestInputOutputFormatFiles(unittest.TestCase): - def test_format_files_for_input_pretty_false(self): + def test_format_files_for_input_pretty_false(self, mock_is_dumb_terminal): io = InputOutput(pretty=False, fancy_input=False) rel_fnames = ["file1.txt", "file[markup].txt", "ro_file.txt"] rel_read_only_fnames = ["ro_file.txt"] @@ -516,7 +516,7 @@ class TestInputOutputFormatFiles(unittest.TestCase): @patch("os.path.abspath") @patch("os.path.join") def test_format_files_for_input_pretty_true_no_files( - self, mock_join, mock_abspath, mock_columns + self, mock_join, mock_abspath, mock_columns, mock_is_dumb_terminal ): io = InputOutput(pretty=True, root="test_root") io.format_files_for_input([], []) @@ -526,7 +526,7 @@ class TestInputOutputFormatFiles(unittest.TestCase): @patch("os.path.abspath") @patch("os.path.join") def test_format_files_for_input_pretty_true_editable_only( - self, mock_join, mock_abspath, mock_columns + self, mock_join, mock_abspath, mock_columns, mock_is_dumb_terminal ): io = InputOutput(pretty=True, root="test_root") rel_fnames = ["edit1.txt", "edit[markup].txt"] @@ -547,7 +547,7 @@ class TestInputOutputFormatFiles(unittest.TestCase): @patch("os.path.abspath") @patch("os.path.join") def test_format_files_for_input_pretty_true_readonly_only( - self, mock_join, mock_abspath, mock_columns + self, mock_join, mock_abspath, mock_columns, mock_is_dumb_terminal ): io = InputOutput(pretty=True, root="test_root") @@ -577,7 +577,7 @@ class TestInputOutputFormatFiles(unittest.TestCase): @patch("os.path.abspath") @patch("os.path.join") def test_format_files_for_input_pretty_true_mixed_files( - self, mock_join, mock_abspath, mock_columns + self, mock_join, mock_abspath, mock_columns, mock_is_dumb_terminal ): io = InputOutput(pretty=True, root="test_root") From bfcff84b286523e020d6d8d2ea31923b1d0099ce Mon Sep 17 00:00:00 2001 From: "Paul Gauthier (aider)" Date: Thu, 8 May 2025 09:42:18 -0700 Subject: [PATCH 7/8] fix: Patch rich.columns.Columns where it is used --- tests/basic/test_io.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/basic/test_io.py b/tests/basic/test_io.py index d1d1f9686..6131f5d65 100644 --- a/tests/basic/test_io.py +++ b/tests/basic/test_io.py @@ -512,7 +512,7 @@ class TestInputOutputFormatFiles(unittest.TestCase): self.assertEqual(normalized_actual_output, expected_output) - @patch("rich.columns.Columns") + @patch("aider.io.Columns") @patch("os.path.abspath") @patch("os.path.join") def test_format_files_for_input_pretty_true_no_files( @@ -522,7 +522,7 @@ class TestInputOutputFormatFiles(unittest.TestCase): io.format_files_for_input([], []) mock_columns.assert_not_called() - @patch("rich.columns.Columns") + @patch("aider.io.Columns") @patch("os.path.abspath") @patch("os.path.join") def test_format_files_for_input_pretty_true_editable_only( @@ -543,7 +543,7 @@ class TestInputOutputFormatFiles(unittest.TestCase): self.assertIsInstance(renderables[1], Text) self.assertEqual(renderables[1].plain, "edit[markup].txt") - @patch("rich.columns.Columns") + @patch("aider.io.Columns") @patch("os.path.abspath") @patch("os.path.join") def test_format_files_for_input_pretty_true_readonly_only( @@ -573,7 +573,7 @@ class TestInputOutputFormatFiles(unittest.TestCase): self.assertIsInstance(renderables[2], Text) self.assertEqual(renderables[2].plain, "ro[markup].txt") - @patch("rich.columns.Columns") + @patch("aider.io.Columns") @patch("os.path.abspath") @patch("os.path.join") def test_format_files_for_input_pretty_true_mixed_files( From 3ed897c66553d7c855a0f3277b0c6e5cfc20f259 Mon Sep 17 00:00:00 2001 From: "Paul Gauthier (aider)" Date: Thu, 8 May 2025 09:43:36 -0700 Subject: [PATCH 8/8] fix: Correct Columns call count assertions in io tests --- tests/basic/test_io.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/basic/test_io.py b/tests/basic/test_io.py index 6131f5d65..270a3c247 100644 --- a/tests/basic/test_io.py +++ b/tests/basic/test_io.py @@ -561,7 +561,7 @@ class TestInputOutputFormatFiles(unittest.TestCase): io.format_files_for_input(rel_fnames, rel_read_only_fnames) - mock_columns.assert_called_once() + self.assertEqual(mock_columns.call_count, 2) args, _ = mock_columns.call_args renderables = args[0] @@ -589,17 +589,17 @@ class TestInputOutputFormatFiles(unittest.TestCase): io.format_files_for_input(rel_fnames, rel_read_only_fnames) - self.assertEqual(mock_columns.call_count, 2) + self.assertEqual(mock_columns.call_count, 4) - # First call for read-only files + # Check arguments for the first rendering of read-only files (call 0) args_ro, _ = mock_columns.call_args_list[0] renderables_ro = args_ro[0] self.assertEqual( renderables_ro, [Text("Readonly:"), Text("ro1.txt"), Text("ro[markup].txt")] ) - # Second call for editable files - args_ed, _ = mock_columns.call_args_list[1] + # Check arguments for the first rendering of editable files (call 2) + args_ed, _ = mock_columns.call_args_list[2] renderables_ed = args_ed[0] self.assertEqual( renderables_ed, [Text("Editable:"), Text("edit1.txt"), Text("edit[markup].txt")]