Merge remote-tracking branch 'origin/main'

This commit is contained in:
Paul Gauthier 2024-06-18 11:23:49 -07:00
commit 5748a575f8
5 changed files with 128 additions and 14 deletions

View file

@ -1,5 +1,5 @@
repos: repos:
- repo: https://github.com/pycqa/isort - repo: https://github.com/PyCQA/isort
rev: 5.12.0 rev: 5.12.0
hooks: hooks:
- id: isort - id: isort

View file

@ -12,6 +12,21 @@ from aider.args_formatter import MarkdownHelpFormatter, YamlHelpFormatter
from .dump import dump # noqa: F401 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): def get_parser(default_config_files, git_root):
parser = configargparse.ArgumentParser( parser = configargparse.ArgumentParser(
description="aider is GPT powered coding in your terminal", 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." " 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( group.add_argument(
"--env-file", "--env-file",
metavar="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)", help="Specify the .env file to load (default: .env in git root)",
) )

View file

@ -10,7 +10,7 @@ from prompt_toolkit.enums import EditingMode
from streamlit.web import cli from streamlit.web import cli
from aider import __version__, models, utils 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.coders import Coder
from aider.commands import SwitchModel from aider.commands import SwitchModel
from aider.io import InputOutput from aider.io import InputOutput
@ -124,12 +124,18 @@ def check_gitignore(git_root, io, ask=True):
def format_settings(parser, args): def format_settings(parser, args):
show = scrub_sensitive_info(args, parser.format_values()) 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 += "\n"
show += "Option settings:\n" show += "Option settings:\n"
for arg, val in sorted(vars(args).items()): for arg, val in sorted(vars(args).items()):
if val: if val:
val = scrub_sensitive_info(args, str(val)) val = scrub_sensitive_info(args, str(val))
show += f" - {arg}: {val}\n" show += f" - {arg}: {val}\n" # noqa: E221
return show return show
@ -225,6 +231,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.append(Path.home() / conf_fname) # homedir
default_config_files = list(map(str, default_config_files)) default_config_files = list(map(str, default_config_files))
preparser = get_preparser(git_root)
pre_args, _ = preparser.parse_known_args(argv)
# Load the .env file specified in the arguments
load_dotenv(pre_args.env_file)
parser = get_parser(default_config_files, git_root) parser = get_parser(default_config_files, git_root)
args = parser.parse_args(argv) args = parser.parse_args(argv)
@ -320,9 +332,6 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F
cmd_line = scrub_sensitive_info(args, cmd_line) cmd_line = scrub_sensitive_info(args, cmd_line)
io.tool_output(cmd_line, log_only=True) io.tool_output(cmd_line, log_only=True)
if args.env_file:
load_dotenv(args.env_file)
if args.anthropic_api_key: if args.anthropic_api_key:
os.environ["ANTHROPIC_API_KEY"] = args.anthropic_api_key os.environ["ANTHROPIC_API_KEY"] = args.anthropic_api_key

View file

@ -1,7 +1,7 @@
import os import os
import shutil
import subprocess import subprocess
import tempfile import tempfile
from io import StringIO
from pathlib import Path from pathlib import Path
from unittest import TestCase from unittest import TestCase
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
@ -13,24 +13,28 @@ from prompt_toolkit.output import DummyOutput
from aider.dump import dump # noqa: F401 from aider.dump import dump # noqa: F401
from aider.io import InputOutput from aider.io import InputOutput
from aider.main import check_gitignore, main, setup_git 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): class TestMain(TestCase):
def setUp(self): def setUp(self):
self.original_env = os.environ.copy()
os.environ["OPENAI_API_KEY"] = "deadbeef" os.environ["OPENAI_API_KEY"] = "deadbeef"
self.original_cwd = os.getcwd() self.original_cwd = os.getcwd()
self.tempdir = tempfile.mkdtemp() self.tempdir_obj = IgnorantTemporaryDirectory()
self.tempdir = self.tempdir_obj.name
os.chdir(self.tempdir) os.chdir(self.tempdir)
def tearDown(self): def tearDown(self):
os.chdir(self.original_cwd) 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): def test_main_with_empty_dir_no_files_on_command(self):
main(["--no-git"], input=DummyInput(), output=DummyOutput()) 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()) main(["foo.txt", "--yes", "--no-git"], input=DummyInput(), output=DummyOutput())
self.assertTrue(os.path.exists("foo.txt")) self.assertTrue(os.path.exists("foo.txt"))
@ -237,3 +241,82 @@ class TestMain(TestCase):
main(["--message", test_message]) main(["--message", test_message])
args, kwargs = MockInputOutput.call_args args, kwargs = MockInputOutput.call_args
self.assertEqual(args[1], None) 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 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):
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(),
)
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):
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
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_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)
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")

View file

@ -17,11 +17,17 @@ class IgnorantTemporaryDirectory:
return self.temp_dir.__enter__() return self.temp_dir.__enter__()
def __exit__(self, exc_type, exc_val, exc_tb): def __exit__(self, exc_type, exc_val, exc_tb):
self.cleanup()
def cleanup(self):
try: try:
self.temp_dir.__exit__(exc_type, exc_val, exc_tb) self.temp_dir.cleanup()
except (OSError, PermissionError): except (OSError, PermissionError):
pass # Ignore errors (Windows) pass # Ignore errors (Windows)
def __getattr__(self, item):
return getattr(self.temp_dir, item)
class ChdirTemporaryDirectory(IgnorantTemporaryDirectory): class ChdirTemporaryDirectory(IgnorantTemporaryDirectory):
def __init__(self): def __init__(self):