Merge branch 'main' into register_settings

This commit is contained in:
paul-gauthier 2024-06-21 16:57:33 -07:00 committed by GitHub
commit b6fa02044f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
51 changed files with 1973 additions and 238 deletions

View file

@ -1 +1 @@
__version__ = "0.37.1-dev"
__version__ = "0.39.1-dev"

4
aider/__main__.py Normal file
View file

@ -0,0 +1,4 @@
from .main import main
if __name__ == "__main__":
main()

View file

@ -7,30 +7,47 @@ import sys
import configargparse
from aider import __version__, models
from aider.args_formatter import MarkdownHelpFormatter, YamlHelpFormatter
from aider.args_formatter import (
DotEnvFormatter,
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",
add_config_file_help=True,
default_config_files=default_config_files,
config_file_parser_class=configargparse.YAMLConfigFileParser,
auto_env_var_prefix="AIDER_",
)
group = parser.add_argument_group("Main")
group.add_argument(
"--vim",
action="store_true",
help="Use VI editing mode in the terminal (default: False)",
default=False,
"--llm-history-file",
metavar="LLM_HISTORY_FILE",
default=None,
help="Log the conversation with the LLM to this file (for example, .aider.llm.history)",
)
group.add_argument(
"files",
metavar="FILE",
nargs="*",
help="files to edit with an LLM (optional)",
"files", metavar="FILE", nargs="*", help="files to edit with an LLM (optional)"
)
group.add_argument(
"--openai-api-key",
@ -59,7 +76,7 @@ def get_parser(default_config_files, git_root):
const=opus_model,
help=f"Use {opus_model} model for the main chat",
)
sonnet_model = "claude-3-sonnet-20240229"
sonnet_model = "claude-3-5-sonnet-20240620"
group.add_argument(
"--sonnet",
action="store_const",
@ -146,13 +163,18 @@ def get_parser(default_config_files, git_root):
metavar="MODEL_SETTINGS_FILE",
default=None,
help="Specify a file with aider model settings for unknown models",
)
group.add_argument(
"--model-metadata-file",
metavar="MODEL_METADATA_FILE",
default=None,
help="Specify a file with context window and costs for unknown models",
)
group.add_argument(
"--verify-ssl",
action=argparse.BooleanOptionalAction,
default=True,
help="Verify the SSL cert when connecting to models (default: True)",
)
group.add_argument(
"--edit-format",
metavar="EDIT_FORMAT",
@ -189,11 +211,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)",
)
@ -375,6 +398,12 @@ def get_parser(default_config_files, git_root):
##########
group = parser.add_argument_group("Other Settings")
group.add_argument(
"--vim",
action="store_true",
help="Use VI editing mode in the terminal (default: False)",
default=False,
)
group.add_argument(
"--voice-language",
metavar="VOICE_LANGUAGE",
@ -500,11 +529,27 @@ def get_sample_yaml():
return parser.format_help()
def get_sample_dotenv():
os.environ["COLUMNS"] = "120"
sys.argv = ["aider"]
parser = get_parser([], None)
# This instantiates all the action.env_var values
parser.parse_known_args()
parser.formatter_class = DotEnvFormatter
return argparse.ArgumentParser.format_help(parser)
return parser.format_help()
def main():
arg = sys.argv[1] if len(sys.argv[1:]) else None
if arg == "md":
print(get_md_help())
elif arg == "dotenv":
print(get_sample_dotenv())
else:
print(get_sample_yaml())

View file

@ -1,8 +1,83 @@
import argparse
from aider import urls
from .dump import dump # noqa: F401
class DotEnvFormatter(argparse.HelpFormatter):
def start_section(self, heading):
res = "\n\n"
res += "#" * (len(heading) + 3)
res += f"\n# {heading}"
super().start_section(res)
def _format_usage(self, usage, actions, groups, prefix):
return ""
def _format_text(self, text):
return f"""
##########################################################
# Sample aider .env file.
# Place at the root of your git repo.
# Or use `aider --env <fname>` to specify.
##########################################################
#################
# LLM parameters:
#
# Include xxx_API_KEY parameters and other params needed for your LLMs.
# See {urls.llms} for details.
## OpenAI
#OPENAI_API_KEY=
## Anthropic
#ANTHROPIC_API_KEY=
##...
"""
def _format_action(self, action):
if not action.option_strings:
return ""
if not action.env_var:
return
parts = [""]
default = action.default
if default == argparse.SUPPRESS:
default = ""
elif isinstance(default, str):
pass
elif isinstance(default, list) and not default:
default = ""
elif action.default is not None:
default = "true" if default else "false"
else:
default = ""
if action.help:
parts.append(f"## {action.help}")
if action.env_var:
env_var = action.env_var
if default:
parts.append(f"#{env_var}={default}\n")
else:
parts.append(f"#{env_var}=\n")
return "\n".join(parts) + "\n"
def _format_action_invocation(self, action):
return ""
def _format_args(self, action, default_metavar):
return ""
class YamlHelpFormatter(argparse.HelpFormatter):
def start_section(self, heading):
res = "\n\n"
@ -17,6 +92,7 @@ class YamlHelpFormatter(argparse.HelpFormatter):
return """
##########################################################
# Sample .aider.conf.yaml
# This file lists *all* the valid configuration entries.
# Place in your home dir, or at the root of your git repo.
##########################################################

View file

@ -27,7 +27,7 @@ from aider.mdstream import MarkdownStream
from aider.repo import GitRepo
from aider.repomap import RepoMap
from aider.sendchat import send_with_retries
from aider.utils import is_image_file
from aider.utils import format_content, format_messages, is_image_file
from ..dump import dump # noqa: F401
@ -783,6 +783,8 @@ class Coder:
messages = self.format_messages()
self.io.log_llm_history("TO LLM", format_messages(messages))
if self.verbose:
utils.show_messages(messages, functions=self.functions)
@ -795,21 +797,23 @@ class Coder:
except ExhaustedContextWindow:
exhausted = True
except litellm.exceptions.BadRequestError as err:
self.io.tool_error(f"BadRequestError: {err}")
return
if "ContextWindowExceededError" in err.message:
exhausted = True
else:
self.io.tool_error(f"BadRequestError: {err}")
return
except openai.BadRequestError as err:
if "maximum context length" in str(err):
exhausted = True
else:
raise err
except Exception as err:
self.io.tool_error(f"Unexpected error: {err}")
return
if exhausted:
self.show_exhausted_error()
self.num_exhausted_context_windows += 1
self.io.tool_error("The chat session is larger than the context window!\n")
self.commands.cmd_tokens("")
self.io.tool_error("\nTo reduce token usage:")
self.io.tool_error(" - Use /drop to remove unneeded files from the chat session.")
self.io.tool_error(" - Use /clear to clear chat history.")
return
if self.partial_response_function_call:
@ -825,6 +829,8 @@ class Coder:
self.io.tool_output()
self.io.log_llm_history("LLM RESPONSE", format_content("ASSISTANT", content))
if interrupted:
content += "\n^C KeyboardInterrupt"
self.cur_messages += [dict(role="assistant", content=content)]
@ -878,6 +884,63 @@ class Coder:
else:
self.reflected_message = add_rel_files_message
def show_exhausted_error(self):
output_tokens = 0
if self.partial_response_content:
output_tokens = self.main_model.token_count(self.partial_response_content)
max_output_tokens = self.main_model.info.get("max_output_tokens", 0)
input_tokens = self.main_model.token_count(self.format_messages())
max_input_tokens = self.main_model.info.get("max_input_tokens", 0)
total_tokens = input_tokens + output_tokens
if output_tokens >= max_output_tokens:
out_err = " -- exceeded output limit!"
else:
out_err = ""
if input_tokens >= max_input_tokens:
inp_err = " -- context window exhausted!"
else:
inp_err = ""
if total_tokens >= max_input_tokens:
tot_err = " -- context window exhausted!"
else:
tot_err = ""
res = ["", ""]
res.append(f"Model {self.main_model.name} has hit a token limit!")
res.append("")
res.append(f"Input tokens: {input_tokens:,} of {max_input_tokens:,}{inp_err}")
res.append(f"Output tokens: {output_tokens:,} of {max_output_tokens:,}{out_err}")
res.append(f"Total tokens: {total_tokens:,} of {max_input_tokens:,}{tot_err}")
if output_tokens >= max_output_tokens:
res.append("")
res.append("To reduce output tokens:")
res.append("- Ask for smaller changes in each request.")
res.append("- Break your code into smaller source files.")
if "diff" not in self.main_model.edit_format:
res.append(
"- Try using a stronger model like gpt-4o or opus that can return diffs."
)
if input_tokens >= max_input_tokens or total_tokens >= max_input_tokens:
res.append("")
res.append("To reduce input tokens:")
res.append("- Use /tokens to see token usage.")
res.append("- Use /drop to remove unneeded files from the chat session.")
res.append("- Use /clear to clear the chat history.")
res.append("- Break your code into smaller source files.")
res.append("")
res.append(f"For more info: {urls.token_limits}")
res = "".join([line + "\n" for line in res])
self.io.tool_error(res)
def lint_edited(self, fnames):
res = ""
for fname in fnames:
@ -1321,7 +1384,7 @@ class Coder:
def auto_commit(self, edited):
# context = self.get_context_from_history(self.cur_messages)
res = self.repo.commit(fnames=edited, prefix="aider: ")
res = self.repo.commit(fnames=edited, aider_edits=True)
if res:
commit_hash, commit_message = res
self.last_aider_commit_hash = commit_hash

View file

@ -414,16 +414,8 @@ def find_original_update_blocks(content, fence=DEFAULT_FENCE):
processed.append(cur) # original_marker
filename = strip_filename(processed[-2].splitlines()[-1], fence)
try:
if not filename:
filename = strip_filename(processed[-2].splitlines()[-2], fence)
if not filename:
if current_filename:
filename = current_filename
else:
raise ValueError(missing_filename_err.format(fence=fence))
except IndexError:
filename = find_filename(processed[-2].splitlines(), fence)
if not filename:
if current_filename:
filename = current_filename
else:
@ -460,6 +452,35 @@ def find_original_update_blocks(content, fence=DEFAULT_FENCE):
raise ValueError(f"{processed}\n^^^ Error parsing SEARCH/REPLACE block.")
def find_filename(lines, fence):
"""
Deepseek Coder v2 has been doing this:
```python
word_count.py
```
```python
<<<<<<< SEARCH
...
This is a more flexible search back for filenames.
"""
# Go back through the 3 preceding lines
lines.reverse()
lines = lines[:3]
for line in lines:
# If we find a filename, done
filename = strip_filename(line, fence)
if filename:
return filename
# Only continue as long as we keep seeing fences
if not line.startswith(fence[0]):
return
if __name__ == "__main__":
edit = """
Here's the change:

View file

@ -332,7 +332,7 @@ class Commands:
last_commit = self.coder.repo.repo.head.commit
if (
not last_commit.message.startswith("aider:")
not last_commit.author.name.endswith(" (aider)")
or last_commit.hexsha[:7] != self.coder.last_aider_commit_hash
):
self.io.tool_error("The last commit was not made by aider in this chat session.")

View file

@ -110,9 +110,6 @@ class GUI:
show_undo = False
res = ""
if commit_hash:
prefix = "aider: "
if commit_message.startswith(prefix):
commit_message = commit_message[len(prefix) :]
res += f"Commit `{commit_hash}`: {commit_message} \n"
if commit_hash == self.coder.last_aider_commit_hash:
show_undo = True

View file

@ -107,6 +107,7 @@ class InputOutput:
tool_error_color="red",
encoding="utf-8",
dry_run=False,
llm_history_file=None,
editingmode=EditingMode.EMACS,
):
self.editingmode = editingmode
@ -128,6 +129,7 @@ class InputOutput:
self.yes = yes
self.input_history_file = input_history_file
self.llm_history_file = llm_history_file
if chat_history_file is not None:
self.chat_history_file = Path(chat_history_file)
else:
@ -209,10 +211,11 @@ class InputOutput:
else:
style = None
completer_instance = AutoCompleter(
root, rel_fnames, addable_rel_fnames, commands, self.encoding
)
while True:
completer_instance = AutoCompleter(
root, rel_fnames, addable_rel_fnames, commands, self.encoding
)
if multiline_input:
show = ". "
@ -271,6 +274,14 @@ class InputOutput:
fh = FileHistory(self.input_history_file)
return fh.load_history_strings()
def log_llm_history(self, role, content):
if not self.llm_history_file:
return
timestamp = datetime.now().isoformat(timespec='seconds')
with open(self.llm_history_file, 'a', encoding=self.encoding) as log_file:
log_file.write(f"{role.upper()} {timestamp}\n")
log_file.write(content + "\n")
def user_input(self, inp, log_only=True):
if not log_only:
style = dict(style=self.user_input_color) if self.user_input_color else dict()

View file

@ -5,12 +5,13 @@ import sys
from pathlib import Path
import git
import httpx
from dotenv import load_dotenv
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
@ -124,12 +125,18 @@ 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()):
if val:
val = scrub_sensitive_info(args, str(val))
show += f" - {arg}: {val}\n"
show += f" - {arg}: {val}\n" # noqa: E221
return show
@ -266,9 +273,18 @@ 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(argv)
# 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)
if not args.verify_ssl:
litellm.client_session = httpx.Client(verify=False)
if args.gui and not return_coder:
launch_gui(argv)
return
@ -302,6 +318,7 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F
tool_error_color=args.tool_error_color,
dry_run=args.dry_run,
encoding=args.encoding,
llm_history_file=args.llm_history_file,
editingmode=editing_mode,
)
@ -360,9 +377,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

View file

@ -179,6 +179,43 @@ MODEL_SETTINGS = [
"whole",
weak_model_name="claude-3-haiku-20240307",
),
ModelSettings(
"claude-3-5-sonnet-20240620",
"diff",
weak_model_name="claude-3-haiku-20240307",
use_repo_map=True,
),
ModelSettings(
"anthropic/claude-3-5-sonnet-20240620",
"diff",
weak_model_name="claude-3-haiku-20240307",
use_repo_map=True,
),
ModelSettings(
"openrouter/anthropic/claude-3.5-sonnet",
"diff",
weak_model_name="openrouter/anthropic/claude-3-haiku-20240307",
use_repo_map=True,
),
# Vertex AI Claude models
ModelSettings(
"vertex_ai/claude-3-5-sonnet@20240620",
"diff",
weak_model_name="vertex_ai/claude-3-haiku@20240307",
use_repo_map=True,
),
ModelSettings(
"vertex_ai/claude-3-opus@20240229",
"diff",
weak_model_name="vertex_ai/claude-3-haiku@20240307",
use_repo_map=True,
send_undo_reply=True,
),
ModelSettings(
"vertex_ai/claude-3-sonnet@20240229",
"whole",
weak_model_name="vertex_ai/claude-3-haiku@20240307",
),
# Cohere
ModelSettings(
"command-r-plus",
@ -219,7 +256,7 @@ MODEL_SETTINGS = [
send_undo_reply=True,
),
ModelSettings(
"openai/deepseek-chat",
"deepseek/deepseek-chat",
"diff",
use_repo_map=True,
send_undo_reply=True,
@ -227,7 +264,15 @@ MODEL_SETTINGS = [
reminder_as_sys_msg=True,
),
ModelSettings(
"deepseek/deepseek-chat",
"deepseek/deepseek-coder",
"diff",
use_repo_map=True,
send_undo_reply=True,
examples_as_sys_msg=True,
reminder_as_sys_msg=True,
),
ModelSettings(
"openrouter/deepseek/deepseek-coder",
"diff",
use_repo_map=True,
send_undo_reply=True,

View file

@ -59,7 +59,7 @@ class GitRepo:
if aider_ignore_file:
self.aider_ignore_file = Path(aider_ignore_file)
def commit(self, fnames=None, context=None, prefix=None, message=None):
def commit(self, fnames=None, context=None, message=None, aider_edits=False):
if not fnames and not self.repo.is_dirty():
return
@ -75,9 +75,6 @@ class GitRepo:
if not commit_message:
commit_message = "(no commit message provided)"
if prefix:
commit_message = prefix + commit_message
full_commit_message = commit_message
if context:
full_commit_message += "\n\n# Aider chat conversation:\n\n" + context
@ -91,10 +88,32 @@ class GitRepo:
else:
cmd += ["-a"]
original_user_name = self.repo.config_reader().get_value("user", "name")
original_committer_name_env = os.environ.get("GIT_COMMITTER_NAME")
committer_name = f"{original_user_name} (aider)"
os.environ["GIT_COMMITTER_NAME"] = committer_name
if aider_edits:
original_auther_name_env = os.environ.get("GIT_AUTHOR_NAME")
os.environ["GIT_AUTHOR_NAME"] = committer_name
self.repo.git.commit(cmd)
commit_hash = self.repo.head.commit.hexsha[:7]
self.io.tool_output(f"Commit {commit_hash} {commit_message}")
# Restore the original GIT_COMMITTER_NAME
if aider_edits:
if original_auther_name_env is not None:
os.environ["GIT_AUTHOR_NAME"] = original_auther_name_env
else:
del os.environ["GIT_AUTHOR_NAME"]
if original_committer_name_env is not None:
os.environ["GIT_COMMITTER_NAME"] = original_committer_name_env
else:
del os.environ["GIT_COMMITTER_NAME"]
return commit_hash, commit_message
def get_rel_repo_dir(self):

View file

@ -10,7 +10,7 @@ from bs4 import BeautifulSoup
from playwright.sync_api import sync_playwright
from aider import __version__, urls
from aider.dump import dump
from aider.dump import dump # noqa: F401
aider_user_agent = f"Aider/{__version__} +{urls.website}"
@ -21,7 +21,7 @@ For better web scraping, install Playwright chromium with this command in your t
playwright install --with-deps chromium
See {urls.enable_playwrite} for more info.
See {urls.enable_playwright} for more info.
"""
@ -53,7 +53,6 @@ class Scraper:
else:
content = self.scrape_with_httpx(url)
dump(content)
if not content:
return

View file

@ -523,16 +523,20 @@ class TestCommands(TestCase):
other_path.write_text("other content")
repo.git.add(str(other_path))
os.environ["GIT_AUTHOR_NAME"] = "Foo (aider)"
# Create and commit a file
filename = "test_file.txt"
file_path = Path(repo_dir) / filename
file_path.write_text("first content")
repo.git.add(filename)
repo.git.commit("-m", "aider: first commit")
repo.git.commit("-m", "first commit")
file_path.write_text("second content")
repo.git.add(filename)
repo.git.commit("-m", "aider: second commit")
repo.git.commit("-m", "second commit")
del os.environ["GIT_AUTHOR_NAME"]
# Store the commit hash
last_commit_hash = repo.head.commit.hexsha[:7]

View file

@ -398,6 +398,32 @@ Hope you like it!
],
)
def test_deepseek_coder_v2_filename_mangling(self):
edit = """
Here's the change:
```python
foo.txt
```
```python
<<<<<<< SEARCH
one
=======
two
>>>>>>> REPLACE
```
Hope you like it!
"""
edits = list(eb.find_original_update_blocks(edit))
self.assertEqual(
edits,
[
("foo.txt", "one\n", "two\n"),
],
)
if __name__ == "__main__":
unittest.main()

View file

@ -1,7 +1,7 @@
import os
import shutil
import subprocess
import tempfile
from io import StringIO
from pathlib import Path
from unittest import TestCase
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.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):
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 = IgnorantTemporaryDirectory()
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())
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"))
@ -237,3 +241,82 @@ 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 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

@ -1,4 +1,5 @@
import os
import platform
import tempfile
import unittest
from pathlib import Path
@ -137,6 +138,52 @@ class TestRepo(unittest.TestCase):
# Assert that the returned message is the expected one
self.assertEqual(result, 'a good "commit message"')
@patch("aider.repo.GitRepo.get_commit_message")
def test_commit_with_custom_committer_name(self, mock_send):
mock_send.return_value = '"a good commit message"'
# Cleanup of the git temp dir explodes on windows
if platform.system() == "Windows":
return
with GitTemporaryDirectory():
# new repo
raw_repo = git.Repo()
raw_repo.config_writer().set_value("user", "name", "Test User").release()
# add a file and commit it
fname = Path("file.txt")
fname.touch()
raw_repo.git.add(str(fname))
raw_repo.git.commit("-m", "initial commit")
io = InputOutput()
git_repo = GitRepo(io, None, None)
# commit a change
fname.write_text("new content")
git_repo.commit(fnames=[str(fname)], aider_edits=True)
# check the committer name
commit = raw_repo.head.commit
self.assertEqual(commit.author.name, "Test User (aider)")
self.assertEqual(commit.committer.name, "Test User (aider)")
# commit a change without aider_edits
fname.write_text("new content again!")
git_repo.commit(fnames=[str(fname)], aider_edits=False)
# check the committer name
commit = raw_repo.head.commit
self.assertEqual(commit.author.name, "Test User")
self.assertEqual(commit.committer.name, "Test User (aider)")
# check that the original committer name is restored
original_committer_name = os.environ.get("GIT_COMMITTER_NAME")
self.assertIsNone(original_committer_name)
original_author_name = os.environ.get("GIT_AUTHOR_NAME")
self.assertIsNone(original_author_name)
def test_get_tracked_files(self):
# Create a temporary directory
tempdir = Path(tempfile.mkdtemp())

View file

@ -2,6 +2,8 @@ website = "https://aider.chat/"
add_all_files = "https://aider.chat/docs/faq.html#how-can-i-add-all-the-files-to-the-chat"
edit_errors = "https://aider.chat/docs/troubleshooting/edit-errors.html"
git = "https://aider.chat/docs/git.html"
enable_playwrite = "https://aider.chat/docs/install/optional.html#enable-playwright"
enable_playwright = "https://aider.chat/docs/install/optional.html#enable-playwright"
favicon = "https://aider.chat/assets/icons/favicon-32x32.png"
model_warnings = "https://aider.chat/docs/llms/warnings.html"
token_limits = "https://aider.chat/docs/troubleshooting/token-limits.html"
llms = "https://aider.chat/docs/llms.html"

View file

@ -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):
@ -84,24 +90,38 @@ def safe_abs_path(res):
return str(res)
def show_messages(messages, title=None, functions=None):
def format_content(role, content):
formatted_lines = []
for line in content.splitlines():
formatted_lines.append(f"{role} {line}")
return "\n".join(formatted_lines)
def format_messages(messages, title=None):
output = []
if title:
print(title.upper(), "*" * 50)
output.append(f"{title.upper()} {'*' * 50}")
for msg in messages:
print()
output.append("")
role = msg["role"].upper()
content = msg.get("content")
if isinstance(content, list): # Handle list content (e.g., image messages)
for item in content:
if isinstance(item, dict) and "image_url" in item:
print(role, "Image URL:", item["image_url"]["url"])
output.append(f"{role} Image URL: {item['image_url']['url']}")
elif isinstance(content, str): # Handle string content
for line in content.splitlines():
print(role, line)
output.append(format_content(role, content))
content = msg.get("function_call")
if content:
print(role, content)
output.append(f"{role} {content}")
return "\n".join(output)
def show_messages(messages, title=None, functions=None):
formatted_output = format_messages(messages, title)
print(formatted_output)
if functions:
dump(functions)