From 249109ba9a14aef66887267191092d3e8e58d8a4 Mon Sep 17 00:00:00 2001 From: "John-Mason P. Shackelford" Date: Fri, 31 May 2024 16:16:35 -0400 Subject: [PATCH 1/6] All AIDER_* environment vars may now be placed within .env --- .pre-commit-config.yaml | 2 +- aider/args.py | 20 +++++++++- aider/main.py | 13 ++++--- aider/tests/test_main.py | 80 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 107 insertions(+), 8 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 95b449726..9222c3e25 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,5 @@ repos: - - repo: https://github.com/pycqa/isort + - repo: https://github.com/PyCQA/isort rev: 5.12.0 hooks: - id: isort diff --git a/aider/args.py b/aider/args.py index 070905bb4..4b0b1bccb 100644 --- a/aider/args.py +++ b/aider/args.py @@ -12,6 +12,21 @@ from aider.args_formatter import MarkdownHelpFormatter, YamlHelpFormatter from .dump import dump # noqa: F401 +def default_env_file(git_root): + return os.path.join(git_root, ".env") if git_root else ".env" + + +def get_preparser(git_root): + parser = configargparse.ArgumentParser(add_help=False) + parser.add_argument( + "--env-file", + metavar="ENV_FILE", + default=default_env_file(git_root), + help="Specify the .env file to load (default: .env in git root)", + ) + return parser + + def get_parser(default_config_files, git_root): parser = configargparse.ArgumentParser( description="aider is GPT powered coding in your terminal", @@ -184,11 +199,12 @@ def get_parser(default_config_files, git_root): " max_chat_history_tokens." ), ) - default_env_file = os.path.join(git_root, ".env") if git_root else ".env" + # This is a duplicate of the argument in the preparser and is a no-op by this time of + # argument parsing, but it's here so that the help is displayed as expected. group.add_argument( "--env-file", metavar="ENV_FILE", - default=default_env_file, + default=default_env_file(git_root), help="Specify the .env file to load (default: .env in git root)", ) diff --git a/aider/main.py b/aider/main.py index c4d211fc7..f1da79066 100644 --- a/aider/main.py +++ b/aider/main.py @@ -10,7 +10,7 @@ from prompt_toolkit.enums import EditingMode from streamlit.web import cli from aider import __version__, models, utils -from aider.args import get_parser +from aider.args import get_parser, get_preparser from aider.coders import Coder from aider.commands import SwitchModel from aider.io import InputOutput @@ -129,7 +129,7 @@ def format_settings(parser, args): for arg, val in sorted(vars(args).items()): if val: val = scrub_sensitive_info(args, str(val)) - show += f" - {arg}: {val}\n" + show += f" - {arg}: {val}\n" # noqa: E221 return show @@ -225,6 +225,12 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F default_config_files.append(Path.home() / conf_fname) # homedir default_config_files = list(map(str, default_config_files)) + preparser = get_preparser(git_root) + pre_args, _ = preparser.parse_known_args() + + # Load the .env file specified in the arguments + load_dotenv(pre_args.env_file) + parser = get_parser(default_config_files, git_root) args = parser.parse_args(argv) @@ -320,9 +326,6 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F cmd_line = scrub_sensitive_info(args, cmd_line) io.tool_output(cmd_line, log_only=True) - if args.env_file: - load_dotenv(args.env_file) - if args.anthropic_api_key: os.environ["ANTHROPIC_API_KEY"] = args.anthropic_api_key diff --git a/aider/tests/test_main.py b/aider/tests/test_main.py index d319a78dd..001551a28 100644 --- a/aider/tests/test_main.py +++ b/aider/tests/test_main.py @@ -237,3 +237,83 @@ class TestMain(TestCase): main(["--message", test_message]) args, kwargs = MockInputOutput.call_args self.assertEqual(args[1], None) + + def test_dark_mode_sets_code_theme(self): + # Mock Coder.create to capture the configuration + with patch("aider.coders.Coder.create") as MockCoder: + main(["--dark-mode", "--no-git"], input=DummyInput(), output=DummyOutput()) + # Ensure Coder.create was called + MockCoder.assert_called_once() + # Check if the code_theme setting is for dark mode + _, kwargs = MockCoder.call_args + self.assertEqual(kwargs["code_theme"], "monokai") + + def test_light_mode_sets_code_theme(self): + # Mock Coder.create to capture the configuration + with patch("aider.coders.Coder.create") as MockCoder: + main(["--light-mode", "--no-git"], input=DummyInput(), output=DummyOutput()) + # Ensure Coder.create was called + MockCoder.assert_called_once() + # Check if the code_theme setting is for light mode + _, kwargs = MockCoder.call_args + self.assertEqual(kwargs["code_theme"], "default") + + def test_env_file_flag_read(self): + # Create a temporary .env file + env_file_path = Path(self.tempdir) / ".env.test" + env_content = "TEST_ENV_VAR=12345" + env_file_path.write_text(env_content) + + # Run the main function with the --env-file flag + main( + ["--env-file", str(env_file_path), "--no-git"], input=DummyInput(), output=DummyOutput() + ) + + # Check if the environment variable is loaded + self.assertEqual(os.getenv("TEST_ENV_VAR"), "12345") + + def test_default_env_file_read(self): + # Create a temporary .env file + env_file_path = Path(self.tempdir) / ".env" + env_content = "TEST_ENV_VAR=12345" + env_file_path.write_text(env_content) + + # Run the main function with the --env-file flag + main(["--no-git"], input=DummyInput(), output=DummyOutput()) + + # Check if the environment variable is loaded + self.assertEqual(os.getenv("TEST_ENV_VAR"), "12345") + + def test_env_file_flag_sets_automatic_variable(self): + # Create a temporary .env file with custom settings + env_file_path = Path(self.tempdir) / ".env.test" + env_content = "AIDER_DARK_MODE=True" + env_file_path.write_text(env_content) + + # Mock the InputOutput to capture the configuration + with patch("aider.coders.Coder.create") as MockCoder: + main( + ["--env-file", str(env_file_path), "--no-git"], + input=DummyInput(), + output=DummyOutput(), + ) + # Ensure Coder.create was called + MockCoder.assert_called_once() + # Check if the color settings are for dark mode + _, kwargs = MockCoder.call_args + self.assertEqual(kwargs["code_theme"], "monokai") + + def test_default_env_file_sets_automatic_variable(self): + # Create a default .env file in the temporary directory + env_file_path = Path(self.tempdir) / ".env" + env_content = "AIDER_DARK_MODE=True" + env_file_path.write_text(env_content) + + # Mock the InputOutput to capture the configuration + with patch("aider.coders.Coder.create") as MockCoder: + main(["--no-git"], input=DummyInput(), output=DummyOutput()) + # Ensure Coder.create was called + MockCoder.assert_called_once() + # Check if the color settings are for dark mode + _, kwargs = MockCoder.call_args + self.assertEqual(kwargs["code_theme"], "monokai") From 75ec0f626600690675a2275700c9458a49a05265 Mon Sep 17 00:00:00 2001 From: "John-Mason P. Shackelford" Date: Sat, 1 Jun 2024 10:51:38 -0400 Subject: [PATCH 2/6] Added tests to ensure boolean values in .env file are properly handled. --- aider/main.py | 2 +- aider/tests/test_main.py | 69 ++++++++++++++++------------------------ 2 files changed, 29 insertions(+), 42 deletions(-) diff --git a/aider/main.py b/aider/main.py index f1da79066..8799acd6c 100644 --- a/aider/main.py +++ b/aider/main.py @@ -226,7 +226,7 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F default_config_files = list(map(str, default_config_files)) preparser = get_preparser(git_root) - pre_args, _ = preparser.parse_known_args() + pre_args, _ = preparser.parse_known_args(argv) # Load the .env file specified in the arguments load_dotenv(pre_args.env_file) diff --git a/aider/tests/test_main.py b/aider/tests/test_main.py index 001551a28..7eebe1351 100644 --- a/aider/tests/test_main.py +++ b/aider/tests/test_main.py @@ -1,5 +1,4 @@ import os -import shutil import subprocess import tempfile from pathlib import Path @@ -18,14 +17,18 @@ from aider.utils import GitTemporaryDirectory, make_repo class TestMain(TestCase): def setUp(self): + self.original_env = os.environ.copy() os.environ["OPENAI_API_KEY"] = "deadbeef" self.original_cwd = os.getcwd() - self.tempdir = tempfile.mkdtemp() + self.tempdir_obj = tempfile.TemporaryDirectory() + self.tempdir = self.tempdir_obj.name os.chdir(self.tempdir) def tearDown(self): os.chdir(self.original_cwd) - shutil.rmtree(self.tempdir, ignore_errors=True) + self.tempdir_obj.cleanup() + os.environ.clear() + os.environ.update(self.original_env) def test_main_with_empty_dir_no_files_on_command(self): main(["--no-git"], input=DummyInput(), output=DummyOutput()) @@ -258,58 +261,26 @@ class TestMain(TestCase): _, kwargs = MockCoder.call_args self.assertEqual(kwargs["code_theme"], "default") - def test_env_file_flag_read(self): - # Create a temporary .env file - env_file_path = Path(self.tempdir) / ".env.test" - env_content = "TEST_ENV_VAR=12345" - env_file_path.write_text(env_content) - - # Run the main function with the --env-file flag - main( - ["--env-file", str(env_file_path), "--no-git"], input=DummyInput(), output=DummyOutput() - ) - - # Check if the environment variable is loaded - self.assertEqual(os.getenv("TEST_ENV_VAR"), "12345") - - def test_default_env_file_read(self): - # Create a temporary .env file - env_file_path = Path(self.tempdir) / ".env" - env_content = "TEST_ENV_VAR=12345" - env_file_path.write_text(env_content) - - # Run the main function with the --env-file flag - main(["--no-git"], input=DummyInput(), output=DummyOutput()) - - # Check if the environment variable is loaded - self.assertEqual(os.getenv("TEST_ENV_VAR"), "12345") + def create_env_file(self, file_name, content): + env_file_path = Path(self.tempdir) / file_name + env_file_path.write_text(content) + return env_file_path def test_env_file_flag_sets_automatic_variable(self): - # Create a temporary .env file with custom settings - env_file_path = Path(self.tempdir) / ".env.test" - env_content = "AIDER_DARK_MODE=True" - env_file_path.write_text(env_content) - - # Mock the InputOutput to capture the configuration + env_file_path = self.create_env_file(".env.test", "AIDER_DARK_MODE=True") with patch("aider.coders.Coder.create") as MockCoder: main( ["--env-file", str(env_file_path), "--no-git"], input=DummyInput(), output=DummyOutput(), ) - # Ensure Coder.create was called MockCoder.assert_called_once() # Check if the color settings are for dark mode _, kwargs = MockCoder.call_args self.assertEqual(kwargs["code_theme"], "monokai") def test_default_env_file_sets_automatic_variable(self): - # Create a default .env file in the temporary directory - env_file_path = Path(self.tempdir) / ".env" - env_content = "AIDER_DARK_MODE=True" - env_file_path.write_text(env_content) - - # Mock the InputOutput to capture the configuration + self.create_env_file(".env", "AIDER_DARK_MODE=True") with patch("aider.coders.Coder.create") as MockCoder: main(["--no-git"], input=DummyInput(), output=DummyOutput()) # Ensure Coder.create was called @@ -317,3 +288,19 @@ class TestMain(TestCase): # Check if the color settings are for dark mode _, kwargs = MockCoder.call_args self.assertEqual(kwargs["code_theme"], "monokai") + + def test_false_vals_in_env_file(self): + self.create_env_file(".env", "AIDER_SHOW_DIFFS=off") + with patch("aider.coders.Coder.create") as MockCoder: + main(["--no-git"], input=DummyInput(), output=DummyOutput()) + MockCoder.assert_called_once() + _, kwargs = MockCoder.call_args + self.assertEqual(kwargs["show_diffs"], False) + + def test_true_vals_in_env_file(self): + self.create_env_file(".env", "AIDER_SHOW_DIFFS=on") + with patch("aider.coders.Coder.create") as MockCoder: + main(["--no-git"], input=DummyInput(), output=DummyOutput()) + MockCoder.assert_called_once() + _, kwargs = MockCoder.call_args + self.assertEqual(kwargs["show_diffs"], True) From f4e4e3af877fcc781c21a05abdf485211683aac6 Mon Sep 17 00:00:00 2001 From: "John-Mason P. Shackelford" Date: Sat, 1 Jun 2024 11:50:43 -0400 Subject: [PATCH 3/6] Added check to ensure verbose output contains environment variables set with .env --- aider/main.py | 6 ++++++ aider/tests/test_main.py | 16 ++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/aider/main.py b/aider/main.py index 8799acd6c..ac8281d4d 100644 --- a/aider/main.py +++ b/aider/main.py @@ -124,6 +124,12 @@ def check_gitignore(git_root, io, ask=True): def format_settings(parser, args): show = scrub_sensitive_info(args, parser.format_values()) + # clean up the headings for consistency w/ new lines + heading_env = "Environment Variables:" + heading_defaults = "Defaults:" + if heading_env in show: + show = show.replace(heading_env, "\n" + heading_env) + show = show.replace(heading_defaults, "\n" + heading_defaults) show += "\n" show += "Option settings:\n" for arg, val in sorted(vars(args).items()): diff --git a/aider/tests/test_main.py b/aider/tests/test_main.py index 7eebe1351..83e0d1234 100644 --- a/aider/tests/test_main.py +++ b/aider/tests/test_main.py @@ -1,6 +1,7 @@ import os import subprocess import tempfile +from io import StringIO from pathlib import Path from unittest import TestCase from unittest.mock import MagicMock, patch @@ -304,3 +305,18 @@ class TestMain(TestCase): MockCoder.assert_called_once() _, kwargs = MockCoder.call_args self.assertEqual(kwargs["show_diffs"], True) + + def test_verbose_mode_lists_env_vars(self): + self.create_env_file(".env", "AIDER_DARK_MODE=on") + with patch("sys.stdout", new_callable=StringIO) as mock_stdout: + main(["--no-git", "--verbose"], input=DummyInput(), output=DummyOutput()) + output = mock_stdout.getvalue() + relevant_output = "\n".join( + line + for line in output.splitlines() + if "AIDER_DARK_MODE" in line or "dark_mode" in line + ) # this bit just helps failing assertions to be easier to read + self.assertIn("AIDER_DARK_MODE", relevant_output) + self.assertIn("dark_mode", relevant_output) + self.assertRegex(relevant_output, r"AIDER_DARK_MODE:\s+on") + self.assertRegex(relevant_output, r"dark_mode:\s+True") From dd6a7964b6f34e4bfa63d172b48c38fce0270b36 Mon Sep 17 00:00:00 2001 From: "John-Mason P. Shackelford" Date: Tue, 18 Jun 2024 11:39:26 -0400 Subject: [PATCH 4/6] tempdirs in test_main now cleanup without windows errors --- aider/tests/test_main.py | 6 +++--- aider/utils.py | 8 +++++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/aider/tests/test_main.py b/aider/tests/test_main.py index 83e0d1234..b576d3754 100644 --- a/aider/tests/test_main.py +++ b/aider/tests/test_main.py @@ -13,7 +13,7 @@ from prompt_toolkit.output import DummyOutput from aider.dump import dump # noqa: F401 from aider.io import InputOutput from aider.main import check_gitignore, main, setup_git -from aider.utils import GitTemporaryDirectory, make_repo +from aider.utils import GitTemporaryDirectory, IgnorantTemporaryDirectory, make_repo class TestMain(TestCase): @@ -21,7 +21,7 @@ class TestMain(TestCase): self.original_env = os.environ.copy() os.environ["OPENAI_API_KEY"] = "deadbeef" self.original_cwd = os.getcwd() - self.tempdir_obj = tempfile.TemporaryDirectory() + self.tempdir_obj = IgnorantTemporaryDirectory() self.tempdir = self.tempdir_obj.name os.chdir(self.tempdir) @@ -34,7 +34,7 @@ class TestMain(TestCase): def test_main_with_empty_dir_no_files_on_command(self): main(["--no-git"], input=DummyInput(), output=DummyOutput()) - def test_main_with_empty_dir_new_file(self): + def test_main_with_emptqy_dir_new_file(self): main(["foo.txt", "--yes", "--no-git"], input=DummyInput(), output=DummyOutput()) self.assertTrue(os.path.exists("foo.txt")) diff --git a/aider/utils.py b/aider/utils.py index 7636eb119..6d097fbe7 100644 --- a/aider/utils.py +++ b/aider/utils.py @@ -17,11 +17,17 @@ class IgnorantTemporaryDirectory: return self.temp_dir.__enter__() def __exit__(self, exc_type, exc_val, exc_tb): + self.cleanup() + + def cleanup(self): try: - self.temp_dir.__exit__(exc_type, exc_val, exc_tb) + self.temp_dir.cleanup() except (OSError, PermissionError): pass # Ignore errors (Windows) + def __getattr__(self, item): + return getattr(self.temp_dir, item) + class ChdirTemporaryDirectory(IgnorantTemporaryDirectory): def __init__(self): From 705bb64580885c8d91b69dd9f12bd3c0fd58c9fa Mon Sep 17 00:00:00 2001 From: John-Mason Shackelford <160164118+jpshack-at-palomar@users.noreply.github.com> Date: Tue, 18 Jun 2024 12:10:57 -0400 Subject: [PATCH 5/6] Update windows-tests.yml --- .github/workflows/windows-tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/windows-tests.yml b/.github/workflows/windows-tests.yml index de4520aa6..c5b2696a6 100644 --- a/.github/workflows/windows-tests.yml +++ b/.github/workflows/windows-tests.yml @@ -13,6 +13,7 @@ on: - README.md branches: - main + - issue-630 jobs: build: From b4291aef377d5e596e3cb1f830914d5fa98368b2 Mon Sep 17 00:00:00 2001 From: John-Mason Shackelford <160164118+jpshack-at-palomar@users.noreply.github.com> Date: Tue, 18 Jun 2024 12:13:36 -0400 Subject: [PATCH 6/6] Update windows-tests.yml --- .github/workflows/windows-tests.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/windows-tests.yml b/.github/workflows/windows-tests.yml index c5b2696a6..de4520aa6 100644 --- a/.github/workflows/windows-tests.yml +++ b/.github/workflows/windows-tests.yml @@ -13,7 +13,6 @@ on: - README.md branches: - main - - issue-630 jobs: build: