mirror of
https://github.com/Aider-AI/aider.git
synced 2025-06-14 08:34:59 +00:00
Merge branch 'main' into dvf_llm_log
This commit is contained in:
commit
3e9f6dcca2
68 changed files with 755 additions and 356 deletions
|
@ -42,7 +42,7 @@ def get_parser(default_config_files, git_root):
|
|||
"--anthropic-api-key",
|
||||
metavar="ANTHROPIC_API_KEY",
|
||||
env_var="ANTHROPIC_API_KEY",
|
||||
help="Specify the OpenAI API key",
|
||||
help="Specify the Anthropic API key",
|
||||
)
|
||||
default_model = models.DEFAULT_MODEL_NAME
|
||||
group.add_argument(
|
||||
|
@ -141,6 +141,12 @@ def get_parser(default_config_files, git_root):
|
|||
env_var="OPENAI_ORGANIZATION_ID",
|
||||
help="Specify the OpenAI organization ID",
|
||||
)
|
||||
group.add_argument(
|
||||
"--model-metadata-file",
|
||||
metavar="MODEL_FILE",
|
||||
default=None,
|
||||
help="Specify a file with context window and costs for unknown models",
|
||||
)
|
||||
group.add_argument(
|
||||
"--edit-format",
|
||||
metavar="EDIT_FORMAT",
|
||||
|
@ -363,6 +369,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",
|
||||
|
|
|
@ -18,7 +18,7 @@ from jsonschema import Draft7Validator
|
|||
from rich.console import Console, Text
|
||||
from rich.markdown import Markdown
|
||||
|
||||
from aider import __version__, models, prompts, utils
|
||||
from aider import __version__, models, prompts, urls, utils
|
||||
from aider.commands import Commands
|
||||
from aider.history import ChatSummary
|
||||
from aider.io import InputOutput
|
||||
|
@ -587,14 +587,16 @@ class Coder:
|
|||
while new_user_message:
|
||||
self.reflected_message = None
|
||||
list(self.send_new_user_message(new_user_message))
|
||||
if self.num_reflections < self.max_reflections:
|
||||
self.num_reflections += 1
|
||||
new_user_message = self.reflected_message
|
||||
else:
|
||||
self.io.tool_error(
|
||||
f"Only {self.max_reflections} reflections allowed, stopping."
|
||||
)
|
||||
new_user_message = None
|
||||
|
||||
new_user_message = None
|
||||
if self.reflected_message:
|
||||
if self.num_reflections < self.max_reflections:
|
||||
self.num_reflections += 1
|
||||
new_user_message = self.reflected_message
|
||||
else:
|
||||
self.io.tool_error(
|
||||
f"Only {self.max_reflections} reflections allowed, stopping."
|
||||
)
|
||||
|
||||
if with_message:
|
||||
return self.partial_response_content
|
||||
|
@ -1221,9 +1223,7 @@ class Coder:
|
|||
return
|
||||
|
||||
self.io.tool_error("Warning: it's best to only add files that need changes to the chat.")
|
||||
self.io.tool_error(
|
||||
"https://aider.chat/docs/faq.html#how-can-i-add-all-the-files-to-the-chat"
|
||||
)
|
||||
self.io.tool_error(urls.edit_errors)
|
||||
self.warning_given = True
|
||||
|
||||
def prepare_to_edit(self, edits):
|
||||
|
@ -1263,9 +1263,7 @@ class Coder:
|
|||
err = err.args[0]
|
||||
|
||||
self.io.tool_error("The LLM did not conform to the edit format.")
|
||||
self.io.tool_error(
|
||||
"For more info see: https://aider.chat/docs/faq.html#aider-isnt-editing-my-files"
|
||||
)
|
||||
self.io.tool_error(urls.edit_errors)
|
||||
self.io.tool_error()
|
||||
self.io.tool_error(str(err), strip=False)
|
||||
|
||||
|
@ -1330,8 +1328,8 @@ class Coder:
|
|||
return context
|
||||
|
||||
def auto_commit(self, edited):
|
||||
context = self.get_context_from_history(self.cur_messages)
|
||||
res = self.repo.commit(fnames=edited, context=context, prefix="aider: ")
|
||||
# context = self.get_context_from_history(self.cur_messages)
|
||||
res = self.repo.commit(fnames=edited, prefix="aider: ")
|
||||
if res:
|
||||
commit_hash, commit_message = res
|
||||
self.last_aider_commit_hash = commit_hash
|
||||
|
|
|
@ -4,140 +4,12 @@ from .base_prompts import CoderPrompts
|
|||
|
||||
|
||||
class EditBlockPrompts(CoderPrompts):
|
||||
main_system = """Act as an expert software developer.
|
||||
Always use best practices when coding.
|
||||
Respect and use existing conventions, libraries, etc that are already present in the code base.
|
||||
{lazy_prompt}
|
||||
Take requests for changes to the supplied code.
|
||||
If the request is ambiguous, ask questions.
|
||||
main_system = """Act as a software expert and answer user questions about how to use the aider program.
|
||||
You never write code, just answer questions.
|
||||
|
||||
Always reply to the user in the same language they are using.
|
||||
|
||||
Once you understand the request you MUST:
|
||||
1. Decide if you need to propose *SEARCH/REPLACE* edits to any files that haven't been added to the chat. You can create new files without asking. But if you need to propose edits to existing files not already added to the chat, you *MUST* tell the user their full path names and ask them to *add the files to the chat*. End your reply and wait for their approval. You can keep asking if you then decide you need to edit more files.
|
||||
2. Think step-by-step and explain the needed changes with a numbered list of short sentences.
|
||||
3. Describe each change with a *SEARCH/REPLACE block* per the examples below. All changes to files must use this *SEARCH/REPLACE block* format. ONLY EVER RETURN CODE IN A *SEARCH/REPLACE BLOCK*!
|
||||
|
||||
All changes to files must use the *SEARCH/REPLACE block* format.
|
||||
Decide if you need to see any files that haven't been added to the chat. If so, you *MUST* tell the user their full path names and ask them to *add the files to the chat*. End your reply and wait for their approval. You can keep asking if you then decide you need to see more files.
|
||||
"""
|
||||
|
||||
example_messages = [
|
||||
dict(
|
||||
role="user",
|
||||
content="Change get_factorial() to use math.factorial",
|
||||
),
|
||||
dict(
|
||||
role="assistant",
|
||||
content="""To make this change we need to modify `mathweb/flask/app.py` to:
|
||||
example_messages = []
|
||||
|
||||
1. Import the math package.
|
||||
2. Remove the existing factorial() function.
|
||||
3. Update get_factorial() to call math.factorial instead.
|
||||
|
||||
Here are the *SEARCH/REPLACE* blocks:
|
||||
|
||||
mathweb/flask/app.py
|
||||
{fence[0]}python
|
||||
<<<<<<< SEARCH
|
||||
from flask import Flask
|
||||
=======
|
||||
import math
|
||||
from flask import Flask
|
||||
>>>>>>> REPLACE
|
||||
{fence[1]}
|
||||
|
||||
mathweb/flask/app.py
|
||||
{fence[0]}python
|
||||
<<<<<<< SEARCH
|
||||
def factorial(n):
|
||||
"compute factorial"
|
||||
|
||||
if n == 0:
|
||||
return 1
|
||||
else:
|
||||
return n * factorial(n-1)
|
||||
|
||||
=======
|
||||
>>>>>>> REPLACE
|
||||
{fence[1]}
|
||||
|
||||
mathweb/flask/app.py
|
||||
{fence[0]}python
|
||||
<<<<<<< SEARCH
|
||||
return str(factorial(n))
|
||||
=======
|
||||
return str(math.factorial(n))
|
||||
>>>>>>> REPLACE
|
||||
{fence[1]}
|
||||
""",
|
||||
),
|
||||
dict(
|
||||
role="user",
|
||||
content="Refactor hello() into its own file.",
|
||||
),
|
||||
dict(
|
||||
role="assistant",
|
||||
content="""To make this change we need to modify `main.py` and make a new file `hello.py`:
|
||||
|
||||
1. Make a new hello.py file with hello() in it.
|
||||
2. Remove hello() from main.py and replace it with an import.
|
||||
|
||||
Here are the *SEARCH/REPLACE* blocks:
|
||||
|
||||
hello.py
|
||||
{fence[0]}python
|
||||
<<<<<<< SEARCH
|
||||
=======
|
||||
def hello():
|
||||
"print a greeting"
|
||||
|
||||
print("hello")
|
||||
>>>>>>> REPLACE
|
||||
{fence[1]}
|
||||
|
||||
main.py
|
||||
{fence[0]}python
|
||||
<<<<<<< SEARCH
|
||||
def hello():
|
||||
"print a greeting"
|
||||
|
||||
print("hello")
|
||||
=======
|
||||
from hello import hello
|
||||
>>>>>>> REPLACE
|
||||
{fence[1]}
|
||||
""",
|
||||
),
|
||||
]
|
||||
|
||||
system_reminder = """# *SEARCH/REPLACE block* Rules:
|
||||
|
||||
Every *SEARCH/REPLACE block* must use this format:
|
||||
1. The file path alone on a line, verbatim. No bold asterisks, no quotes around it, no escaping of characters, etc.
|
||||
2. The opening fence and code language, eg: {fence[0]}python
|
||||
3. The start of search block: <<<<<<< SEARCH
|
||||
4. A contiguous chunk of lines to search for in the existing source code
|
||||
5. The dividing line: =======
|
||||
6. The lines to replace into the source code
|
||||
7. The end of the replace block: >>>>>>> REPLACE
|
||||
8. The closing fence: {fence[1]}
|
||||
|
||||
Every *SEARCH* section must *EXACTLY MATCH* the existing source code, character for character, including all comments, docstrings, etc.
|
||||
|
||||
*SEARCH/REPLACE* blocks will replace *all* matching occurrences.
|
||||
Include enough lines to make the SEARCH blocks unique.
|
||||
|
||||
Include *ALL* the code being searched and replaced!
|
||||
|
||||
Only create *SEARCH/REPLACE* blocks for files that the user has added to the chat!
|
||||
|
||||
To move code within a file, use 2 *SEARCH/REPLACE* blocks: 1 to delete it from its current location, 1 to insert it in the new location.
|
||||
|
||||
If you want to put code in a new file, use a *SEARCH/REPLACE block* with:
|
||||
- A new file path, including dir name if needed
|
||||
- An empty `SEARCH` section
|
||||
- The new file's contents in the `REPLACE` section
|
||||
|
||||
{lazy_prompt}
|
||||
ONLY EVER RETURN CODE IN A *SEARCH/REPLACE BLOCK*!
|
||||
"""
|
||||
system_reminder = ""
|
||||
|
|
|
@ -121,7 +121,6 @@ class Commands:
|
|||
def run(self, inp):
|
||||
if inp.startswith("!"):
|
||||
return self.do_run("run", inp[1:])
|
||||
return
|
||||
|
||||
res = self.matching_commands(inp)
|
||||
if res is None:
|
||||
|
|
30
aider/gui.py
30
aider/gui.py
|
@ -6,6 +6,7 @@ import sys
|
|||
|
||||
import streamlit as st
|
||||
|
||||
from aider import urls
|
||||
from aider.coders import Coder
|
||||
from aider.dump import dump # noqa: F401
|
||||
from aider.io import InputOutput
|
||||
|
@ -18,9 +19,11 @@ class CaptureIO(InputOutput):
|
|||
|
||||
def tool_output(self, msg):
|
||||
self.lines.append(msg)
|
||||
super().tool_output(msg)
|
||||
|
||||
def tool_error(self, msg):
|
||||
self.lines.append(msg)
|
||||
super().tool_error(msg)
|
||||
|
||||
def get_captured_lines(self):
|
||||
lines = self.lines
|
||||
|
@ -75,6 +78,9 @@ def get_coder():
|
|||
# coder.io = io # this breaks the input_history
|
||||
coder.commands.io = io
|
||||
|
||||
for line in coder.get_announcements():
|
||||
coder.io.tool_output(line)
|
||||
|
||||
return coder
|
||||
|
||||
|
||||
|
@ -159,12 +165,12 @@ class GUI:
|
|||
pass
|
||||
|
||||
def do_recommended_actions(self):
|
||||
text = "Aider works best when your code is stored in a git repo. \n"
|
||||
text += f"[See the FAQ for more info]({urls.git})"
|
||||
|
||||
with st.expander("Recommended actions", expanded=True):
|
||||
with st.popover("Create a git repo to track changes"):
|
||||
st.write(
|
||||
"Aider works best when your code is stored in a git repo. \n[See the FAQ"
|
||||
" for more info](https://aider.chat/docs/git.html)"
|
||||
)
|
||||
st.write(text)
|
||||
self.button("Create git repo", key=random.random(), help="?")
|
||||
|
||||
with st.popover("Update your `.gitignore` file"):
|
||||
|
@ -405,14 +411,22 @@ class GUI:
|
|||
prompt = self.state.prompt
|
||||
self.state.prompt = None
|
||||
|
||||
# This duplicates logic from within Coder
|
||||
self.num_reflections = 0
|
||||
self.max_reflections = 3
|
||||
|
||||
while prompt:
|
||||
with self.messages.chat_message("assistant"):
|
||||
res = st.write_stream(self.coder.run_stream(prompt))
|
||||
self.state.messages.append({"role": "assistant", "content": res})
|
||||
# self.cost()
|
||||
|
||||
prompt = None
|
||||
if self.coder.reflected_message:
|
||||
self.info(self.coder.reflected_message)
|
||||
prompt = self.coder.reflected_message
|
||||
if self.num_reflections < self.max_reflections:
|
||||
self.num_reflections += 1
|
||||
self.info(self.coder.reflected_message)
|
||||
prompt = self.coder.reflected_message
|
||||
|
||||
with self.messages:
|
||||
edit = dict(
|
||||
|
@ -513,9 +527,9 @@ def gui_main():
|
|||
st.set_page_config(
|
||||
layout="wide",
|
||||
page_title="Aider",
|
||||
page_icon="https://aider.chat/assets/favicon-32x32.png",
|
||||
page_icon=urls.favicon,
|
||||
menu_items={
|
||||
"Get Help": "https://aider.chat/",
|
||||
"Get Help": urls.website,
|
||||
"Report a bug": "https://github.com/paul-gauthier/aider/issues",
|
||||
"About": "# Aider\nAI pair programming in your browser.",
|
||||
},
|
||||
|
|
|
@ -5,6 +5,7 @@ from datetime import datetime
|
|||
from pathlib import Path
|
||||
|
||||
from prompt_toolkit.completion import Completer, Completion
|
||||
from prompt_toolkit.enums import EditingMode
|
||||
from prompt_toolkit.history import FileHistory
|
||||
from prompt_toolkit.key_binding import KeyBindings
|
||||
from prompt_toolkit.lexers import PygmentsLexer
|
||||
|
@ -107,7 +108,9 @@ class InputOutput:
|
|||
encoding="utf-8",
|
||||
dry_run=False,
|
||||
llm_history_file=None,
|
||||
editingmode=EditingMode.EMACS,
|
||||
):
|
||||
self.editingmode = editingmode
|
||||
no_color = os.environ.get("NO_COLOR")
|
||||
if no_color is not None and no_color != "":
|
||||
pretty = False
|
||||
|
@ -237,7 +240,9 @@ class InputOutput:
|
|||
def _(event):
|
||||
event.current_buffer.insert_text("\n")
|
||||
|
||||
session = PromptSession(key_bindings=kb, **session_kwargs)
|
||||
session = PromptSession(
|
||||
key_bindings=kb, editing_mode=self.editingmode, **session_kwargs
|
||||
)
|
||||
line = session.prompt()
|
||||
|
||||
if line and line[0] == "{" and not multiline_input:
|
||||
|
|
|
@ -6,6 +6,7 @@ from pathlib import Path
|
|||
|
||||
import git
|
||||
from dotenv import load_dotenv
|
||||
from prompt_toolkit.enums import EditingMode
|
||||
from streamlit.web import cli
|
||||
|
||||
from aider import __version__, models, utils
|
||||
|
@ -66,7 +67,7 @@ def setup_git(git_root, io):
|
|||
with repo.config_reader() as config:
|
||||
try:
|
||||
user_name = config.get_value("user", "name", None)
|
||||
except configparser.NoSectionError:
|
||||
except (configparser.NoSectionError, configparser.NoOptionError):
|
||||
pass
|
||||
try:
|
||||
user_email = config.get_value("user", "email", None)
|
||||
|
@ -246,6 +247,8 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F
|
|||
if return_coder and args.yes is None:
|
||||
args.yes = True
|
||||
|
||||
editing_mode = EditingMode.VI if args.vim else EditingMode.EMACS
|
||||
|
||||
io = InputOutput(
|
||||
args.pretty,
|
||||
args.yes,
|
||||
|
@ -259,6 +262,7 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F
|
|||
dry_run=args.dry_run,
|
||||
encoding=args.encoding,
|
||||
llm_history_file=args.llm_history_file,
|
||||
editingmode=editing_mode,
|
||||
)
|
||||
|
||||
fnames = [str(Path(fn).resolve()) for fn in args.files]
|
||||
|
@ -333,6 +337,26 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F
|
|||
if args.openai_organization_id:
|
||||
os.environ["OPENAI_ORGANIZATION"] = args.openai_organization_id
|
||||
|
||||
model_def_files = []
|
||||
model_def_fname = Path(".aider.models.json")
|
||||
model_def_files.append(Path.home() / model_def_fname) # homedir
|
||||
if git_root:
|
||||
model_def_files.append(Path(git_root) / model_def_fname) # git root
|
||||
if args.model_metadata_file:
|
||||
model_def_files.append(args.model_metadata_file)
|
||||
model_def_files.append(model_def_fname.resolve())
|
||||
model_def_files = list(map(str, model_def_files))
|
||||
model_def_files = list(dict.fromkeys(model_def_files))
|
||||
try:
|
||||
model_metadata_files_loaded = models.register_models(model_def_files)
|
||||
if len(model_metadata_files_loaded) > 0:
|
||||
io.tool_output(f"Loaded {len(model_metadata_files_loaded)} model file(s)")
|
||||
for model_metadata_file in model_metadata_files_loaded:
|
||||
io.tool_output(f" - {model_metadata_file}")
|
||||
except Exception as e:
|
||||
io.tool_error(f"Error loading model info/cost: {e}")
|
||||
return 1
|
||||
|
||||
main_model = models.Model(args.model, weak_model=args.weak_model)
|
||||
|
||||
lint_cmds = parse_lint_cmds(args.lint_cmd, io)
|
||||
|
@ -389,7 +413,10 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F
|
|||
return
|
||||
|
||||
if args.commit:
|
||||
coder.commands.cmd_commit()
|
||||
if args.dry_run:
|
||||
io.tool_output("Dry run enabled, skipping commit.")
|
||||
else:
|
||||
coder.commands.cmd_commit()
|
||||
return
|
||||
|
||||
if args.lint:
|
||||
|
|
|
@ -8,6 +8,7 @@ from typing import Optional
|
|||
|
||||
from PIL import Image
|
||||
|
||||
from aider import urls
|
||||
from aider.dump import dump # noqa: F401
|
||||
from aider.litellm import litellm
|
||||
|
||||
|
@ -426,6 +427,23 @@ class Model:
|
|||
return res
|
||||
|
||||
|
||||
def register_models(model_def_fnames):
|
||||
model_metadata_files_loaded = []
|
||||
for model_def_fname in model_def_fnames:
|
||||
if not os.path.exists(model_def_fname):
|
||||
continue
|
||||
model_metadata_files_loaded.append(model_def_fname)
|
||||
try:
|
||||
with open(model_def_fname, "r") as model_def_file:
|
||||
model_def = json.load(model_def_file)
|
||||
except json.JSONDecodeError as e:
|
||||
raise Exception(f"Error loading model definition from {model_def_fname}: {e}")
|
||||
|
||||
litellm.register_model(model_def)
|
||||
|
||||
return model_metadata_files_loaded
|
||||
|
||||
|
||||
def validate_variables(vars):
|
||||
missing = []
|
||||
for var in vars:
|
||||
|
@ -452,17 +470,17 @@ def sanity_check_model(io, model):
|
|||
io.tool_error(f"- {key}")
|
||||
elif not model.keys_in_environment:
|
||||
show = True
|
||||
io.tool_error(f"Model {model}: Unknown which environment variables are required.")
|
||||
io.tool_output(f"Model {model}: Unknown which environment variables are required.")
|
||||
|
||||
if not model.info:
|
||||
show = True
|
||||
io.tool_error(
|
||||
io.tool_output(
|
||||
f"Model {model}: Unknown model, context window size and token costs unavailable."
|
||||
)
|
||||
|
||||
possible_matches = fuzzy_match_models(model.name)
|
||||
if possible_matches:
|
||||
io.tool_error("Did you mean one of these?")
|
||||
io.tool_output("Did you mean one of these?")
|
||||
for match in possible_matches:
|
||||
fq, m = match
|
||||
if fq == m:
|
||||
|
@ -471,7 +489,7 @@ def sanity_check_model(io, model):
|
|||
io.tool_error(f"- {m} ({fq})")
|
||||
|
||||
if show:
|
||||
io.tool_error("For more info see https://aider.chat/docs/llms/warnings.html")
|
||||
io.tool_error(urls.model_warnings)
|
||||
|
||||
|
||||
def fuzzy_match_models(name):
|
||||
|
|
|
@ -4,22 +4,24 @@ import re
|
|||
import sys
|
||||
|
||||
import httpx
|
||||
import playwright
|
||||
import pypandoc
|
||||
from bs4 import BeautifulSoup
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
from aider import __version__
|
||||
from aider import __version__, urls
|
||||
from aider.dump import dump
|
||||
|
||||
aider_user_agent = f"Aider/{__version__} +https://aider.chat"
|
||||
aider_user_agent = f"Aider/{__version__} +{urls.website}"
|
||||
|
||||
# Playwright is nice because it has a simple way to install dependencies on most
|
||||
# platforms.
|
||||
PLAYWRIGHT_INFO = """
|
||||
PLAYWRIGHT_INFO = f"""
|
||||
For better web scraping, install Playwright chromium with this command in your terminal:
|
||||
|
||||
playwright install --with-deps chromium
|
||||
|
||||
See https://aider.chat/docs/install/optional.html#enable-playwright for more info.
|
||||
See {urls.enable_playwrite} for more info.
|
||||
"""
|
||||
|
||||
|
||||
|
@ -51,6 +53,7 @@ class Scraper:
|
|||
else:
|
||||
content = self.scrape_with_httpx(url)
|
||||
|
||||
dump(content)
|
||||
if not content:
|
||||
return
|
||||
|
||||
|
@ -79,7 +82,10 @@ class Scraper:
|
|||
user_agent += " " + aider_user_agent
|
||||
|
||||
page = browser.new_page(user_agent=user_agent)
|
||||
page.goto(url)
|
||||
try:
|
||||
page.goto(url, wait_until="networkidle", timeout=5000)
|
||||
except playwright._impl._errors.TimeoutError:
|
||||
pass
|
||||
content = page.content()
|
||||
browser.close()
|
||||
|
||||
|
|
0
aider/tests/__init__.py
Normal file
0
aider/tests/__init__.py
Normal file
612
aider/tests/test_coder.py
Normal file
612
aider/tests/test_coder.py
Normal file
|
@ -0,0 +1,612 @@
|
|||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import git
|
||||
import openai
|
||||
|
||||
from aider.coders import Coder
|
||||
from aider.dump import dump # noqa: F401
|
||||
from aider.io import InputOutput
|
||||
from aider.models import Model
|
||||
from aider.utils import ChdirTemporaryDirectory, GitTemporaryDirectory
|
||||
|
||||
|
||||
class TestCoder(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.GPT35 = Model("gpt-3.5-turbo")
|
||||
|
||||
def test_allowed_to_edit(self):
|
||||
with GitTemporaryDirectory():
|
||||
repo = git.Repo()
|
||||
|
||||
fname = Path("added.txt")
|
||||
fname.touch()
|
||||
repo.git.add(str(fname))
|
||||
|
||||
fname = Path("repo.txt")
|
||||
fname.touch()
|
||||
repo.git.add(str(fname))
|
||||
|
||||
repo.git.commit("-m", "init")
|
||||
|
||||
# YES!
|
||||
io = InputOutput(yes=True)
|
||||
coder = Coder.create(self.GPT35, None, io, fnames=["added.txt"])
|
||||
|
||||
self.assertTrue(coder.allowed_to_edit("added.txt"))
|
||||
self.assertTrue(coder.allowed_to_edit("repo.txt"))
|
||||
self.assertTrue(coder.allowed_to_edit("new.txt"))
|
||||
|
||||
self.assertIn("repo.txt", str(coder.abs_fnames))
|
||||
self.assertIn("new.txt", str(coder.abs_fnames))
|
||||
|
||||
self.assertFalse(coder.need_commit_before_edits)
|
||||
|
||||
def test_allowed_to_edit_no(self):
|
||||
with GitTemporaryDirectory():
|
||||
repo = git.Repo()
|
||||
|
||||
fname = Path("added.txt")
|
||||
fname.touch()
|
||||
repo.git.add(str(fname))
|
||||
|
||||
fname = Path("repo.txt")
|
||||
fname.touch()
|
||||
repo.git.add(str(fname))
|
||||
|
||||
repo.git.commit("-m", "init")
|
||||
|
||||
# say NO
|
||||
io = InputOutput(yes=False)
|
||||
|
||||
coder = Coder.create(self.GPT35, None, io, fnames=["added.txt"])
|
||||
|
||||
self.assertTrue(coder.allowed_to_edit("added.txt"))
|
||||
self.assertFalse(coder.allowed_to_edit("repo.txt"))
|
||||
self.assertFalse(coder.allowed_to_edit("new.txt"))
|
||||
|
||||
self.assertNotIn("repo.txt", str(coder.abs_fnames))
|
||||
self.assertNotIn("new.txt", str(coder.abs_fnames))
|
||||
|
||||
self.assertFalse(coder.need_commit_before_edits)
|
||||
|
||||
def test_allowed_to_edit_dirty(self):
|
||||
with GitTemporaryDirectory():
|
||||
repo = git.Repo()
|
||||
|
||||
fname = Path("added.txt")
|
||||
fname.touch()
|
||||
repo.git.add(str(fname))
|
||||
|
||||
repo.git.commit("-m", "init")
|
||||
|
||||
# say NO
|
||||
io = InputOutput(yes=False)
|
||||
|
||||
coder = Coder.create(self.GPT35, None, io, fnames=["added.txt"])
|
||||
|
||||
self.assertTrue(coder.allowed_to_edit("added.txt"))
|
||||
self.assertFalse(coder.need_commit_before_edits)
|
||||
|
||||
fname.write_text("dirty!")
|
||||
self.assertTrue(coder.allowed_to_edit("added.txt"))
|
||||
self.assertTrue(coder.need_commit_before_edits)
|
||||
|
||||
def test_get_last_modified(self):
|
||||
# Mock the IO object
|
||||
mock_io = MagicMock()
|
||||
|
||||
with GitTemporaryDirectory():
|
||||
repo = git.Repo(Path.cwd())
|
||||
fname = Path("new.txt")
|
||||
fname.touch()
|
||||
repo.git.add(str(fname))
|
||||
repo.git.commit("-m", "new")
|
||||
|
||||
# Initialize the Coder object with the mocked IO and mocked repo
|
||||
coder = Coder.create(self.GPT35, None, mock_io)
|
||||
|
||||
mod = coder.get_last_modified()
|
||||
|
||||
fname.write_text("hi")
|
||||
mod_newer = coder.get_last_modified()
|
||||
self.assertLess(mod, mod_newer)
|
||||
|
||||
fname.unlink()
|
||||
self.assertEqual(coder.get_last_modified(), 0)
|
||||
|
||||
def test_get_files_content(self):
|
||||
tempdir = Path(tempfile.mkdtemp())
|
||||
|
||||
file1 = tempdir / "file1.txt"
|
||||
file2 = tempdir / "file2.txt"
|
||||
|
||||
file1.touch()
|
||||
file2.touch()
|
||||
|
||||
files = [file1, file2]
|
||||
|
||||
# Initialize the Coder object with the mocked IO and mocked repo
|
||||
coder = Coder.create(self.GPT35, None, io=InputOutput(), fnames=files)
|
||||
|
||||
content = coder.get_files_content().splitlines()
|
||||
self.assertIn("file1.txt", content)
|
||||
self.assertIn("file2.txt", content)
|
||||
|
||||
def test_check_for_filename_mentions(self):
|
||||
with GitTemporaryDirectory():
|
||||
repo = git.Repo()
|
||||
|
||||
mock_io = MagicMock()
|
||||
|
||||
fname1 = Path("file1.txt")
|
||||
fname2 = Path("file2.py")
|
||||
|
||||
fname1.write_text("one\n")
|
||||
fname2.write_text("two\n")
|
||||
|
||||
repo.git.add(str(fname1))
|
||||
repo.git.add(str(fname2))
|
||||
repo.git.commit("-m", "new")
|
||||
|
||||
# Initialize the Coder object with the mocked IO and mocked repo
|
||||
coder = Coder.create(self.GPT35, None, mock_io)
|
||||
|
||||
# Call the check_for_file_mentions method
|
||||
coder.check_for_file_mentions("Please check file1.txt and file2.py")
|
||||
|
||||
# Check if coder.abs_fnames contains both files
|
||||
expected_files = set(
|
||||
[
|
||||
str(Path(coder.root) / fname1),
|
||||
str(Path(coder.root) / fname2),
|
||||
]
|
||||
)
|
||||
|
||||
self.assertEqual(coder.abs_fnames, expected_files)
|
||||
|
||||
def test_check_for_ambiguous_filename_mentions_of_longer_paths(self):
|
||||
with GitTemporaryDirectory():
|
||||
io = InputOutput(pretty=False, yes=True)
|
||||
coder = Coder.create(self.GPT35, None, io)
|
||||
|
||||
fname = Path("file1.txt")
|
||||
fname.touch()
|
||||
|
||||
other_fname = Path("other") / "file1.txt"
|
||||
other_fname.parent.mkdir(parents=True, exist_ok=True)
|
||||
other_fname.touch()
|
||||
|
||||
mock = MagicMock()
|
||||
mock.return_value = set([str(fname), str(other_fname)])
|
||||
coder.repo.get_tracked_files = mock
|
||||
|
||||
# Call the check_for_file_mentions method
|
||||
coder.check_for_file_mentions(f"Please check {fname}!")
|
||||
|
||||
self.assertEqual(coder.abs_fnames, set([str(fname.resolve())]))
|
||||
|
||||
def test_check_for_subdir_mention(self):
|
||||
with GitTemporaryDirectory():
|
||||
io = InputOutput(pretty=False, yes=True)
|
||||
coder = Coder.create(self.GPT35, None, io)
|
||||
|
||||
fname = Path("other") / "file1.txt"
|
||||
fname.parent.mkdir(parents=True, exist_ok=True)
|
||||
fname.touch()
|
||||
|
||||
mock = MagicMock()
|
||||
mock.return_value = set([str(fname)])
|
||||
coder.repo.get_tracked_files = mock
|
||||
|
||||
# Call the check_for_file_mentions method
|
||||
coder.check_for_file_mentions(f"Please check `{fname}`")
|
||||
|
||||
self.assertEqual(coder.abs_fnames, set([str(fname.resolve())]))
|
||||
|
||||
def test_run_with_file_deletion(self):
|
||||
# Create a few temporary files
|
||||
|
||||
tempdir = Path(tempfile.mkdtemp())
|
||||
|
||||
file1 = tempdir / "file1.txt"
|
||||
file2 = tempdir / "file2.txt"
|
||||
|
||||
file1.touch()
|
||||
file2.touch()
|
||||
|
||||
files = [file1, file2]
|
||||
|
||||
# Initialize the Coder object with the mocked IO and mocked repo
|
||||
coder = Coder.create(self.GPT35, None, io=InputOutput(), fnames=files)
|
||||
|
||||
def mock_send(*args, **kwargs):
|
||||
coder.partial_response_content = "ok"
|
||||
coder.partial_response_function_call = dict()
|
||||
return []
|
||||
|
||||
coder.send = mock_send
|
||||
|
||||
# Call the run method with a message
|
||||
coder.run(with_message="hi")
|
||||
self.assertEqual(len(coder.abs_fnames), 2)
|
||||
|
||||
file1.unlink()
|
||||
|
||||
# Call the run method again with a message
|
||||
coder.run(with_message="hi")
|
||||
self.assertEqual(len(coder.abs_fnames), 1)
|
||||
|
||||
def test_run_with_file_unicode_error(self):
|
||||
# Create a few temporary files
|
||||
_, file1 = tempfile.mkstemp()
|
||||
_, file2 = tempfile.mkstemp()
|
||||
|
||||
files = [file1, file2]
|
||||
|
||||
# Initialize the Coder object with the mocked IO and mocked repo
|
||||
coder = Coder.create(self.GPT35, None, io=InputOutput(), fnames=files)
|
||||
|
||||
def mock_send(*args, **kwargs):
|
||||
coder.partial_response_content = "ok"
|
||||
coder.partial_response_function_call = dict()
|
||||
return []
|
||||
|
||||
coder.send = mock_send
|
||||
|
||||
# Call the run method with a message
|
||||
coder.run(with_message="hi")
|
||||
self.assertEqual(len(coder.abs_fnames), 2)
|
||||
|
||||
# Write some non-UTF8 text into the file
|
||||
with open(file1, "wb") as f:
|
||||
f.write(b"\x80abc")
|
||||
|
||||
# Call the run method again with a message
|
||||
coder.run(with_message="hi")
|
||||
self.assertEqual(len(coder.abs_fnames), 1)
|
||||
|
||||
def test_choose_fence(self):
|
||||
# Create a few temporary files
|
||||
_, file1 = tempfile.mkstemp()
|
||||
|
||||
with open(file1, "wb") as f:
|
||||
f.write(b"this contains ``` backticks")
|
||||
|
||||
files = [file1]
|
||||
|
||||
# Initialize the Coder object with the mocked IO and mocked repo
|
||||
coder = Coder.create(self.GPT35, None, io=InputOutput(), fnames=files)
|
||||
|
||||
def mock_send(*args, **kwargs):
|
||||
coder.partial_response_content = "ok"
|
||||
coder.partial_response_function_call = dict()
|
||||
return []
|
||||
|
||||
coder.send = mock_send
|
||||
|
||||
# Call the run method with a message
|
||||
coder.run(with_message="hi")
|
||||
|
||||
self.assertNotEqual(coder.fence[0], "```")
|
||||
|
||||
def test_run_with_file_utf_unicode_error(self):
|
||||
"make sure that we honor InputOutput(encoding) and don't just assume utf-8"
|
||||
# Create a few temporary files
|
||||
_, file1 = tempfile.mkstemp()
|
||||
_, file2 = tempfile.mkstemp()
|
||||
|
||||
files = [file1, file2]
|
||||
|
||||
encoding = "utf-16"
|
||||
|
||||
# Initialize the Coder object with the mocked IO and mocked repo
|
||||
coder = Coder.create(
|
||||
self.GPT35,
|
||||
None,
|
||||
io=InputOutput(encoding=encoding),
|
||||
fnames=files,
|
||||
)
|
||||
|
||||
def mock_send(*args, **kwargs):
|
||||
coder.partial_response_content = "ok"
|
||||
coder.partial_response_function_call = dict()
|
||||
return []
|
||||
|
||||
coder.send = mock_send
|
||||
|
||||
# Call the run method with a message
|
||||
coder.run(with_message="hi")
|
||||
self.assertEqual(len(coder.abs_fnames), 2)
|
||||
|
||||
some_content_which_will_error_if_read_with_encoding_utf8 = "ÅÍÎÏ".encode(encoding)
|
||||
with open(file1, "wb") as f:
|
||||
f.write(some_content_which_will_error_if_read_with_encoding_utf8)
|
||||
|
||||
coder.run(with_message="hi")
|
||||
|
||||
# both files should still be here
|
||||
self.assertEqual(len(coder.abs_fnames), 2)
|
||||
|
||||
def test_run_with_invalid_request_error(self):
|
||||
with ChdirTemporaryDirectory():
|
||||
# Mock the IO object
|
||||
mock_io = MagicMock()
|
||||
|
||||
# Initialize the Coder object with the mocked IO and mocked repo
|
||||
coder = Coder.create(self.GPT35, None, mock_io)
|
||||
|
||||
# Call the run method and assert that InvalidRequestError is raised
|
||||
with self.assertRaises(openai.BadRequestError):
|
||||
with patch("litellm.completion") as Mock:
|
||||
Mock.side_effect = openai.BadRequestError(
|
||||
message="Invalid request",
|
||||
response=MagicMock(),
|
||||
body=None,
|
||||
)
|
||||
|
||||
coder.run(with_message="hi")
|
||||
|
||||
def test_new_file_edit_one_commit(self):
|
||||
"""A new file shouldn't get pre-committed before the GPT edit commit"""
|
||||
with GitTemporaryDirectory():
|
||||
repo = git.Repo()
|
||||
|
||||
fname = Path("file.txt")
|
||||
|
||||
io = InputOutput(yes=True)
|
||||
coder = Coder.create(self.GPT35, "diff", io=io, fnames=[str(fname)])
|
||||
|
||||
self.assertTrue(fname.exists())
|
||||
|
||||
# make sure it was not committed
|
||||
with self.assertRaises(git.exc.GitCommandError):
|
||||
list(repo.iter_commits(repo.active_branch.name))
|
||||
|
||||
def mock_send(*args, **kwargs):
|
||||
coder.partial_response_content = f"""
|
||||
Do this:
|
||||
|
||||
{str(fname)}
|
||||
<<<<<<< SEARCH
|
||||
=======
|
||||
new
|
||||
>>>>>>> REPLACE
|
||||
|
||||
"""
|
||||
coder.partial_response_function_call = dict()
|
||||
return []
|
||||
|
||||
coder.send = mock_send
|
||||
coder.repo.get_commit_message = MagicMock()
|
||||
coder.repo.get_commit_message.return_value = "commit message"
|
||||
|
||||
coder.run(with_message="hi")
|
||||
|
||||
content = fname.read_text()
|
||||
self.assertEqual(content, "new\n")
|
||||
|
||||
num_commits = len(list(repo.iter_commits(repo.active_branch.name)))
|
||||
self.assertEqual(num_commits, 1)
|
||||
|
||||
def test_only_commit_gpt_edited_file(self):
|
||||
"""
|
||||
Only commit file that gpt edits, not other dirty files.
|
||||
Also ensure commit msg only depends on diffs from the GPT edited file.
|
||||
"""
|
||||
|
||||
with GitTemporaryDirectory():
|
||||
repo = git.Repo()
|
||||
|
||||
fname1 = Path("file1.txt")
|
||||
fname2 = Path("file2.txt")
|
||||
|
||||
fname1.write_text("one\n")
|
||||
fname2.write_text("two\n")
|
||||
|
||||
repo.git.add(str(fname1))
|
||||
repo.git.add(str(fname2))
|
||||
repo.git.commit("-m", "new")
|
||||
|
||||
# DIRTY!
|
||||
fname1.write_text("ONE\n")
|
||||
|
||||
io = InputOutput(yes=True)
|
||||
coder = Coder.create(self.GPT35, "diff", io=io, fnames=[str(fname1), str(fname2)])
|
||||
|
||||
def mock_send(*args, **kwargs):
|
||||
coder.partial_response_content = f"""
|
||||
Do this:
|
||||
|
||||
{str(fname2)}
|
||||
<<<<<<< SEARCH
|
||||
two
|
||||
=======
|
||||
TWO
|
||||
>>>>>>> REPLACE
|
||||
|
||||
"""
|
||||
coder.partial_response_function_call = dict()
|
||||
return []
|
||||
|
||||
def mock_get_commit_message(diffs, context):
|
||||
self.assertNotIn("one", diffs)
|
||||
self.assertNotIn("ONE", diffs)
|
||||
return "commit message"
|
||||
|
||||
coder.send = mock_send
|
||||
coder.repo.get_commit_message = MagicMock(side_effect=mock_get_commit_message)
|
||||
|
||||
coder.run(with_message="hi")
|
||||
|
||||
content = fname2.read_text()
|
||||
self.assertEqual(content, "TWO\n")
|
||||
|
||||
self.assertTrue(repo.is_dirty(path=str(fname1)))
|
||||
|
||||
def test_gpt_edit_to_dirty_file(self):
|
||||
"""A dirty file should be committed before the GPT edits are committed"""
|
||||
|
||||
with GitTemporaryDirectory():
|
||||
repo = git.Repo()
|
||||
|
||||
fname = Path("file.txt")
|
||||
fname.write_text("one\n")
|
||||
repo.git.add(str(fname))
|
||||
|
||||
fname2 = Path("other.txt")
|
||||
fname2.write_text("other\n")
|
||||
repo.git.add(str(fname2))
|
||||
|
||||
repo.git.commit("-m", "new")
|
||||
|
||||
# dirty
|
||||
fname.write_text("two\n")
|
||||
fname2.write_text("OTHER\n")
|
||||
|
||||
io = InputOutput(yes=True)
|
||||
coder = Coder.create(self.GPT35, "diff", io=io, fnames=[str(fname)])
|
||||
|
||||
def mock_send(*args, **kwargs):
|
||||
coder.partial_response_content = f"""
|
||||
Do this:
|
||||
|
||||
{str(fname)}
|
||||
<<<<<<< SEARCH
|
||||
two
|
||||
=======
|
||||
three
|
||||
>>>>>>> REPLACE
|
||||
|
||||
"""
|
||||
coder.partial_response_function_call = dict()
|
||||
return []
|
||||
|
||||
saved_diffs = []
|
||||
|
||||
def mock_get_commit_message(diffs, context):
|
||||
saved_diffs.append(diffs)
|
||||
return "commit message"
|
||||
|
||||
coder.repo.get_commit_message = MagicMock(side_effect=mock_get_commit_message)
|
||||
coder.send = mock_send
|
||||
|
||||
coder.run(with_message="hi")
|
||||
|
||||
content = fname.read_text()
|
||||
self.assertEqual(content, "three\n")
|
||||
|
||||
num_commits = len(list(repo.iter_commits(repo.active_branch.name)))
|
||||
self.assertEqual(num_commits, 3)
|
||||
|
||||
diff = repo.git.diff(["HEAD~2", "HEAD~1"])
|
||||
self.assertIn("one", diff)
|
||||
self.assertIn("two", diff)
|
||||
self.assertNotIn("three", diff)
|
||||
self.assertNotIn("other", diff)
|
||||
self.assertNotIn("OTHER", diff)
|
||||
|
||||
diff = saved_diffs[0]
|
||||
self.assertIn("one", diff)
|
||||
self.assertIn("two", diff)
|
||||
self.assertNotIn("three", diff)
|
||||
self.assertNotIn("other", diff)
|
||||
self.assertNotIn("OTHER", diff)
|
||||
|
||||
diff = repo.git.diff(["HEAD~1", "HEAD"])
|
||||
self.assertNotIn("one", diff)
|
||||
self.assertIn("two", diff)
|
||||
self.assertIn("three", diff)
|
||||
self.assertNotIn("other", diff)
|
||||
self.assertNotIn("OTHER", diff)
|
||||
|
||||
diff = saved_diffs[1]
|
||||
self.assertNotIn("one", diff)
|
||||
self.assertIn("two", diff)
|
||||
self.assertIn("three", diff)
|
||||
self.assertNotIn("other", diff)
|
||||
self.assertNotIn("OTHER", diff)
|
||||
|
||||
self.assertEqual(len(saved_diffs), 2)
|
||||
|
||||
def test_gpt_edit_to_existing_file_not_in_repo(self):
|
||||
with GitTemporaryDirectory():
|
||||
repo = git.Repo()
|
||||
|
||||
fname = Path("file.txt")
|
||||
fname.write_text("one\n")
|
||||
|
||||
fname2 = Path("other.txt")
|
||||
fname2.write_text("other\n")
|
||||
repo.git.add(str(fname2))
|
||||
|
||||
repo.git.commit("-m", "initial")
|
||||
|
||||
io = InputOutput(yes=True)
|
||||
coder = Coder.create(self.GPT35, "diff", io=io, fnames=[str(fname)])
|
||||
|
||||
def mock_send(*args, **kwargs):
|
||||
coder.partial_response_content = f"""
|
||||
Do this:
|
||||
|
||||
{str(fname)}
|
||||
<<<<<<< SEARCH
|
||||
one
|
||||
=======
|
||||
two
|
||||
>>>>>>> REPLACE
|
||||
|
||||
"""
|
||||
coder.partial_response_function_call = dict()
|
||||
return []
|
||||
|
||||
saved_diffs = []
|
||||
|
||||
def mock_get_commit_message(diffs, context):
|
||||
saved_diffs.append(diffs)
|
||||
return "commit message"
|
||||
|
||||
coder.repo.get_commit_message = MagicMock(side_effect=mock_get_commit_message)
|
||||
coder.send = mock_send
|
||||
|
||||
coder.run(with_message="hi")
|
||||
|
||||
content = fname.read_text()
|
||||
self.assertEqual(content, "two\n")
|
||||
|
||||
diff = saved_diffs[0]
|
||||
self.assertIn("file.txt", diff)
|
||||
|
||||
def test_skip_aiderignored_files(self):
|
||||
with GitTemporaryDirectory():
|
||||
repo = git.Repo()
|
||||
|
||||
fname1 = "ignoreme1.txt"
|
||||
fname2 = "ignoreme2.txt"
|
||||
fname3 = "dir/ignoreme3.txt"
|
||||
|
||||
Path(fname2).touch()
|
||||
repo.git.add(str(fname2))
|
||||
repo.git.commit("-m", "initial")
|
||||
|
||||
aignore = Path(".aiderignore")
|
||||
aignore.write_text(f"{fname1}\n{fname2}\ndir\n")
|
||||
|
||||
io = InputOutput(yes=True)
|
||||
coder = Coder.create(
|
||||
self.GPT35,
|
||||
None,
|
||||
io,
|
||||
fnames=[fname1, fname2, fname3],
|
||||
aider_ignore_file=str(aignore),
|
||||
)
|
||||
|
||||
self.assertNotIn(fname1, str(coder.abs_fnames))
|
||||
self.assertNotIn(fname2, str(coder.abs_fnames))
|
||||
self.assertNotIn(fname3, str(coder.abs_fnames))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
588
aider/tests/test_commands.py
Normal file
588
aider/tests/test_commands.py
Normal file
|
@ -0,0 +1,588 @@
|
|||
import codecs
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
from io import StringIO
|
||||
from pathlib import Path
|
||||
from unittest import TestCase
|
||||
|
||||
import git
|
||||
|
||||
from aider.coders import Coder
|
||||
from aider.commands import Commands
|
||||
from aider.dump import dump # noqa: F401
|
||||
from aider.io import InputOutput
|
||||
from aider.models import Model
|
||||
from aider.utils import ChdirTemporaryDirectory, GitTemporaryDirectory, make_repo
|
||||
|
||||
|
||||
class TestCommands(TestCase):
|
||||
def setUp(self):
|
||||
self.original_cwd = os.getcwd()
|
||||
self.tempdir = tempfile.mkdtemp()
|
||||
os.chdir(self.tempdir)
|
||||
|
||||
self.GPT35 = Model("gpt-3.5-turbo")
|
||||
|
||||
def tearDown(self):
|
||||
os.chdir(self.original_cwd)
|
||||
shutil.rmtree(self.tempdir, ignore_errors=True)
|
||||
|
||||
def test_cmd_add(self):
|
||||
# Initialize the Commands and InputOutput objects
|
||||
io = InputOutput(pretty=False, yes=True)
|
||||
from aider.coders import Coder
|
||||
|
||||
coder = Coder.create(self.GPT35, None, io)
|
||||
commands = Commands(io, coder)
|
||||
|
||||
# Call the cmd_add method with 'foo.txt' and 'bar.txt' as a single string
|
||||
commands.cmd_add("foo.txt bar.txt")
|
||||
|
||||
# Check if both files have been created in the temporary directory
|
||||
self.assertTrue(os.path.exists("foo.txt"))
|
||||
self.assertTrue(os.path.exists("bar.txt"))
|
||||
|
||||
def test_cmd_add_bad_glob(self):
|
||||
# https://github.com/paul-gauthier/aider/issues/293
|
||||
|
||||
io = InputOutput(pretty=False, yes=False)
|
||||
from aider.coders import Coder
|
||||
|
||||
coder = Coder.create(self.GPT35, None, io)
|
||||
commands = Commands(io, coder)
|
||||
|
||||
commands.cmd_add("**.txt")
|
||||
|
||||
def test_cmd_add_with_glob_patterns(self):
|
||||
# Initialize the Commands and InputOutput objects
|
||||
io = InputOutput(pretty=False, yes=True)
|
||||
from aider.coders import Coder
|
||||
|
||||
coder = Coder.create(self.GPT35, None, io)
|
||||
commands = Commands(io, coder)
|
||||
|
||||
# Create some test files
|
||||
with open("test1.py", "w") as f:
|
||||
f.write("print('test1')")
|
||||
with open("test2.py", "w") as f:
|
||||
f.write("print('test2')")
|
||||
with open("test.txt", "w") as f:
|
||||
f.write("test")
|
||||
|
||||
# Call the cmd_add method with a glob pattern
|
||||
commands.cmd_add("*.py")
|
||||
|
||||
# Check if the Python files have been added to the chat session
|
||||
self.assertIn(str(Path("test1.py").resolve()), coder.abs_fnames)
|
||||
self.assertIn(str(Path("test2.py").resolve()), coder.abs_fnames)
|
||||
|
||||
# Check if the text file has not been added to the chat session
|
||||
self.assertNotIn(str(Path("test.txt").resolve()), coder.abs_fnames)
|
||||
|
||||
def test_cmd_add_no_match(self):
|
||||
# yes=False means we will *not* create the file when it is not found
|
||||
io = InputOutput(pretty=False, yes=False)
|
||||
from aider.coders import Coder
|
||||
|
||||
coder = Coder.create(self.GPT35, None, io)
|
||||
commands = Commands(io, coder)
|
||||
|
||||
# Call the cmd_add method with a non-existent file pattern
|
||||
commands.cmd_add("*.nonexistent")
|
||||
|
||||
# Check if no files have been added to the chat session
|
||||
self.assertEqual(len(coder.abs_fnames), 0)
|
||||
|
||||
def test_cmd_add_no_match_but_make_it(self):
|
||||
# yes=True means we *will* create the file when it is not found
|
||||
io = InputOutput(pretty=False, yes=True)
|
||||
from aider.coders import Coder
|
||||
|
||||
coder = Coder.create(self.GPT35, None, io)
|
||||
commands = Commands(io, coder)
|
||||
|
||||
fname = Path("[abc].nonexistent")
|
||||
|
||||
# Call the cmd_add method with a non-existent file pattern
|
||||
commands.cmd_add(str(fname))
|
||||
|
||||
# Check if no files have been added to the chat session
|
||||
self.assertEqual(len(coder.abs_fnames), 1)
|
||||
self.assertTrue(fname.exists())
|
||||
|
||||
def test_cmd_add_drop_directory(self):
|
||||
# Initialize the Commands and InputOutput objects
|
||||
io = InputOutput(pretty=False, yes=False)
|
||||
from aider.coders import Coder
|
||||
|
||||
coder = Coder.create(self.GPT35, None, io)
|
||||
commands = Commands(io, coder)
|
||||
|
||||
# Create a directory and add files to it using pathlib
|
||||
Path("test_dir").mkdir()
|
||||
Path("test_dir/another_dir").mkdir()
|
||||
Path("test_dir/test_file1.txt").write_text("Test file 1")
|
||||
Path("test_dir/test_file2.txt").write_text("Test file 2")
|
||||
Path("test_dir/another_dir/test_file.txt").write_text("Test file 3")
|
||||
|
||||
# Call the cmd_add method with a directory
|
||||
commands.cmd_add("test_dir test_dir/test_file2.txt")
|
||||
|
||||
# Check if the files have been added to the chat session
|
||||
self.assertIn(str(Path("test_dir/test_file1.txt").resolve()), coder.abs_fnames)
|
||||
self.assertIn(str(Path("test_dir/test_file2.txt").resolve()), coder.abs_fnames)
|
||||
self.assertIn(str(Path("test_dir/another_dir/test_file.txt").resolve()), coder.abs_fnames)
|
||||
|
||||
commands.cmd_drop("test_dir/another_dir")
|
||||
self.assertIn(str(Path("test_dir/test_file1.txt").resolve()), coder.abs_fnames)
|
||||
self.assertIn(str(Path("test_dir/test_file2.txt").resolve()), coder.abs_fnames)
|
||||
self.assertNotIn(
|
||||
str(Path("test_dir/another_dir/test_file.txt").resolve()), coder.abs_fnames
|
||||
)
|
||||
|
||||
# Issue #139 /add problems when cwd != git_root
|
||||
|
||||
# remember the proper abs path to this file
|
||||
abs_fname = str(Path("test_dir/another_dir/test_file.txt").resolve())
|
||||
|
||||
# chdir to someplace other than git_root
|
||||
Path("side_dir").mkdir()
|
||||
os.chdir("side_dir")
|
||||
|
||||
# add it via it's git_root referenced name
|
||||
commands.cmd_add("test_dir/another_dir/test_file.txt")
|
||||
|
||||
# it should be there, but was not in v0.10.0
|
||||
self.assertIn(abs_fname, coder.abs_fnames)
|
||||
|
||||
# drop it via it's git_root referenced name
|
||||
commands.cmd_drop("test_dir/another_dir/test_file.txt")
|
||||
|
||||
# it should be there, but was not in v0.10.0
|
||||
self.assertNotIn(abs_fname, coder.abs_fnames)
|
||||
|
||||
def test_cmd_drop_with_glob_patterns(self):
|
||||
# Initialize the Commands and InputOutput objects
|
||||
io = InputOutput(pretty=False, yes=True)
|
||||
from aider.coders import Coder
|
||||
|
||||
coder = Coder.create(self.GPT35, None, io)
|
||||
commands = Commands(io, coder)
|
||||
|
||||
subdir = Path("subdir")
|
||||
subdir.mkdir()
|
||||
(subdir / "subtest1.py").touch()
|
||||
(subdir / "subtest2.py").touch()
|
||||
|
||||
Path("test1.py").touch()
|
||||
Path("test2.py").touch()
|
||||
|
||||
# Add some files to the chat session
|
||||
commands.cmd_add("*.py")
|
||||
|
||||
self.assertEqual(len(coder.abs_fnames), 2)
|
||||
|
||||
# Call the cmd_drop method with a glob pattern
|
||||
commands.cmd_drop("*2.py")
|
||||
|
||||
self.assertIn(str(Path("test1.py").resolve()), coder.abs_fnames)
|
||||
self.assertNotIn(str(Path("test2.py").resolve()), coder.abs_fnames)
|
||||
|
||||
def test_cmd_add_bad_encoding(self):
|
||||
# Initialize the Commands and InputOutput objects
|
||||
io = InputOutput(pretty=False, yes=True)
|
||||
from aider.coders import Coder
|
||||
|
||||
coder = Coder.create(self.GPT35, None, io)
|
||||
commands = Commands(io, coder)
|
||||
|
||||
# Create a new file foo.bad which will fail to decode as utf-8
|
||||
with codecs.open("foo.bad", "w", encoding="iso-8859-15") as f:
|
||||
f.write("ÆØÅ") # Characters not present in utf-8
|
||||
|
||||
commands.cmd_add("foo.bad")
|
||||
|
||||
self.assertEqual(coder.abs_fnames, set())
|
||||
|
||||
def test_cmd_git(self):
|
||||
# Initialize the Commands and InputOutput objects
|
||||
io = InputOutput(pretty=False, yes=True)
|
||||
|
||||
with GitTemporaryDirectory() as tempdir:
|
||||
# Create a file in the temporary directory
|
||||
with open(f"{tempdir}/test.txt", "w") as f:
|
||||
f.write("test")
|
||||
|
||||
coder = Coder.create(self.GPT35, None, io)
|
||||
commands = Commands(io, coder)
|
||||
|
||||
# Run the cmd_git method with the arguments "commit -a -m msg"
|
||||
commands.cmd_git("add test.txt")
|
||||
commands.cmd_git("commit -a -m msg")
|
||||
|
||||
# Check if the file has been committed to the repository
|
||||
repo = git.Repo(tempdir)
|
||||
files_in_repo = repo.git.ls_files()
|
||||
self.assertIn("test.txt", files_in_repo)
|
||||
|
||||
def test_cmd_tokens(self):
|
||||
# Initialize the Commands and InputOutput objects
|
||||
io = InputOutput(pretty=False, yes=True)
|
||||
|
||||
coder = Coder.create(self.GPT35, None, io)
|
||||
commands = Commands(io, coder)
|
||||
|
||||
commands.cmd_add("foo.txt bar.txt")
|
||||
|
||||
# Redirect the standard output to an instance of io.StringIO
|
||||
stdout = StringIO()
|
||||
sys.stdout = stdout
|
||||
|
||||
commands.cmd_tokens("")
|
||||
|
||||
# Reset the standard output
|
||||
sys.stdout = sys.__stdout__
|
||||
|
||||
# Get the console output
|
||||
console_output = stdout.getvalue()
|
||||
|
||||
self.assertIn("foo.txt", console_output)
|
||||
self.assertIn("bar.txt", console_output)
|
||||
|
||||
def test_cmd_add_from_subdir(self):
|
||||
repo = git.Repo.init()
|
||||
repo.config_writer().set_value("user", "name", "Test User").release()
|
||||
repo.config_writer().set_value("user", "email", "testuser@example.com").release()
|
||||
|
||||
# Create three empty files and add them to the git repository
|
||||
filenames = ["one.py", Path("subdir") / "two.py", Path("anotherdir") / "three.py"]
|
||||
for filename in filenames:
|
||||
file_path = Path(filename)
|
||||
file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
file_path.touch()
|
||||
repo.git.add(str(file_path))
|
||||
repo.git.commit("-m", "added")
|
||||
|
||||
filenames = [str(Path(fn).resolve()) for fn in filenames]
|
||||
|
||||
###
|
||||
|
||||
os.chdir("subdir")
|
||||
|
||||
io = InputOutput(pretty=False, yes=True)
|
||||
coder = Coder.create(self.GPT35, None, io)
|
||||
commands = Commands(io, coder)
|
||||
|
||||
# this should get added
|
||||
commands.cmd_add(str(Path("anotherdir") / "three.py"))
|
||||
|
||||
# this should add one.py
|
||||
commands.cmd_add("*.py")
|
||||
|
||||
self.assertIn(filenames[0], coder.abs_fnames)
|
||||
self.assertNotIn(filenames[1], coder.abs_fnames)
|
||||
self.assertIn(filenames[2], coder.abs_fnames)
|
||||
|
||||
def test_cmd_add_from_subdir_again(self):
|
||||
with GitTemporaryDirectory():
|
||||
io = InputOutput(pretty=False, yes=False)
|
||||
from aider.coders import Coder
|
||||
|
||||
coder = Coder.create(self.GPT35, None, io)
|
||||
commands = Commands(io, coder)
|
||||
|
||||
Path("side_dir").mkdir()
|
||||
os.chdir("side_dir")
|
||||
|
||||
# add a file that is in the side_dir
|
||||
with open("temp.txt", "w"):
|
||||
pass
|
||||
|
||||
# this was blowing up with GitCommandError, per:
|
||||
# https://github.com/paul-gauthier/aider/issues/201
|
||||
commands.cmd_add("temp.txt")
|
||||
|
||||
def test_cmd_commit(self):
|
||||
with GitTemporaryDirectory():
|
||||
fname = "test.txt"
|
||||
with open(fname, "w") as f:
|
||||
f.write("test")
|
||||
repo = git.Repo()
|
||||
repo.git.add(fname)
|
||||
repo.git.commit("-m", "initial")
|
||||
|
||||
io = InputOutput(pretty=False, yes=True)
|
||||
coder = Coder.create(self.GPT35, None, io)
|
||||
commands = Commands(io, coder)
|
||||
|
||||
self.assertFalse(repo.is_dirty())
|
||||
with open(fname, "w") as f:
|
||||
f.write("new")
|
||||
self.assertTrue(repo.is_dirty())
|
||||
|
||||
commit_message = "Test commit message"
|
||||
commands.cmd_commit(commit_message)
|
||||
self.assertFalse(repo.is_dirty())
|
||||
|
||||
def test_cmd_add_from_outside_root(self):
|
||||
with ChdirTemporaryDirectory() as tmp_dname:
|
||||
root = Path("root")
|
||||
root.mkdir()
|
||||
os.chdir(str(root))
|
||||
|
||||
io = InputOutput(pretty=False, yes=False)
|
||||
from aider.coders import Coder
|
||||
|
||||
coder = Coder.create(self.GPT35, None, io)
|
||||
commands = Commands(io, coder)
|
||||
|
||||
outside_file = Path(tmp_dname) / "outside.txt"
|
||||
outside_file.touch()
|
||||
|
||||
# This should not be allowed!
|
||||
# https://github.com/paul-gauthier/aider/issues/178
|
||||
commands.cmd_add("../outside.txt")
|
||||
|
||||
self.assertEqual(len(coder.abs_fnames), 0)
|
||||
|
||||
def test_cmd_add_from_outside_git(self):
|
||||
with ChdirTemporaryDirectory() as tmp_dname:
|
||||
root = Path("root")
|
||||
root.mkdir()
|
||||
os.chdir(str(root))
|
||||
|
||||
make_repo()
|
||||
|
||||
io = InputOutput(pretty=False, yes=False)
|
||||
from aider.coders import Coder
|
||||
|
||||
coder = Coder.create(self.GPT35, None, io)
|
||||
commands = Commands(io, coder)
|
||||
|
||||
outside_file = Path(tmp_dname) / "outside.txt"
|
||||
outside_file.touch()
|
||||
|
||||
# This should not be allowed!
|
||||
# It was blowing up with GitCommandError, per:
|
||||
# https://github.com/paul-gauthier/aider/issues/178
|
||||
commands.cmd_add("../outside.txt")
|
||||
|
||||
self.assertEqual(len(coder.abs_fnames), 0)
|
||||
|
||||
def test_cmd_add_filename_with_special_chars(self):
|
||||
with ChdirTemporaryDirectory():
|
||||
io = InputOutput(pretty=False, yes=False)
|
||||
from aider.coders import Coder
|
||||
|
||||
coder = Coder.create(self.GPT35, None, io)
|
||||
commands = Commands(io, coder)
|
||||
|
||||
fname = Path("with[brackets].txt")
|
||||
fname.touch()
|
||||
|
||||
commands.cmd_add(str(fname))
|
||||
|
||||
self.assertIn(str(fname.resolve()), coder.abs_fnames)
|
||||
|
||||
def test_cmd_add_dirname_with_special_chars(self):
|
||||
with ChdirTemporaryDirectory():
|
||||
io = InputOutput(pretty=False, yes=False)
|
||||
from aider.coders import Coder
|
||||
|
||||
coder = Coder.create(self.GPT35, None, io)
|
||||
commands = Commands(io, coder)
|
||||
|
||||
dname = Path("with[brackets]")
|
||||
dname.mkdir()
|
||||
fname = dname / "filename.txt"
|
||||
fname.touch()
|
||||
|
||||
commands.cmd_add(str(dname))
|
||||
|
||||
self.assertIn(str(fname.resolve()), coder.abs_fnames)
|
||||
|
||||
def test_cmd_add_abs_filename(self):
|
||||
with ChdirTemporaryDirectory():
|
||||
io = InputOutput(pretty=False, yes=False)
|
||||
from aider.coders import Coder
|
||||
|
||||
coder = Coder.create(self.GPT35, None, io)
|
||||
commands = Commands(io, coder)
|
||||
|
||||
fname = Path("file.txt")
|
||||
fname.touch()
|
||||
|
||||
commands.cmd_add(str(fname.resolve()))
|
||||
|
||||
self.assertIn(str(fname.resolve()), coder.abs_fnames)
|
||||
|
||||
def test_cmd_add_quoted_filename(self):
|
||||
with ChdirTemporaryDirectory():
|
||||
io = InputOutput(pretty=False, yes=False)
|
||||
from aider.coders import Coder
|
||||
|
||||
coder = Coder.create(self.GPT35, None, io)
|
||||
commands = Commands(io, coder)
|
||||
|
||||
fname = Path("file with spaces.txt")
|
||||
fname.touch()
|
||||
|
||||
commands.cmd_add(f'"{fname}"')
|
||||
|
||||
self.assertIn(str(fname.resolve()), coder.abs_fnames)
|
||||
|
||||
def test_cmd_add_existing_with_dirty_repo(self):
|
||||
with GitTemporaryDirectory():
|
||||
repo = git.Repo()
|
||||
|
||||
files = ["one.txt", "two.txt"]
|
||||
for fname in files:
|
||||
Path(fname).touch()
|
||||
repo.git.add(fname)
|
||||
repo.git.commit("-m", "initial")
|
||||
|
||||
commit = repo.head.commit.hexsha
|
||||
|
||||
# leave a dirty `git rm`
|
||||
repo.git.rm("one.txt")
|
||||
|
||||
io = InputOutput(pretty=False, yes=True)
|
||||
from aider.coders import Coder
|
||||
|
||||
coder = Coder.create(self.GPT35, None, io)
|
||||
commands = Commands(io, coder)
|
||||
|
||||
# There's no reason this /add should trigger a commit
|
||||
commands.cmd_add("two.txt")
|
||||
|
||||
self.assertEqual(commit, repo.head.commit.hexsha)
|
||||
|
||||
# Windows is throwing:
|
||||
# PermissionError: [WinError 32] The process cannot access
|
||||
# the file because it is being used by another process
|
||||
|
||||
repo.git.commit("-m", "cleanup")
|
||||
|
||||
del coder
|
||||
del commands
|
||||
del repo
|
||||
|
||||
def test_cmd_add_unicode_error(self):
|
||||
# Initialize the Commands and InputOutput objects
|
||||
io = InputOutput(pretty=False, yes=True)
|
||||
from aider.coders import Coder
|
||||
|
||||
coder = Coder.create(self.GPT35, None, io)
|
||||
commands = Commands(io, coder)
|
||||
|
||||
fname = "file.txt"
|
||||
encoding = "utf-16"
|
||||
some_content_which_will_error_if_read_with_encoding_utf8 = "ÅÍÎÏ".encode(encoding)
|
||||
with open(fname, "wb") as f:
|
||||
f.write(some_content_which_will_error_if_read_with_encoding_utf8)
|
||||
|
||||
commands.cmd_add("file.txt")
|
||||
self.assertEqual(coder.abs_fnames, set())
|
||||
|
||||
def test_cmd_add_drop_untracked_files(self):
|
||||
with GitTemporaryDirectory():
|
||||
repo = git.Repo()
|
||||
|
||||
io = InputOutput(pretty=False, yes=False)
|
||||
from aider.coders import Coder
|
||||
|
||||
coder = Coder.create(self.GPT35, None, io)
|
||||
commands = Commands(io, coder)
|
||||
|
||||
fname = Path("test.txt")
|
||||
fname.touch()
|
||||
|
||||
self.assertEqual(len(coder.abs_fnames), 0)
|
||||
|
||||
commands.cmd_add(str(fname))
|
||||
|
||||
files_in_repo = repo.git.ls_files()
|
||||
self.assertNotIn(str(fname), files_in_repo)
|
||||
|
||||
self.assertEqual(len(coder.abs_fnames), 1)
|
||||
|
||||
commands.cmd_drop(str(fname))
|
||||
|
||||
self.assertEqual(len(coder.abs_fnames), 0)
|
||||
|
||||
def test_cmd_undo_with_dirty_files_not_in_last_commit(self):
|
||||
with GitTemporaryDirectory() as repo_dir:
|
||||
repo = git.Repo(repo_dir)
|
||||
io = InputOutput(pretty=False, yes=True)
|
||||
coder = Coder.create(self.GPT35, None, io)
|
||||
commands = Commands(io, coder)
|
||||
|
||||
other_path = Path(repo_dir) / "other_file.txt"
|
||||
other_path.write_text("other content")
|
||||
repo.git.add(str(other_path))
|
||||
|
||||
# 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")
|
||||
|
||||
file_path.write_text("second content")
|
||||
repo.git.add(filename)
|
||||
repo.git.commit("-m", "aider: second commit")
|
||||
|
||||
# Store the commit hash
|
||||
last_commit_hash = repo.head.commit.hexsha[:7]
|
||||
coder.last_aider_commit_hash = last_commit_hash
|
||||
|
||||
file_path.write_text("dirty content")
|
||||
|
||||
# Attempt to undo the last commit
|
||||
commands.cmd_undo("")
|
||||
|
||||
# Check that the last commit is still present
|
||||
self.assertEqual(last_commit_hash, repo.head.commit.hexsha[:7])
|
||||
|
||||
# Put back the initial content (so it's not dirty now)
|
||||
file_path.write_text("second content")
|
||||
other_path.write_text("dirty content")
|
||||
|
||||
commands.cmd_undo("")
|
||||
self.assertNotEqual(last_commit_hash, repo.head.commit.hexsha[:7])
|
||||
|
||||
self.assertEqual(file_path.read_text(), "first content")
|
||||
self.assertEqual(other_path.read_text(), "dirty content")
|
||||
|
||||
del coder
|
||||
del commands
|
||||
del repo
|
||||
|
||||
def test_cmd_add_aiderignored_file(self):
|
||||
with GitTemporaryDirectory():
|
||||
repo = git.Repo()
|
||||
|
||||
fname1 = "ignoreme1.txt"
|
||||
fname2 = "ignoreme2.txt"
|
||||
fname3 = "dir/ignoreme3.txt"
|
||||
|
||||
Path(fname2).touch()
|
||||
repo.git.add(str(fname2))
|
||||
repo.git.commit("-m", "initial")
|
||||
|
||||
aignore = Path(".aiderignore")
|
||||
aignore.write_text(f"{fname1}\n{fname2}\ndir\n")
|
||||
|
||||
io = InputOutput(yes=True)
|
||||
coder = Coder.create(
|
||||
self.GPT35, None, io, fnames=[fname1, fname2], aider_ignore_file=str(aignore)
|
||||
)
|
||||
commands = Commands(io, coder)
|
||||
|
||||
commands.cmd_add(f"{fname1} {fname2} {fname3}")
|
||||
|
||||
self.assertNotIn(fname1, str(coder.abs_fnames))
|
||||
self.assertNotIn(fname2, str(coder.abs_fnames))
|
||||
self.assertNotIn(fname3, str(coder.abs_fnames))
|
403
aider/tests/test_editblock.py
Normal file
403
aider/tests/test_editblock.py
Normal file
|
@ -0,0 +1,403 @@
|
|||
# flake8: noqa: E501
|
||||
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from aider.coders import Coder
|
||||
from aider.coders import editblock_coder as eb
|
||||
from aider.dump import dump # noqa: F401
|
||||
from aider.io import InputOutput
|
||||
from aider.models import Model
|
||||
|
||||
|
||||
class TestUtils(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.GPT35 = Model("gpt-3.5-turbo")
|
||||
|
||||
# fuzzy logic disabled v0.11.2-dev
|
||||
def __test_replace_most_similar_chunk(self):
|
||||
whole = "This is a sample text.\nAnother line of text.\nYet another line.\n"
|
||||
part = "This is a sample text\n"
|
||||
replace = "This is a replaced text.\n"
|
||||
expected_output = "This is a replaced text.\nAnother line of text.\nYet another line.\n"
|
||||
|
||||
result = eb.replace_most_similar_chunk(whole, part, replace)
|
||||
self.assertEqual(result, expected_output)
|
||||
|
||||
# fuzzy logic disabled v0.11.2-dev
|
||||
def __test_replace_most_similar_chunk_not_perfect_match(self):
|
||||
whole = "This is a sample text.\nAnother line of text.\nYet another line.\n"
|
||||
part = "This was a sample text.\nAnother line of txt\n"
|
||||
replace = "This is a replaced text.\nModified line of text.\n"
|
||||
expected_output = "This is a replaced text.\nModified line of text.\nYet another line.\n"
|
||||
|
||||
result = eb.replace_most_similar_chunk(whole, part, replace)
|
||||
self.assertEqual(result, expected_output)
|
||||
|
||||
def test_strip_quoted_wrapping(self):
|
||||
input_text = (
|
||||
"filename.ext\n```\nWe just want this content\nNot the filename and triple quotes\n```"
|
||||
)
|
||||
expected_output = "We just want this content\nNot the filename and triple quotes\n"
|
||||
result = eb.strip_quoted_wrapping(input_text, "filename.ext")
|
||||
self.assertEqual(result, expected_output)
|
||||
|
||||
def test_strip_quoted_wrapping_no_filename(self):
|
||||
input_text = "```\nWe just want this content\nNot the triple quotes\n```"
|
||||
expected_output = "We just want this content\nNot the triple quotes\n"
|
||||
result = eb.strip_quoted_wrapping(input_text)
|
||||
self.assertEqual(result, expected_output)
|
||||
|
||||
def test_strip_quoted_wrapping_no_wrapping(self):
|
||||
input_text = "We just want this content\nNot the triple quotes\n"
|
||||
expected_output = "We just want this content\nNot the triple quotes\n"
|
||||
result = eb.strip_quoted_wrapping(input_text)
|
||||
self.assertEqual(result, expected_output)
|
||||
|
||||
def test_find_original_update_blocks(self):
|
||||
edit = """
|
||||
Here's the change:
|
||||
|
||||
```text
|
||||
foo.txt
|
||||
<<<<<<< SEARCH
|
||||
Two
|
||||
=======
|
||||
Tooooo
|
||||
>>>>>>> REPLACE
|
||||
```
|
||||
|
||||
Hope you like it!
|
||||
"""
|
||||
|
||||
edits = list(eb.find_original_update_blocks(edit))
|
||||
self.assertEqual(edits, [("foo.txt", "Two\n", "Tooooo\n")])
|
||||
|
||||
def test_find_original_update_blocks_mangled_filename_w_source_tag(self):
|
||||
source = "source"
|
||||
|
||||
edit = """
|
||||
Here's the change:
|
||||
|
||||
<%s>foo.txt
|
||||
<<<<<<< SEARCH
|
||||
One
|
||||
=======
|
||||
Two
|
||||
>>>>>>> REPLACE
|
||||
</%s>
|
||||
|
||||
Hope you like it!
|
||||
""" % (source, source)
|
||||
|
||||
fence = ("<%s>" % source, "</%s>" % source)
|
||||
|
||||
with self.assertRaises(ValueError) as cm:
|
||||
_edits = list(eb.find_original_update_blocks(edit, fence))
|
||||
self.assertIn("missing filename", str(cm.exception))
|
||||
|
||||
def test_find_original_update_blocks_quote_below_filename(self):
|
||||
edit = """
|
||||
Here's the change:
|
||||
|
||||
foo.txt
|
||||
```text
|
||||
<<<<<<< SEARCH
|
||||
Two
|
||||
=======
|
||||
Tooooo
|
||||
>>>>>>> REPLACE
|
||||
```
|
||||
|
||||
Hope you like it!
|
||||
"""
|
||||
|
||||
edits = list(eb.find_original_update_blocks(edit))
|
||||
self.assertEqual(edits, [("foo.txt", "Two\n", "Tooooo\n")])
|
||||
|
||||
def test_find_original_update_blocks_unclosed(self):
|
||||
edit = """
|
||||
Here's the change:
|
||||
|
||||
```text
|
||||
foo.txt
|
||||
<<<<<<< SEARCH
|
||||
Two
|
||||
=======
|
||||
Tooooo
|
||||
|
||||
|
||||
oops!
|
||||
"""
|
||||
|
||||
with self.assertRaises(ValueError) as cm:
|
||||
list(eb.find_original_update_blocks(edit))
|
||||
self.assertIn("Incomplete", str(cm.exception))
|
||||
|
||||
def test_find_original_update_blocks_missing_filename(self):
|
||||
edit = """
|
||||
Here's the change:
|
||||
|
||||
```text
|
||||
<<<<<<< SEARCH
|
||||
Two
|
||||
=======
|
||||
Tooooo
|
||||
|
||||
|
||||
oops!
|
||||
"""
|
||||
|
||||
with self.assertRaises(ValueError) as cm:
|
||||
list(eb.find_original_update_blocks(edit))
|
||||
self.assertIn("filename", str(cm.exception))
|
||||
|
||||
def test_find_original_update_blocks_no_final_newline(self):
|
||||
edit = """
|
||||
aider/coder.py
|
||||
<<<<<<< SEARCH
|
||||
self.console.print("[red]^C again to quit")
|
||||
=======
|
||||
self.io.tool_error("^C again to quit")
|
||||
>>>>>>> REPLACE
|
||||
|
||||
aider/coder.py
|
||||
<<<<<<< SEARCH
|
||||
self.io.tool_error("Malformed ORIGINAL/UPDATE blocks, retrying...")
|
||||
self.io.tool_error(err)
|
||||
=======
|
||||
self.io.tool_error("Malformed ORIGINAL/UPDATE blocks, retrying...")
|
||||
self.io.tool_error(str(err))
|
||||
>>>>>>> REPLACE
|
||||
|
||||
aider/coder.py
|
||||
<<<<<<< SEARCH
|
||||
self.console.print("[red]Unable to get commit message from gpt-3.5-turbo. Use /commit to try again.\n")
|
||||
=======
|
||||
self.io.tool_error("Unable to get commit message from gpt-3.5-turbo. Use /commit to try again.")
|
||||
>>>>>>> REPLACE
|
||||
|
||||
aider/coder.py
|
||||
<<<<<<< SEARCH
|
||||
self.console.print("[red]Skipped commmit.")
|
||||
=======
|
||||
self.io.tool_error("Skipped commmit.")
|
||||
>>>>>>> REPLACE"""
|
||||
|
||||
# Should not raise a ValueError
|
||||
list(eb.find_original_update_blocks(edit))
|
||||
|
||||
def test_incomplete_edit_block_missing_filename(self):
|
||||
edit = """
|
||||
No problem! Here are the changes to patch `subprocess.check_output` instead of `subprocess.run` in both tests:
|
||||
|
||||
```python
|
||||
tests/test_repomap.py
|
||||
<<<<<<< SEARCH
|
||||
def test_check_for_ctags_failure(self):
|
||||
with patch("subprocess.run") as mock_run:
|
||||
mock_run.side_effect = Exception("ctags not found")
|
||||
=======
|
||||
def test_check_for_ctags_failure(self):
|
||||
with patch("subprocess.check_output") as mock_check_output:
|
||||
mock_check_output.side_effect = Exception("ctags not found")
|
||||
>>>>>>> REPLACE
|
||||
|
||||
<<<<<<< SEARCH
|
||||
def test_check_for_ctags_success(self):
|
||||
with patch("subprocess.run") as mock_run:
|
||||
mock_run.return_value = CompletedProcess(args=["ctags", "--version"], returncode=0, stdout='''{
|
||||
"_type": "tag",
|
||||
"name": "status",
|
||||
"path": "aider/main.py",
|
||||
"pattern": "/^ status = main()$/",
|
||||
"kind": "variable"
|
||||
}''')
|
||||
=======
|
||||
def test_check_for_ctags_success(self):
|
||||
with patch("subprocess.check_output") as mock_check_output:
|
||||
mock_check_output.return_value = '''{
|
||||
"_type": "tag",
|
||||
"name": "status",
|
||||
"path": "aider/main.py",
|
||||
"pattern": "/^ status = main()$/",
|
||||
"kind": "variable"
|
||||
}'''
|
||||
>>>>>>> REPLACE
|
||||
```
|
||||
|
||||
These changes replace the `subprocess.run` patches with `subprocess.check_output` patches in both `test_check_for_ctags_failure` and `test_check_for_ctags_success` tests.
|
||||
"""
|
||||
edit_blocks = list(eb.find_original_update_blocks(edit))
|
||||
self.assertEqual(len(edit_blocks), 2) # 2 edits
|
||||
self.assertEqual(edit_blocks[0][0], "tests/test_repomap.py")
|
||||
self.assertEqual(edit_blocks[1][0], "tests/test_repomap.py")
|
||||
|
||||
def test_replace_part_with_missing_varied_leading_whitespace(self):
|
||||
whole = """
|
||||
line1
|
||||
line2
|
||||
line3
|
||||
line4
|
||||
"""
|
||||
|
||||
part = "line2\n line3\n"
|
||||
replace = "new_line2\n new_line3\n"
|
||||
expected_output = """
|
||||
line1
|
||||
new_line2
|
||||
new_line3
|
||||
line4
|
||||
"""
|
||||
|
||||
result = eb.replace_most_similar_chunk(whole, part, replace)
|
||||
self.assertEqual(result, expected_output)
|
||||
|
||||
def test_replace_part_with_missing_leading_whitespace(self):
|
||||
whole = " line1\n line2\n line3\n"
|
||||
part = "line1\nline2\n"
|
||||
replace = "new_line1\nnew_line2\n"
|
||||
expected_output = " new_line1\n new_line2\n line3\n"
|
||||
|
||||
result = eb.replace_most_similar_chunk(whole, part, replace)
|
||||
self.assertEqual(result, expected_output)
|
||||
|
||||
def test_replace_part_with_just_some_missing_leading_whitespace(self):
|
||||
whole = " line1\n line2\n line3\n"
|
||||
part = " line1\n line2\n"
|
||||
replace = " new_line1\n new_line2\n"
|
||||
expected_output = " new_line1\n new_line2\n line3\n"
|
||||
|
||||
result = eb.replace_most_similar_chunk(whole, part, replace)
|
||||
self.assertEqual(result, expected_output)
|
||||
|
||||
def test_replace_part_with_missing_leading_whitespace_including_blank_line(self):
|
||||
"""
|
||||
The part has leading whitespace on all lines, so should be ignored.
|
||||
But it has a *blank* line with no whitespace at all, which was causing a
|
||||
bug per issue #25. Test case to repro and confirm fix.
|
||||
"""
|
||||
whole = " line1\n line2\n line3\n"
|
||||
part = "\n line1\n line2\n"
|
||||
replace = " new_line1\n new_line2\n"
|
||||
expected_output = " new_line1\n new_line2\n line3\n"
|
||||
|
||||
result = eb.replace_most_similar_chunk(whole, part, replace)
|
||||
self.assertEqual(result, expected_output)
|
||||
|
||||
def test_full_edit(self):
|
||||
# Create a few temporary files
|
||||
_, file1 = tempfile.mkstemp()
|
||||
|
||||
with open(file1, "w", encoding="utf-8") as f:
|
||||
f.write("one\ntwo\nthree\n")
|
||||
|
||||
files = [file1]
|
||||
|
||||
# Initialize the Coder object with the mocked IO and mocked repo
|
||||
coder = Coder.create(self.GPT35, "diff", io=InputOutput(), fnames=files)
|
||||
|
||||
def mock_send(*args, **kwargs):
|
||||
coder.partial_response_content = f"""
|
||||
Do this:
|
||||
|
||||
{Path(file1).name}
|
||||
<<<<<<< SEARCH
|
||||
two
|
||||
=======
|
||||
new
|
||||
>>>>>>> REPLACE
|
||||
|
||||
"""
|
||||
coder.partial_response_function_call = dict()
|
||||
return []
|
||||
|
||||
coder.send = mock_send
|
||||
|
||||
# Call the run method with a message
|
||||
coder.run(with_message="hi")
|
||||
|
||||
content = Path(file1).read_text(encoding="utf-8")
|
||||
self.assertEqual(content, "one\nnew\nthree\n")
|
||||
|
||||
def test_full_edit_dry_run(self):
|
||||
# Create a few temporary files
|
||||
_, file1 = tempfile.mkstemp()
|
||||
|
||||
orig_content = "one\ntwo\nthree\n"
|
||||
|
||||
with open(file1, "w", encoding="utf-8") as f:
|
||||
f.write(orig_content)
|
||||
|
||||
files = [file1]
|
||||
|
||||
# Initialize the Coder object with the mocked IO and mocked repo
|
||||
coder = Coder.create(
|
||||
self.GPT35,
|
||||
"diff",
|
||||
io=InputOutput(dry_run=True),
|
||||
fnames=files,
|
||||
dry_run=True,
|
||||
)
|
||||
|
||||
def mock_send(*args, **kwargs):
|
||||
coder.partial_response_content = f"""
|
||||
Do this:
|
||||
|
||||
{Path(file1).name}
|
||||
<<<<<<< SEARCH
|
||||
two
|
||||
=======
|
||||
new
|
||||
>>>>>>> REPLACE
|
||||
|
||||
"""
|
||||
coder.partial_response_function_call = dict()
|
||||
return []
|
||||
|
||||
coder.send = mock_send
|
||||
|
||||
# Call the run method with a message
|
||||
coder.run(with_message="hi")
|
||||
|
||||
content = Path(file1).read_text(encoding="utf-8")
|
||||
self.assertEqual(content, orig_content)
|
||||
|
||||
def test_find_original_update_blocks_mupltiple_same_file(self):
|
||||
edit = """
|
||||
Here's the change:
|
||||
|
||||
```text
|
||||
foo.txt
|
||||
<<<<<<< SEARCH
|
||||
one
|
||||
=======
|
||||
two
|
||||
>>>>>>> REPLACE
|
||||
|
||||
...
|
||||
|
||||
<<<<<<< SEARCH
|
||||
three
|
||||
=======
|
||||
four
|
||||
>>>>>>> REPLACE
|
||||
```
|
||||
|
||||
Hope you like it!
|
||||
"""
|
||||
|
||||
edits = list(eb.find_original_update_blocks(edit))
|
||||
self.assertEqual(
|
||||
edits,
|
||||
[
|
||||
("foo.txt", "one\n", "two\n"),
|
||||
("foo.txt", "three\n", "four\n"),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
65
aider/tests/test_io.py
Normal file
65
aider/tests/test_io.py
Normal file
|
@ -0,0 +1,65 @@
|
|||
import os
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from aider.io import AutoCompleter, InputOutput
|
||||
from aider.utils import ChdirTemporaryDirectory
|
||||
|
||||
|
||||
class TestInputOutput(unittest.TestCase):
|
||||
def test_no_color_environment_variable(self):
|
||||
with patch.dict(os.environ, {"NO_COLOR": "1"}):
|
||||
io = InputOutput()
|
||||
self.assertFalse(io.pretty)
|
||||
|
||||
def test_autocompleter_with_non_existent_file(self):
|
||||
root = ""
|
||||
rel_fnames = ["non_existent_file.txt"]
|
||||
addable_rel_fnames = []
|
||||
commands = None
|
||||
autocompleter = AutoCompleter(root, rel_fnames, addable_rel_fnames, commands, "utf-8")
|
||||
self.assertEqual(autocompleter.words, set(rel_fnames))
|
||||
|
||||
def test_autocompleter_with_unicode_file(self):
|
||||
with ChdirTemporaryDirectory():
|
||||
root = ""
|
||||
fname = "file.py"
|
||||
rel_fnames = [fname]
|
||||
addable_rel_fnames = []
|
||||
commands = None
|
||||
autocompleter = AutoCompleter(root, rel_fnames, addable_rel_fnames, commands, "utf-8")
|
||||
self.assertEqual(autocompleter.words, set(rel_fnames))
|
||||
|
||||
Path(fname).write_text("def hello(): pass\n")
|
||||
autocompleter = AutoCompleter(root, rel_fnames, addable_rel_fnames, commands, "utf-8")
|
||||
self.assertEqual(autocompleter.words, set(rel_fnames + ["hello"]))
|
||||
|
||||
encoding = "utf-16"
|
||||
some_content_which_will_error_if_read_with_encoding_utf8 = "ÅÍÎÏ".encode(encoding)
|
||||
with open(fname, "wb") as f:
|
||||
f.write(some_content_which_will_error_if_read_with_encoding_utf8)
|
||||
|
||||
autocompleter = AutoCompleter(root, rel_fnames, addable_rel_fnames, commands, "utf-8")
|
||||
self.assertEqual(autocompleter.words, set(rel_fnames))
|
||||
|
||||
@patch("aider.io.PromptSession")
|
||||
def test_get_input_is_a_directory_error(self, MockPromptSession):
|
||||
# Mock the PromptSession to simulate user input
|
||||
mock_session = MockPromptSession.return_value
|
||||
mock_session.prompt.return_value = "test input"
|
||||
|
||||
io = InputOutput(pretty=False) # Windows tests throw UnicodeDecodeError
|
||||
root = "/"
|
||||
rel_fnames = ["existing_file.txt"]
|
||||
addable_rel_fnames = ["new_file.txt"]
|
||||
commands = MagicMock()
|
||||
|
||||
# Simulate IsADirectoryError
|
||||
with patch("aider.io.open", side_effect=IsADirectoryError):
|
||||
result = io.get_input(root, rel_fnames, addable_rel_fnames, commands)
|
||||
self.assertEqual(result, "test input")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
239
aider/tests/test_main.py
Normal file
239
aider/tests/test_main.py
Normal file
|
@ -0,0 +1,239 @@
|
|||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from unittest import TestCase
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import git
|
||||
from prompt_toolkit.input import DummyInput
|
||||
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
|
||||
|
||||
|
||||
class TestMain(TestCase):
|
||||
def setUp(self):
|
||||
os.environ["OPENAI_API_KEY"] = "deadbeef"
|
||||
self.original_cwd = os.getcwd()
|
||||
self.tempdir = tempfile.mkdtemp()
|
||||
os.chdir(self.tempdir)
|
||||
|
||||
def tearDown(self):
|
||||
os.chdir(self.original_cwd)
|
||||
shutil.rmtree(self.tempdir, ignore_errors=True)
|
||||
|
||||
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):
|
||||
main(["foo.txt", "--yes", "--no-git"], input=DummyInput(), output=DummyOutput())
|
||||
self.assertTrue(os.path.exists("foo.txt"))
|
||||
|
||||
@patch("aider.repo.GitRepo.get_commit_message", return_value="mock commit message")
|
||||
def test_main_with_empty_git_dir_new_file(self, _):
|
||||
make_repo()
|
||||
main(["--yes", "foo.txt"], input=DummyInput(), output=DummyOutput())
|
||||
self.assertTrue(os.path.exists("foo.txt"))
|
||||
|
||||
@patch("aider.repo.GitRepo.get_commit_message", return_value="mock commit message")
|
||||
def test_main_with_empty_git_dir_new_files(self, _):
|
||||
make_repo()
|
||||
main(["--yes", "foo.txt", "bar.txt"], input=DummyInput(), output=DummyOutput())
|
||||
self.assertTrue(os.path.exists("foo.txt"))
|
||||
self.assertTrue(os.path.exists("bar.txt"))
|
||||
|
||||
def test_main_with_dname_and_fname(self):
|
||||
subdir = Path("subdir")
|
||||
subdir.mkdir()
|
||||
make_repo(str(subdir))
|
||||
res = main(["subdir", "foo.txt"], input=DummyInput(), output=DummyOutput())
|
||||
self.assertNotEqual(res, None)
|
||||
|
||||
@patch("aider.repo.GitRepo.get_commit_message", return_value="mock commit message")
|
||||
def test_main_with_subdir_repo_fnames(self, _):
|
||||
subdir = Path("subdir")
|
||||
subdir.mkdir()
|
||||
make_repo(str(subdir))
|
||||
main(
|
||||
["--yes", str(subdir / "foo.txt"), str(subdir / "bar.txt")],
|
||||
input=DummyInput(),
|
||||
output=DummyOutput(),
|
||||
)
|
||||
self.assertTrue((subdir / "foo.txt").exists())
|
||||
self.assertTrue((subdir / "bar.txt").exists())
|
||||
|
||||
def test_main_with_git_config_yml(self):
|
||||
make_repo()
|
||||
|
||||
Path(".aider.conf.yml").write_text("auto-commits: false\n")
|
||||
with patch("aider.main.Coder.create") as MockCoder:
|
||||
main(["--yes"], input=DummyInput(), output=DummyOutput())
|
||||
_, kwargs = MockCoder.call_args
|
||||
assert kwargs["auto_commits"] is False
|
||||
|
||||
Path(".aider.conf.yml").write_text("auto-commits: true\n")
|
||||
with patch("aider.main.Coder.create") as MockCoder:
|
||||
main([], input=DummyInput(), output=DummyOutput())
|
||||
_, kwargs = MockCoder.call_args
|
||||
assert kwargs["auto_commits"] is True
|
||||
|
||||
def test_main_with_empty_git_dir_new_subdir_file(self):
|
||||
make_repo()
|
||||
subdir = Path("subdir")
|
||||
subdir.mkdir()
|
||||
fname = subdir / "foo.txt"
|
||||
fname.touch()
|
||||
subprocess.run(["git", "add", str(subdir)])
|
||||
subprocess.run(["git", "commit", "-m", "added"])
|
||||
|
||||
# This will throw a git error on windows if get_tracked_files doesn't
|
||||
# properly convert git/posix/paths to git\posix\paths.
|
||||
# Because aider will try and `git add` a file that's already in the repo.
|
||||
main(["--yes", str(fname)], input=DummyInput(), output=DummyOutput())
|
||||
|
||||
def test_setup_git(self):
|
||||
io = InputOutput(pretty=False, yes=True)
|
||||
git_root = setup_git(None, io)
|
||||
git_root = Path(git_root).resolve()
|
||||
self.assertEqual(git_root, Path(self.tempdir).resolve())
|
||||
|
||||
self.assertTrue(git.Repo(self.tempdir))
|
||||
|
||||
gitignore = Path.cwd() / ".gitignore"
|
||||
self.assertTrue(gitignore.exists())
|
||||
self.assertEqual(".aider*", gitignore.read_text().splitlines()[0])
|
||||
|
||||
def test_check_gitignore(self):
|
||||
with GitTemporaryDirectory():
|
||||
os.environ["GIT_CONFIG_GLOBAL"] = "globalgitconfig"
|
||||
|
||||
io = InputOutput(pretty=False, yes=True)
|
||||
cwd = Path.cwd()
|
||||
gitignore = cwd / ".gitignore"
|
||||
|
||||
self.assertFalse(gitignore.exists())
|
||||
check_gitignore(cwd, io)
|
||||
self.assertTrue(gitignore.exists())
|
||||
|
||||
self.assertEqual(".aider*", gitignore.read_text().splitlines()[0])
|
||||
|
||||
gitignore.write_text("one\ntwo\n")
|
||||
check_gitignore(cwd, io)
|
||||
self.assertEqual("one\ntwo\n.aider*\n", gitignore.read_text())
|
||||
del os.environ["GIT_CONFIG_GLOBAL"]
|
||||
|
||||
def test_main_git_ignore(self):
|
||||
cwd = Path().cwd()
|
||||
self.assertFalse((cwd / ".git").exists())
|
||||
self.assertFalse((cwd / ".gitignore").exists())
|
||||
|
||||
with patch("aider.main.Coder.create"):
|
||||
main(["--yes"], input=DummyInput())
|
||||
|
||||
self.assertTrue((cwd / ".git").exists())
|
||||
self.assertTrue((cwd / ".gitignore").exists())
|
||||
|
||||
def test_main_args(self):
|
||||
with patch("aider.main.Coder.create") as MockCoder:
|
||||
# --yes will just ok the git repo without blocking on input
|
||||
# following calls to main will see the new repo already
|
||||
main(["--no-auto-commits", "--yes"], input=DummyInput())
|
||||
_, kwargs = MockCoder.call_args
|
||||
assert kwargs["auto_commits"] is False
|
||||
|
||||
with patch("aider.main.Coder.create") as MockCoder:
|
||||
main(["--auto-commits"], input=DummyInput())
|
||||
_, kwargs = MockCoder.call_args
|
||||
assert kwargs["auto_commits"] is True
|
||||
|
||||
with patch("aider.main.Coder.create") as MockCoder:
|
||||
main([], input=DummyInput())
|
||||
_, kwargs = MockCoder.call_args
|
||||
assert kwargs["dirty_commits"] is True
|
||||
assert kwargs["auto_commits"] is True
|
||||
assert kwargs["pretty"] is True
|
||||
|
||||
with patch("aider.main.Coder.create") as MockCoder:
|
||||
main(["--no-pretty"], input=DummyInput())
|
||||
_, kwargs = MockCoder.call_args
|
||||
assert kwargs["pretty"] is False
|
||||
|
||||
with patch("aider.main.Coder.create") as MockCoder:
|
||||
main(["--pretty"], input=DummyInput())
|
||||
_, kwargs = MockCoder.call_args
|
||||
assert kwargs["pretty"] is True
|
||||
|
||||
with patch("aider.main.Coder.create") as MockCoder:
|
||||
main(["--no-dirty-commits"], input=DummyInput())
|
||||
_, kwargs = MockCoder.call_args
|
||||
assert kwargs["dirty_commits"] is False
|
||||
|
||||
with patch("aider.main.Coder.create") as MockCoder:
|
||||
main(["--dirty-commits"], input=DummyInput())
|
||||
_, kwargs = MockCoder.call_args
|
||||
assert kwargs["dirty_commits"] is True
|
||||
|
||||
def test_message_file_flag(self):
|
||||
message_file_content = "This is a test message from a file."
|
||||
message_file_path = tempfile.mktemp()
|
||||
with open(message_file_path, "w", encoding="utf-8") as message_file:
|
||||
message_file.write(message_file_content)
|
||||
|
||||
with patch("aider.main.Coder.create") as MockCoder:
|
||||
MockCoder.return_value.run = MagicMock()
|
||||
main(
|
||||
["--yes", "--message-file", message_file_path],
|
||||
input=DummyInput(),
|
||||
output=DummyOutput(),
|
||||
)
|
||||
MockCoder.return_value.run.assert_called_once_with(with_message=message_file_content)
|
||||
|
||||
os.remove(message_file_path)
|
||||
|
||||
def test_encodings_arg(self):
|
||||
fname = "foo.py"
|
||||
|
||||
with GitTemporaryDirectory():
|
||||
with patch("aider.main.Coder.create") as MockCoder: # noqa: F841
|
||||
with patch("aider.main.InputOutput") as MockSend:
|
||||
|
||||
def side_effect(*args, **kwargs):
|
||||
self.assertEqual(kwargs["encoding"], "iso-8859-15")
|
||||
return MagicMock()
|
||||
|
||||
MockSend.side_effect = side_effect
|
||||
|
||||
main(["--yes", fname, "--encoding", "iso-8859-15"])
|
||||
|
||||
@patch("aider.main.InputOutput")
|
||||
@patch("aider.coders.base_coder.Coder.run")
|
||||
def test_main_message_adds_to_input_history(self, mock_run, MockInputOutput):
|
||||
test_message = "test message"
|
||||
mock_io_instance = MockInputOutput.return_value
|
||||
|
||||
main(["--message", test_message], input=DummyInput(), output=DummyOutput())
|
||||
|
||||
mock_io_instance.add_to_input_history.assert_called_once_with(test_message)
|
||||
|
||||
@patch("aider.main.InputOutput")
|
||||
@patch("aider.coders.base_coder.Coder.run")
|
||||
def test_yes(self, mock_run, MockInputOutput):
|
||||
test_message = "test message"
|
||||
|
||||
main(["--yes", "--message", test_message])
|
||||
args, kwargs = MockInputOutput.call_args
|
||||
self.assertTrue(args[1])
|
||||
|
||||
@patch("aider.main.InputOutput")
|
||||
@patch("aider.coders.base_coder.Coder.run")
|
||||
def test_default_yes(self, mock_run, MockInputOutput):
|
||||
test_message = "test message"
|
||||
|
||||
main(["--message", test_message])
|
||||
args, kwargs = MockInputOutput.call_args
|
||||
self.assertEqual(args[1], None)
|
28
aider/tests/test_models.py
Normal file
28
aider/tests/test_models.py
Normal file
|
@ -0,0 +1,28 @@
|
|||
import unittest
|
||||
|
||||
from aider.models import Model
|
||||
|
||||
|
||||
class TestModels(unittest.TestCase):
|
||||
def test_max_context_tokens(self):
|
||||
model = Model("gpt-3.5-turbo")
|
||||
self.assertEqual(model.info["max_input_tokens"], 16385)
|
||||
|
||||
model = Model("gpt-3.5-turbo-16k")
|
||||
self.assertEqual(model.info["max_input_tokens"], 16385)
|
||||
|
||||
model = Model("gpt-3.5-turbo-1106")
|
||||
self.assertEqual(model.info["max_input_tokens"], 16385)
|
||||
|
||||
model = Model("gpt-4")
|
||||
self.assertEqual(model.info["max_input_tokens"], 8 * 1024)
|
||||
|
||||
model = Model("gpt-4-32k")
|
||||
self.assertEqual(model.info["max_input_tokens"], 32 * 1024)
|
||||
|
||||
model = Model("gpt-4-0613")
|
||||
self.assertEqual(model.info["max_input_tokens"], 8 * 1024)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
296
aider/tests/test_repo.py
Normal file
296
aider/tests/test_repo.py
Normal file
|
@ -0,0 +1,296 @@
|
|||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
import git
|
||||
|
||||
from aider.dump import dump # noqa: F401
|
||||
from aider.io import InputOutput
|
||||
from aider.models import Model
|
||||
from aider.repo import GitRepo
|
||||
from aider.utils import GitTemporaryDirectory
|
||||
|
||||
|
||||
class TestRepo(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.GPT35 = Model("gpt-3.5-turbo")
|
||||
|
||||
def test_diffs_empty_repo(self):
|
||||
with GitTemporaryDirectory():
|
||||
repo = git.Repo()
|
||||
|
||||
# Add a change to the index
|
||||
fname = Path("foo.txt")
|
||||
fname.write_text("index\n")
|
||||
repo.git.add(str(fname))
|
||||
|
||||
# Make a change in the working dir
|
||||
fname.write_text("workingdir\n")
|
||||
|
||||
git_repo = GitRepo(InputOutput(), None, ".")
|
||||
diffs = git_repo.get_diffs()
|
||||
self.assertIn("index", diffs)
|
||||
self.assertIn("workingdir", diffs)
|
||||
|
||||
def test_diffs_nonempty_repo(self):
|
||||
with GitTemporaryDirectory():
|
||||
repo = git.Repo()
|
||||
fname = Path("foo.txt")
|
||||
fname.touch()
|
||||
repo.git.add(str(fname))
|
||||
|
||||
fname2 = Path("bar.txt")
|
||||
fname2.touch()
|
||||
repo.git.add(str(fname2))
|
||||
|
||||
repo.git.commit("-m", "initial")
|
||||
|
||||
fname.write_text("index\n")
|
||||
repo.git.add(str(fname))
|
||||
|
||||
fname2.write_text("workingdir\n")
|
||||
|
||||
git_repo = GitRepo(InputOutput(), None, ".")
|
||||
diffs = git_repo.get_diffs()
|
||||
self.assertIn("index", diffs)
|
||||
self.assertIn("workingdir", diffs)
|
||||
|
||||
def test_diffs_detached_head(self):
|
||||
with GitTemporaryDirectory():
|
||||
repo = git.Repo()
|
||||
fname = Path("foo.txt")
|
||||
fname.touch()
|
||||
repo.git.add(str(fname))
|
||||
repo.git.commit("-m", "foo")
|
||||
|
||||
fname2 = Path("bar.txt")
|
||||
fname2.touch()
|
||||
repo.git.add(str(fname2))
|
||||
repo.git.commit("-m", "bar")
|
||||
|
||||
fname3 = Path("baz.txt")
|
||||
fname3.touch()
|
||||
repo.git.add(str(fname3))
|
||||
repo.git.commit("-m", "baz")
|
||||
|
||||
repo.git.checkout("HEAD^")
|
||||
|
||||
fname.write_text("index\n")
|
||||
repo.git.add(str(fname))
|
||||
|
||||
fname2.write_text("workingdir\n")
|
||||
|
||||
git_repo = GitRepo(InputOutput(), None, ".")
|
||||
diffs = git_repo.get_diffs()
|
||||
self.assertIn("index", diffs)
|
||||
self.assertIn("workingdir", diffs)
|
||||
|
||||
def test_diffs_between_commits(self):
|
||||
with GitTemporaryDirectory():
|
||||
repo = git.Repo()
|
||||
fname = Path("foo.txt")
|
||||
|
||||
fname.write_text("one\n")
|
||||
repo.git.add(str(fname))
|
||||
repo.git.commit("-m", "initial")
|
||||
|
||||
fname.write_text("two\n")
|
||||
repo.git.add(str(fname))
|
||||
repo.git.commit("-m", "second")
|
||||
|
||||
git_repo = GitRepo(InputOutput(), None, ".")
|
||||
diffs = git_repo.diff_commits(False, "HEAD~1", "HEAD")
|
||||
self.assertIn("two", diffs)
|
||||
|
||||
@patch("aider.repo.simple_send_with_retries")
|
||||
def test_get_commit_message(self, mock_send):
|
||||
mock_send.return_value = "a good commit message"
|
||||
|
||||
repo = GitRepo(InputOutput(), None, None, models=[self.GPT35])
|
||||
# Call the get_commit_message method with dummy diff and context
|
||||
result = repo.get_commit_message("dummy diff", "dummy context")
|
||||
|
||||
# Assert that the returned message is the expected one
|
||||
self.assertEqual(result, "a good commit message")
|
||||
|
||||
@patch("aider.repo.simple_send_with_retries")
|
||||
def test_get_commit_message_strip_quotes(self, mock_send):
|
||||
mock_send.return_value = '"a good commit message"'
|
||||
|
||||
repo = GitRepo(InputOutput(), None, None, models=[self.GPT35])
|
||||
# Call the get_commit_message method with dummy diff and context
|
||||
result = repo.get_commit_message("dummy diff", "dummy context")
|
||||
|
||||
# Assert that the returned message is the expected one
|
||||
self.assertEqual(result, "a good commit message")
|
||||
|
||||
@patch("aider.repo.simple_send_with_retries")
|
||||
def test_get_commit_message_no_strip_unmatched_quotes(self, mock_send):
|
||||
mock_send.return_value = 'a good "commit message"'
|
||||
|
||||
repo = GitRepo(InputOutput(), None, None, models=[self.GPT35])
|
||||
# Call the get_commit_message method with dummy diff and context
|
||||
result = repo.get_commit_message("dummy diff", "dummy context")
|
||||
|
||||
# Assert that the returned message is the expected one
|
||||
self.assertEqual(result, 'a good "commit message"')
|
||||
|
||||
def test_get_tracked_files(self):
|
||||
# Create a temporary directory
|
||||
tempdir = Path(tempfile.mkdtemp())
|
||||
|
||||
# Initialize a git repository in the temporary directory and set user name and email
|
||||
repo = git.Repo.init(tempdir)
|
||||
repo.config_writer().set_value("user", "name", "Test User").release()
|
||||
repo.config_writer().set_value("user", "email", "testuser@example.com").release()
|
||||
|
||||
# Create three empty files and add them to the git repository
|
||||
filenames = ["README.md", "subdir/fänny.md", "systemüber/blick.md", 'file"with"quotes.txt']
|
||||
created_files = []
|
||||
for filename in filenames:
|
||||
file_path = tempdir / filename
|
||||
try:
|
||||
file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
file_path.touch()
|
||||
repo.git.add(str(file_path))
|
||||
created_files.append(Path(filename))
|
||||
except OSError:
|
||||
# windows won't allow files with quotes, that's ok
|
||||
self.assertIn('"', filename)
|
||||
self.assertEqual(os.name, "nt")
|
||||
|
||||
self.assertTrue(len(created_files) >= 3)
|
||||
|
||||
repo.git.commit("-m", "added")
|
||||
|
||||
tracked_files = GitRepo(InputOutput(), [tempdir], None).get_tracked_files()
|
||||
|
||||
# On windows, paths will come back \like\this, so normalize them back to Paths
|
||||
tracked_files = [Path(fn) for fn in tracked_files]
|
||||
|
||||
# Assert that coder.get_tracked_files() returns the three filenames
|
||||
self.assertEqual(set(tracked_files), set(created_files))
|
||||
|
||||
def test_get_tracked_files_with_new_staged_file(self):
|
||||
with GitTemporaryDirectory():
|
||||
# new repo
|
||||
raw_repo = git.Repo()
|
||||
|
||||
# add it, but no commits at all in the raw_repo yet
|
||||
fname = Path("new.txt")
|
||||
fname.touch()
|
||||
raw_repo.git.add(str(fname))
|
||||
|
||||
git_repo = GitRepo(InputOutput(), None, None)
|
||||
|
||||
# better be there
|
||||
fnames = git_repo.get_tracked_files()
|
||||
self.assertIn(str(fname), fnames)
|
||||
|
||||
# commit it, better still be there
|
||||
raw_repo.git.commit("-m", "new")
|
||||
fnames = git_repo.get_tracked_files()
|
||||
self.assertIn(str(fname), fnames)
|
||||
|
||||
# new file, added but not committed
|
||||
fname2 = Path("new2.txt")
|
||||
fname2.touch()
|
||||
raw_repo.git.add(str(fname2))
|
||||
|
||||
# both should be there
|
||||
fnames = git_repo.get_tracked_files()
|
||||
self.assertIn(str(fname), fnames)
|
||||
self.assertIn(str(fname2), fnames)
|
||||
|
||||
def test_get_tracked_files_with_aiderignore(self):
|
||||
with GitTemporaryDirectory():
|
||||
# new repo
|
||||
raw_repo = git.Repo()
|
||||
|
||||
# add it, but no commits at all in the raw_repo yet
|
||||
fname = Path("new.txt")
|
||||
fname.touch()
|
||||
raw_repo.git.add(str(fname))
|
||||
|
||||
aiderignore = Path(".aiderignore")
|
||||
git_repo = GitRepo(InputOutput(), None, None, str(aiderignore))
|
||||
|
||||
# better be there
|
||||
fnames = git_repo.get_tracked_files()
|
||||
self.assertIn(str(fname), fnames)
|
||||
|
||||
# commit it, better still be there
|
||||
raw_repo.git.commit("-m", "new")
|
||||
fnames = git_repo.get_tracked_files()
|
||||
self.assertIn(str(fname), fnames)
|
||||
|
||||
# new file, added but not committed
|
||||
fname2 = Path("new2.txt")
|
||||
fname2.touch()
|
||||
raw_repo.git.add(str(fname2))
|
||||
|
||||
# both should be there
|
||||
fnames = git_repo.get_tracked_files()
|
||||
self.assertIn(str(fname), fnames)
|
||||
self.assertIn(str(fname2), fnames)
|
||||
|
||||
aiderignore.write_text("new.txt\n")
|
||||
|
||||
# new.txt should be gone!
|
||||
fnames = git_repo.get_tracked_files()
|
||||
self.assertNotIn(str(fname), fnames)
|
||||
self.assertIn(str(fname2), fnames)
|
||||
|
||||
# This does not work in github actions?!
|
||||
# The mtime doesn't change, even if I time.sleep(1)
|
||||
# Before doing this write_text()!?
|
||||
#
|
||||
# aiderignore.write_text("new2.txt\n")
|
||||
# new2.txt should be gone!
|
||||
# fnames = git_repo.get_tracked_files()
|
||||
# self.assertIn(str(fname), fnames)
|
||||
# self.assertNotIn(str(fname2), fnames)
|
||||
|
||||
def test_get_tracked_files_from_subdir(self):
|
||||
with GitTemporaryDirectory():
|
||||
# new repo
|
||||
raw_repo = git.Repo()
|
||||
|
||||
# add it, but no commits at all in the raw_repo yet
|
||||
fname = Path("subdir/new.txt")
|
||||
fname.parent.mkdir()
|
||||
fname.touch()
|
||||
raw_repo.git.add(str(fname))
|
||||
|
||||
os.chdir(fname.parent)
|
||||
|
||||
git_repo = GitRepo(InputOutput(), None, None)
|
||||
|
||||
# better be there
|
||||
fnames = git_repo.get_tracked_files()
|
||||
self.assertIn(str(fname), fnames)
|
||||
|
||||
# commit it, better still be there
|
||||
raw_repo.git.commit("-m", "new")
|
||||
fnames = git_repo.get_tracked_files()
|
||||
self.assertIn(str(fname), fnames)
|
||||
|
||||
@patch("aider.repo.simple_send_with_retries")
|
||||
def test_noop_commit(self, mock_send):
|
||||
mock_send.return_value = '"a good commit message"'
|
||||
|
||||
with GitTemporaryDirectory():
|
||||
# new repo
|
||||
raw_repo = git.Repo()
|
||||
|
||||
# add it, but no commits at all in the raw_repo yet
|
||||
fname = Path("file.txt")
|
||||
fname.touch()
|
||||
raw_repo.git.add(str(fname))
|
||||
raw_repo.git.commit("-m", "new")
|
||||
|
||||
git_repo = GitRepo(InputOutput(), None, None)
|
||||
|
||||
git_repo.commit(fnames=[str(fname)])
|
217
aider/tests/test_repomap.py
Normal file
217
aider/tests/test_repomap.py
Normal file
|
@ -0,0 +1,217 @@
|
|||
import os
|
||||
import unittest
|
||||
|
||||
from aider.dump import dump # noqa: F401
|
||||
from aider.io import InputOutput
|
||||
from aider.models import Model
|
||||
from aider.repomap import RepoMap
|
||||
from aider.utils import IgnorantTemporaryDirectory
|
||||
|
||||
|
||||
class TestRepoMap(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.GPT35 = Model("gpt-3.5-turbo")
|
||||
|
||||
def test_get_repo_map(self):
|
||||
# Create a temporary directory with sample files for testing
|
||||
test_files = [
|
||||
"test_file1.py",
|
||||
"test_file2.py",
|
||||
"test_file3.md",
|
||||
"test_file4.json",
|
||||
]
|
||||
|
||||
with IgnorantTemporaryDirectory() as temp_dir:
|
||||
for file in test_files:
|
||||
with open(os.path.join(temp_dir, file), "w") as f:
|
||||
f.write("")
|
||||
|
||||
io = InputOutput()
|
||||
repo_map = RepoMap(main_model=self.GPT35, root=temp_dir, io=io)
|
||||
other_files = [os.path.join(temp_dir, file) for file in test_files]
|
||||
result = repo_map.get_repo_map([], other_files)
|
||||
|
||||
# Check if the result contains the expected tags map
|
||||
self.assertIn("test_file1.py", result)
|
||||
self.assertIn("test_file2.py", result)
|
||||
self.assertIn("test_file3.md", result)
|
||||
self.assertIn("test_file4.json", result)
|
||||
|
||||
# close the open cache files, so Windows won't error
|
||||
del repo_map
|
||||
|
||||
def test_get_repo_map_with_identifiers(self):
|
||||
# Create a temporary directory with a sample Python file containing identifiers
|
||||
test_file1 = "test_file_with_identifiers.py"
|
||||
file_content1 = """\
|
||||
class MyClass:
|
||||
def my_method(self, arg1, arg2):
|
||||
return arg1 + arg2
|
||||
|
||||
def my_function(arg1, arg2):
|
||||
return arg1 * arg2
|
||||
"""
|
||||
|
||||
test_file2 = "test_file_import.py"
|
||||
file_content2 = """\
|
||||
from test_file_with_identifiers import MyClass
|
||||
|
||||
obj = MyClass()
|
||||
print(obj.my_method(1, 2))
|
||||
print(my_function(3, 4))
|
||||
"""
|
||||
|
||||
test_file3 = "test_file_pass.py"
|
||||
file_content3 = "pass"
|
||||
|
||||
with IgnorantTemporaryDirectory() as temp_dir:
|
||||
with open(os.path.join(temp_dir, test_file1), "w") as f:
|
||||
f.write(file_content1)
|
||||
|
||||
with open(os.path.join(temp_dir, test_file2), "w") as f:
|
||||
f.write(file_content2)
|
||||
|
||||
with open(os.path.join(temp_dir, test_file3), "w") as f:
|
||||
f.write(file_content3)
|
||||
|
||||
io = InputOutput()
|
||||
repo_map = RepoMap(main_model=self.GPT35, root=temp_dir, io=io)
|
||||
other_files = [
|
||||
os.path.join(temp_dir, test_file1),
|
||||
os.path.join(temp_dir, test_file2),
|
||||
os.path.join(temp_dir, test_file3),
|
||||
]
|
||||
result = repo_map.get_repo_map([], other_files)
|
||||
|
||||
# Check if the result contains the expected tags map with identifiers
|
||||
self.assertIn("test_file_with_identifiers.py", result)
|
||||
self.assertIn("MyClass", result)
|
||||
self.assertIn("my_method", result)
|
||||
self.assertIn("my_function", result)
|
||||
self.assertIn("test_file_pass.py", result)
|
||||
|
||||
# close the open cache files, so Windows won't error
|
||||
del repo_map
|
||||
|
||||
def test_get_repo_map_all_files(self):
|
||||
test_files = [
|
||||
"test_file0.py",
|
||||
"test_file1.txt",
|
||||
"test_file2.md",
|
||||
"test_file3.json",
|
||||
"test_file4.html",
|
||||
"test_file5.css",
|
||||
"test_file6.js",
|
||||
]
|
||||
|
||||
with IgnorantTemporaryDirectory() as temp_dir:
|
||||
for file in test_files:
|
||||
with open(os.path.join(temp_dir, file), "w") as f:
|
||||
f.write("")
|
||||
|
||||
repo_map = RepoMap(main_model=self.GPT35, root=temp_dir, io=InputOutput())
|
||||
|
||||
other_files = [os.path.join(temp_dir, file) for file in test_files]
|
||||
result = repo_map.get_repo_map([], other_files)
|
||||
dump(other_files)
|
||||
dump(repr(result))
|
||||
|
||||
# Check if the result contains each specific file in the expected tags map without ctags
|
||||
for file in test_files:
|
||||
self.assertIn(file, result)
|
||||
|
||||
# close the open cache files, so Windows won't error
|
||||
del repo_map
|
||||
|
||||
def test_get_repo_map_excludes_added_files(self):
|
||||
# Create a temporary directory with sample files for testing
|
||||
test_files = [
|
||||
"test_file1.py",
|
||||
"test_file2.py",
|
||||
"test_file3.md",
|
||||
"test_file4.json",
|
||||
]
|
||||
|
||||
with IgnorantTemporaryDirectory() as temp_dir:
|
||||
for file in test_files:
|
||||
with open(os.path.join(temp_dir, file), "w") as f:
|
||||
f.write("def foo(): pass\n")
|
||||
|
||||
io = InputOutput()
|
||||
repo_map = RepoMap(main_model=self.GPT35, root=temp_dir, io=io)
|
||||
test_files = [os.path.join(temp_dir, file) for file in test_files]
|
||||
result = repo_map.get_repo_map(test_files[:2], test_files[2:])
|
||||
|
||||
dump(result)
|
||||
|
||||
# Check if the result contains the expected tags map
|
||||
self.assertNotIn("test_file1.py", result)
|
||||
self.assertNotIn("test_file2.py", result)
|
||||
self.assertIn("test_file3.md", result)
|
||||
self.assertIn("test_file4.json", result)
|
||||
|
||||
# close the open cache files, so Windows won't error
|
||||
del repo_map
|
||||
|
||||
|
||||
class TestRepoMapTypescript(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.GPT35 = Model("gpt-3.5-turbo")
|
||||
|
||||
def test_get_repo_map_typescript(self):
|
||||
# Create a temporary directory with a sample TypeScript file
|
||||
test_file_ts = "test_file.ts"
|
||||
file_content_ts = """\
|
||||
interface IMyInterface {
|
||||
someMethod(): void;
|
||||
}
|
||||
|
||||
type ExampleType = {
|
||||
key: string;
|
||||
value: number;
|
||||
};
|
||||
|
||||
enum Status {
|
||||
New,
|
||||
InProgress,
|
||||
Completed,
|
||||
}
|
||||
|
||||
export class MyClass {
|
||||
constructor(public value: number) {}
|
||||
|
||||
add(input: number): number {
|
||||
return this.value + input;
|
||||
return this.value + input;
|
||||
}
|
||||
}
|
||||
|
||||
export function myFunction(input: number): number {
|
||||
return input * 2;
|
||||
}
|
||||
"""
|
||||
|
||||
with IgnorantTemporaryDirectory() as temp_dir:
|
||||
with open(os.path.join(temp_dir, test_file_ts), "w") as f:
|
||||
f.write(file_content_ts)
|
||||
|
||||
io = InputOutput()
|
||||
repo_map = RepoMap(main_model=self.GPT35, root=temp_dir, io=io)
|
||||
other_files = [os.path.join(temp_dir, test_file_ts)]
|
||||
result = repo_map.get_repo_map([], other_files)
|
||||
|
||||
# Check if the result contains the expected tags map with TypeScript identifiers
|
||||
self.assertIn("test_file.ts", result)
|
||||
self.assertIn("IMyInterface", result)
|
||||
self.assertIn("ExampleType", result)
|
||||
self.assertIn("Status", result)
|
||||
self.assertIn("MyClass", result)
|
||||
self.assertIn("add", result)
|
||||
self.assertIn("myFunction", result)
|
||||
|
||||
# close the open cache files, so Windows won't error
|
||||
del repo_map
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
47
aider/tests/test_sendchat.py
Normal file
47
aider/tests/test_sendchat.py
Normal file
|
@ -0,0 +1,47 @@
|
|||
import unittest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import httpx
|
||||
|
||||
from aider.litellm import litellm
|
||||
from aider.sendchat import send_with_retries
|
||||
|
||||
|
||||
class PrintCalled(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class TestSendChat(unittest.TestCase):
|
||||
@patch("litellm.completion")
|
||||
@patch("builtins.print")
|
||||
def test_send_with_retries_rate_limit_error(self, mock_print, mock_completion):
|
||||
mock = MagicMock()
|
||||
mock.status_code = 500
|
||||
|
||||
# Set up the mock to raise
|
||||
mock_completion.side_effect = [
|
||||
litellm.exceptions.RateLimitError(
|
||||
"rate limit exceeded",
|
||||
response=mock,
|
||||
llm_provider="llm_provider",
|
||||
model="model",
|
||||
),
|
||||
None,
|
||||
]
|
||||
|
||||
# Call the send_with_retries method
|
||||
send_with_retries("model", ["message"], None, False)
|
||||
mock_print.assert_called_once()
|
||||
|
||||
@patch("litellm.completion")
|
||||
@patch("builtins.print")
|
||||
def test_send_with_retries_connection_error(self, mock_print, mock_completion):
|
||||
# Set up the mock to raise
|
||||
mock_completion.side_effect = [
|
||||
httpx.ConnectError("Connection error"),
|
||||
None,
|
||||
]
|
||||
|
||||
# Call the send_with_retries method
|
||||
send_with_retries("model", ["message"], None, False)
|
||||
mock_print.assert_called_once()
|
119
aider/tests/test_udiff.py
Normal file
119
aider/tests/test_udiff.py
Normal file
|
@ -0,0 +1,119 @@
|
|||
import unittest
|
||||
|
||||
from aider.coders.udiff_coder import find_diffs
|
||||
from aider.dump import dump # noqa: F401
|
||||
|
||||
|
||||
class TestUnifiedDiffCoder(unittest.TestCase):
|
||||
def test_find_diffs_single_hunk(self):
|
||||
# Test find_diffs with a single hunk
|
||||
content = """
|
||||
Some text...
|
||||
|
||||
```diff
|
||||
--- file.txt
|
||||
+++ file.txt
|
||||
@@ ... @@
|
||||
-Original
|
||||
+Modified
|
||||
```
|
||||
"""
|
||||
edits = find_diffs(content)
|
||||
dump(edits)
|
||||
self.assertEqual(len(edits), 1)
|
||||
|
||||
edit = edits[0]
|
||||
self.assertEqual(edit[0], "file.txt")
|
||||
self.assertEqual(edit[1], ["-Original\n", "+Modified\n"])
|
||||
|
||||
def test_find_diffs_dev_null(self):
|
||||
# Test find_diffs with a single hunk
|
||||
content = """
|
||||
Some text...
|
||||
|
||||
```diff
|
||||
--- /dev/null
|
||||
+++ file.txt
|
||||
@@ ... @@
|
||||
-Original
|
||||
+Modified
|
||||
```
|
||||
"""
|
||||
edits = find_diffs(content)
|
||||
dump(edits)
|
||||
self.assertEqual(len(edits), 1)
|
||||
|
||||
edit = edits[0]
|
||||
self.assertEqual(edit[0], "file.txt")
|
||||
self.assertEqual(edit[1], ["-Original\n", "+Modified\n"])
|
||||
|
||||
def test_find_diffs_dirname_with_spaces(self):
|
||||
# Test find_diffs with a single hunk
|
||||
content = """
|
||||
Some text...
|
||||
|
||||
```diff
|
||||
--- dir name with spaces/file.txt
|
||||
+++ dir name with spaces/file.txt
|
||||
@@ ... @@
|
||||
-Original
|
||||
+Modified
|
||||
```
|
||||
"""
|
||||
edits = find_diffs(content)
|
||||
dump(edits)
|
||||
self.assertEqual(len(edits), 1)
|
||||
|
||||
edit = edits[0]
|
||||
self.assertEqual(edit[0], "dir name with spaces/file.txt")
|
||||
self.assertEqual(edit[1], ["-Original\n", "+Modified\n"])
|
||||
|
||||
def test_find_multi_diffs(self):
|
||||
content = """
|
||||
To implement the `--check-update` option, I will make the following changes:
|
||||
|
||||
1. Add the `--check-update` argument to the argument parser in `aider/main.py`.
|
||||
2. Modify the `check_version` function in `aider/versioncheck.py` to return a boolean indicating whether an update is available.
|
||||
3. Use the returned value from `check_version` in `aider/main.py` to set the exit status code when `--check-update` is used.
|
||||
|
||||
Here are the diffs for those changes:
|
||||
|
||||
```diff
|
||||
--- aider/versioncheck.py
|
||||
+++ aider/versioncheck.py
|
||||
@@ ... @@
|
||||
except Exception as err:
|
||||
print_cmd(f"Error checking pypi for new version: {err}")
|
||||
+ return False
|
||||
|
||||
--- aider/main.py
|
||||
+++ aider/main.py
|
||||
@@ ... @@
|
||||
other_group.add_argument(
|
||||
"--version",
|
||||
action="version",
|
||||
version=f"%(prog)s {__version__}",
|
||||
help="Show the version number and exit",
|
||||
)
|
||||
+ other_group.add_argument(
|
||||
+ "--check-update",
|
||||
+ action="store_true",
|
||||
+ help="Check for updates and return status in the exit code",
|
||||
+ default=False,
|
||||
+ )
|
||||
other_group.add_argument(
|
||||
"--apply",
|
||||
metavar="FILE",
|
||||
```
|
||||
|
||||
These changes will add the `--check-update` option to the command-line interface and use the `check_version` function to determine if an update is available, exiting with status code `0` if no update is available and `1` if an update is available.
|
||||
""" # noqa: E501
|
||||
|
||||
edits = find_diffs(content)
|
||||
dump(edits)
|
||||
self.assertEqual(len(edits), 2)
|
||||
self.assertEqual(len(edits[0][1]), 3)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
15
aider/tests/test_urls.py
Normal file
15
aider/tests/test_urls.py
Normal file
|
@ -0,0 +1,15 @@
|
|||
import requests
|
||||
|
||||
from aider import urls
|
||||
|
||||
|
||||
def test_urls():
|
||||
url_attributes = [
|
||||
attr
|
||||
for attr in dir(urls)
|
||||
if not callable(getattr(urls, attr)) and not attr.startswith("__")
|
||||
]
|
||||
for attr in url_attributes:
|
||||
url = getattr(urls, attr)
|
||||
response = requests.get(url)
|
||||
assert response.status_code == 200, f"URL {url} returned status code {response.status_code}"
|
321
aider/tests/test_wholefile.py
Normal file
321
aider/tests/test_wholefile.py
Normal file
|
@ -0,0 +1,321 @@
|
|||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from aider.coders import Coder
|
||||
from aider.coders.wholefile_coder import WholeFileCoder
|
||||
from aider.dump import dump # noqa: F401
|
||||
from aider.io import InputOutput
|
||||
from aider.models import Model
|
||||
|
||||
|
||||
class TestWholeFileCoder(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.original_cwd = os.getcwd()
|
||||
self.tempdir = tempfile.mkdtemp()
|
||||
os.chdir(self.tempdir)
|
||||
|
||||
self.GPT35 = Model("gpt-3.5-turbo")
|
||||
|
||||
def tearDown(self):
|
||||
os.chdir(self.original_cwd)
|
||||
shutil.rmtree(self.tempdir, ignore_errors=True)
|
||||
|
||||
def test_no_files(self):
|
||||
# Initialize WholeFileCoder with the temporary directory
|
||||
io = InputOutput(yes=True)
|
||||
|
||||
coder = WholeFileCoder(main_model=self.GPT35, io=io, fnames=[])
|
||||
coder.partial_response_content = (
|
||||
'To print "Hello, World!" in most programming languages, you can use the following'
|
||||
' code:\n\n```python\nprint("Hello, World!")\n```\n\nThis code will output "Hello,'
|
||||
' World!" to the console.'
|
||||
)
|
||||
|
||||
# This is throwing ValueError!
|
||||
coder.render_incremental_response(True)
|
||||
|
||||
def test_no_files_new_file_should_ask(self):
|
||||
io = InputOutput(yes=False) # <- yes=FALSE
|
||||
coder = WholeFileCoder(main_model=self.GPT35, io=io, fnames=[])
|
||||
coder.partial_response_content = (
|
||||
'To print "Hello, World!" in most programming languages, you can use the following'
|
||||
' code:\n\nfoo.js\n```python\nprint("Hello, World!")\n```\n\nThis code will output'
|
||||
' "Hello, World!" to the console.'
|
||||
)
|
||||
coder.update_files()
|
||||
self.assertFalse(Path("foo.js").exists())
|
||||
|
||||
def test_update_files(self):
|
||||
# Create a sample file in the temporary directory
|
||||
sample_file = "sample.txt"
|
||||
with open(sample_file, "w") as f:
|
||||
f.write("Original content\n")
|
||||
|
||||
# Initialize WholeFileCoder with the temporary directory
|
||||
io = InputOutput(yes=True)
|
||||
coder = WholeFileCoder(main_model=self.GPT35, io=io, fnames=[sample_file])
|
||||
|
||||
# Set the partial response content with the updated content
|
||||
coder.partial_response_content = f"{sample_file}\n```\nUpdated content\n```"
|
||||
|
||||
# Call update_files method
|
||||
edited_files = coder.update_files()
|
||||
|
||||
# Check if the sample file was updated
|
||||
self.assertIn("sample.txt", edited_files)
|
||||
|
||||
# Check if the content of the sample file was updated
|
||||
with open(sample_file, "r") as f:
|
||||
updated_content = f.read()
|
||||
self.assertEqual(updated_content, "Updated content\n")
|
||||
|
||||
def test_update_files_live_diff(self):
|
||||
# Create a sample file in the temporary directory
|
||||
sample_file = "sample.txt"
|
||||
with open(sample_file, "w") as f:
|
||||
f.write("\n".join(map(str, range(0, 100))))
|
||||
|
||||
# Initialize WholeFileCoder with the temporary directory
|
||||
io = InputOutput(yes=True)
|
||||
coder = WholeFileCoder(main_model=self.GPT35, io=io, fnames=[sample_file])
|
||||
|
||||
# Set the partial response content with the updated content
|
||||
coder.partial_response_content = f"{sample_file}\n```\n0\n\1\n2\n"
|
||||
|
||||
lines = coder.get_edits(mode="diff").splitlines()
|
||||
|
||||
# the live diff should be concise, since we haven't changed anything yet
|
||||
self.assertLess(len(lines), 20)
|
||||
|
||||
def test_update_files_with_existing_fence(self):
|
||||
# Create a sample file in the temporary directory
|
||||
sample_file = "sample.txt"
|
||||
original_content = """
|
||||
Here is some quoted text:
|
||||
```
|
||||
Quote!
|
||||
```
|
||||
"""
|
||||
with open(sample_file, "w") as f:
|
||||
f.write(original_content)
|
||||
|
||||
# Initialize WholeFileCoder with the temporary directory
|
||||
io = InputOutput(yes=True)
|
||||
coder = WholeFileCoder(main_model=self.GPT35, io=io, fnames=[sample_file])
|
||||
|
||||
coder.choose_fence()
|
||||
|
||||
self.assertNotEqual(coder.fence[0], "```")
|
||||
|
||||
# Set the partial response content with the updated content
|
||||
coder.partial_response_content = (
|
||||
f"{sample_file}\n{coder.fence[0]}\nUpdated content\n{coder.fence[1]}"
|
||||
)
|
||||
|
||||
# Call update_files method
|
||||
edited_files = coder.update_files()
|
||||
|
||||
# Check if the sample file was updated
|
||||
self.assertIn("sample.txt", edited_files)
|
||||
|
||||
# Check if the content of the sample file was updated
|
||||
with open(sample_file, "r") as f:
|
||||
updated_content = f.read()
|
||||
self.assertEqual(updated_content, "Updated content\n")
|
||||
|
||||
def test_update_files_bogus_path_prefix(self):
|
||||
# Create a sample file in the temporary directory
|
||||
sample_file = "sample.txt"
|
||||
with open(sample_file, "w") as f:
|
||||
f.write("Original content\n")
|
||||
|
||||
# Initialize WholeFileCoder with the temporary directory
|
||||
io = InputOutput(yes=True)
|
||||
coder = WholeFileCoder(main_model=self.GPT35, io=io, fnames=[sample_file])
|
||||
|
||||
# Set the partial response content with the updated content
|
||||
# With path/to/ prepended onto the filename
|
||||
coder.partial_response_content = f"path/to/{sample_file}\n```\nUpdated content\n```"
|
||||
|
||||
# Call update_files method
|
||||
edited_files = coder.update_files()
|
||||
|
||||
# Check if the sample file was updated
|
||||
self.assertIn("sample.txt", edited_files)
|
||||
|
||||
# Check if the content of the sample file was updated
|
||||
with open(sample_file, "r") as f:
|
||||
updated_content = f.read()
|
||||
self.assertEqual(updated_content, "Updated content\n")
|
||||
|
||||
def test_update_files_not_in_chat(self):
|
||||
# Create a sample file in the temporary directory
|
||||
sample_file = "sample.txt"
|
||||
with open(sample_file, "w") as f:
|
||||
f.write("Original content\n")
|
||||
|
||||
# Initialize WholeFileCoder with the temporary directory
|
||||
io = InputOutput(yes=True)
|
||||
coder = WholeFileCoder(main_model=self.GPT35, io=io)
|
||||
|
||||
# Set the partial response content with the updated content
|
||||
coder.partial_response_content = f"{sample_file}\n```\nUpdated content\n```"
|
||||
|
||||
# Call update_files method
|
||||
edited_files = coder.update_files()
|
||||
|
||||
# Check if the sample file was updated
|
||||
self.assertIn("sample.txt", edited_files)
|
||||
|
||||
# Check if the content of the sample file was updated
|
||||
with open(sample_file, "r") as f:
|
||||
updated_content = f.read()
|
||||
self.assertEqual(updated_content, "Updated content\n")
|
||||
|
||||
def test_update_files_no_filename_single_file_in_chat(self):
|
||||
sample_file = "accumulate.py"
|
||||
content = (
|
||||
"def accumulate(collection, operation):\n return [operation(x) for x in"
|
||||
" collection]\n"
|
||||
)
|
||||
|
||||
with open(sample_file, "w") as f:
|
||||
f.write("Original content\n")
|
||||
|
||||
# Initialize WholeFileCoder with the temporary directory
|
||||
io = InputOutput(yes=True)
|
||||
coder = WholeFileCoder(main_model=self.GPT35, io=io, fnames=[sample_file])
|
||||
|
||||
# Set the partial response content with the updated content
|
||||
coder.partial_response_content = (
|
||||
f"Here's the modified `{sample_file}` file that implements the `accumulate`"
|
||||
f" function as per the given instructions:\n\n```\n{content}```\n\nThis"
|
||||
" implementation uses a list comprehension to apply the `operation` function to"
|
||||
" each element of the `collection` and returns the resulting list."
|
||||
)
|
||||
|
||||
# Call update_files method
|
||||
edited_files = coder.update_files()
|
||||
|
||||
# Check if the sample file was updated
|
||||
self.assertIn(sample_file, edited_files)
|
||||
|
||||
# Check if the content of the sample file was updated
|
||||
with open(sample_file, "r") as f:
|
||||
updated_content = f.read()
|
||||
self.assertEqual(updated_content, content)
|
||||
|
||||
def test_update_files_earlier_filename(self):
|
||||
fname_a = Path("a.txt")
|
||||
fname_b = Path("b.txt")
|
||||
|
||||
fname_a.write_text("before a\n")
|
||||
fname_b.write_text("before b\n")
|
||||
|
||||
response = """
|
||||
Here is a new version of `a.txt` for you to consider:
|
||||
|
||||
```
|
||||
after a
|
||||
```
|
||||
|
||||
And here is `b.txt`:
|
||||
|
||||
```
|
||||
after b
|
||||
```
|
||||
"""
|
||||
# Initialize WholeFileCoder with the temporary directory
|
||||
io = InputOutput(yes=True)
|
||||
coder = WholeFileCoder(main_model=self.GPT35, io=io, fnames=[fname_a, fname_b])
|
||||
|
||||
# Set the partial response content with the updated content
|
||||
coder.partial_response_content = response
|
||||
|
||||
# Call update_files method
|
||||
edited_files = coder.update_files()
|
||||
|
||||
# Check if the sample file was updated
|
||||
self.assertIn(str(fname_a), edited_files)
|
||||
self.assertIn(str(fname_b), edited_files)
|
||||
|
||||
self.assertEqual(fname_a.read_text(), "after a\n")
|
||||
self.assertEqual(fname_b.read_text(), "after b\n")
|
||||
|
||||
def test_update_named_file_but_extra_unnamed_code_block(self):
|
||||
sample_file = "hello.py"
|
||||
new_content = "new\ncontent\ngoes\nhere\n"
|
||||
|
||||
with open(sample_file, "w") as f:
|
||||
f.write("Original content\n")
|
||||
|
||||
# Initialize WholeFileCoder with the temporary directory
|
||||
io = InputOutput(yes=True)
|
||||
coder = WholeFileCoder(main_model=self.GPT35, io=io, fnames=[sample_file])
|
||||
|
||||
# Set the partial response content with the updated content
|
||||
coder.partial_response_content = (
|
||||
f"Here's the modified `{sample_file}` file that implements the `accumulate`"
|
||||
f" function as per the given instructions:\n\n```\n{new_content}```\n\nThis"
|
||||
" implementation uses a list comprehension to apply the `operation` function to"
|
||||
" each element of the `collection` and returns the resulting list.\n"
|
||||
"Run it like this:\n\n"
|
||||
"```\npython {sample_file}\n```\n\n"
|
||||
)
|
||||
|
||||
# Call update_files method
|
||||
edited_files = coder.update_files()
|
||||
|
||||
# Check if the sample file was updated
|
||||
self.assertIn(sample_file, edited_files)
|
||||
|
||||
# Check if the content of the sample file was updated
|
||||
with open(sample_file, "r") as f:
|
||||
updated_content = f.read()
|
||||
self.assertEqual(updated_content, new_content)
|
||||
|
||||
def test_full_edit(self):
|
||||
# Create a few temporary files
|
||||
_, file1 = tempfile.mkstemp()
|
||||
|
||||
with open(file1, "w", encoding="utf-8") as f:
|
||||
f.write("one\ntwo\nthree\n")
|
||||
|
||||
files = [file1]
|
||||
|
||||
# Initialize the Coder object with the mocked IO and mocked repo
|
||||
coder = Coder.create(self.GPT35, "whole", io=InputOutput(), fnames=files)
|
||||
|
||||
# no trailing newline so the response content below doesn't add ANOTHER newline
|
||||
new_content = "new\ntwo\nthree"
|
||||
|
||||
def mock_send(*args, **kwargs):
|
||||
coder.partial_response_content = f"""
|
||||
Do this:
|
||||
|
||||
{Path(file1).name}
|
||||
```
|
||||
{new_content}
|
||||
```
|
||||
|
||||
"""
|
||||
coder.partial_response_function_call = dict()
|
||||
return []
|
||||
|
||||
coder.send = MagicMock(side_effect=mock_send)
|
||||
|
||||
# Call the run method with a message
|
||||
coder.run(with_message="hi")
|
||||
|
||||
content = Path(file1).read_text(encoding="utf-8")
|
||||
|
||||
# check for one trailing newline
|
||||
self.assertEqual(content, new_content + "\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
7
aider/urls.py
Normal file
7
aider/urls.py
Normal file
|
@ -0,0 +1,7 @@
|
|||
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"
|
||||
favicon = "https://aider.chat/assets/icons/favicon-32x32.png"
|
||||
model_warnings = "https://aider.chat/docs/llms/warnings.html"
|
Loading…
Add table
Add a link
Reference in a new issue