This commit introduces the `--message-file` flag to the `aider` tool, allowing users to specify a file containing the message to send to GPT. This feature processes the reply and then exits, disabling the chat mode. The implementation includes reading the content of the specified file and using it as the prompt message.
Additionally, a unit test has been added to `tests/test_main.py` to ensure the correct functionality of the `--message-file` flag. The test includes necessary mocks to handle non-interactive environments and verifies that the tool behaves as expected when a message file is provided.
This enhancement improves the usability of the `aider` tool for users who prefer or require non-interactive execution, such as in scripting or automated workflows.
# Aider chat conversation:
USER: in `test_check_gitignore` can we have gitpython not use the global gitignore?
ASSISTANT: To ensure that GitPython does not use the global gitignore, we can set the `GIT_CONFIG_GLOBAL` environment variable to point to a non-existent file or an empty temporary file during the test execution. This will effectively ignore the global gitignore settings.
We will modify the `test_check_gitignore` method in the `tests/test_main.py` file to set the `GIT_CONFIG_GLOBAL` environment variable before the test runs and reset it after the test is completed.
Here are the *SEARCH/REPLACE* blocks:
tests/test_main.py
```python
<<<<<<< SEARCH
def test_check_gitignore(self):
make_repo()
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())
=======
def test_check_gitignore(self):
with tempfile.NamedTemporaryFile() as temp_gitconfig:
os.environ['GIT_CONFIG_GLOBAL'] = temp_gitconfig.name
make_repo()
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']
>>>>>>> REPLACE
```
This change will create a temporary file to act as a dummy global git configuration file and set the `GIT_CONFIG_GLOBAL` environment variable to its path. After the test, it will delete the environment variable to avoid affecting other tests or operations.
# Aider chat conversation:
USER: the real `get_commit_message` is being called in `test_main_with_empty_git_dir_new_file`
ASSISTANT: To prevent the real `get_commit_message` from being called during the test `test_main_with_empty_git_dir_new_file`, we can use the `unittest.mock.patch` decorator to replace it with a mock function.
Here's how to do it:
```python
tests/test_main.py
<<<<<<< ORIGINAL
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.AiderRepo.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"))
>>>>>>> UPDATED
```
This change will replace the `get_commit_message` method with a mock that always returns "mock commit message" during the execution of `test_main_with_empty_git_dir_new_file`.
# Aider chat conversation:
USER: I ran this command:
pytest tests/test_main.py -k test_main_with_empty_git_dir_new_file
And got this output:
============================= test session starts ==============================
platform darwin -- Python 3.11.3, pytest-7.3.1, pluggy-1.0.0
rootdir: /Users/gauthier/Projects/aider
configfile: pytest.ini
collected 9 items / 8 deselected / 1 selected
tests/test_main.py F [100%]
=================================== FAILURES ===================================
________________ TestMain.test_main_with_empty_git_dir_new_file ________________
self = <tests.test_main.TestMain testMethod=test_main_with_empty_git_dir_new_file>
def test_main_with_empty_git_dir_new_file(self):
make_repo()
> main(["--yes", "foo.txt"], input=DummyInput(), output=DummyOutput())
tests/test_main.py:44:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
aider/main.py:452: in main
coder.dirty_commit()
aider/coders/base_coder.py:850: in dirty_commit
self.repo.commit(message=message)
aider/repo.py:64: in commit
commit_message = self.get_commit_message(diffs, context)
aider/repo.py:110: in get_commit_message
_hash, response = send_with_retries(
.venv/lib/python3.11/site-packages/backoff/_sync.py:105: in retry
ret = target(*args, **kwargs)
aider/sendchat.py:43: in send_with_retries
res = openai.ChatCompletion.create(**kwargs)
.venv/lib/python3.11/site-packages/openai/api_resources/chat_completion.py:25: in create
return super().create(*args, **kwargs)
.venv/lib/python3.11/site-packages/openai/api_resources/abstract/engine_api_resource.py:153: in create
response, _, api_key = requestor.request(
.venv/lib/python3.11/site-packages/openai/api_requestor.py:298: in request
resp, got_stream = self._interpret_response(result, stream)
.venv/lib/python3.11/site-packages/openai/api_requestor.py:700: in _interpret_response
self._interpret_response_line(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <openai.api_requestor.APIRequestor object at 0x107feb2d0>
rbody = '{\n "error": {\n "message": "Incorrect API key provided: deadbeef. You can find your API key at https://pla...ys.",\n "type": "invalid_request_error",\n "param": null,\n "code": "invalid_api_key"\n }\n}\n'
rcode = 401
rheaders = {'Date': 'Fri, 21 Jul 2023 18:57:18 GMT', 'Content-Type': 'application/json; charset=utf-8', 'Content-Length': '258', ...F-Cache-Status': 'DYNAMIC', 'Server': 'cloudflare', 'CF-RAY': '7ea5ab3bde6d41e9-EWR', 'alt-svc': 'h3=":443"; ma=86400'}
stream = False
def _interpret_response_line(
self, rbody: str, rcode: int, rheaders, stream: bool
) -> OpenAIResponse:
# HTTP 204 response code does not have any content in the body.
if rcode == 204:
return OpenAIResponse(None, rheaders)
if rcode == 503:
raise error.ServiceUnavailableError(
"The server is overloaded or not ready yet.",
rbody,
rcode,
headers=rheaders,
)
try:
if 'text/plain' in rheaders.get('Content-Type', ''):
data = rbody
else:
data = json.loads(rbody)
except (JSONDecodeError, UnicodeDecodeError) as e:
raise error.APIError(
f"HTTP code {rcode} from API ({rbody})", rbody, rcode, headers=rheaders
) from e
resp = OpenAIResponse(data, rheaders)
# In the future, we might add a "status" parameter to errors
# to better handle the "error while streaming" case.
stream_error = stream and "error" in resp.data
if stream_error or not 200 <= rcode < 300:
> raise self.handle_error_response(
rbody, rcode, resp.data, rheaders, stream_error=stream_error
)
E openai.error.AuthenticationError: Incorrect API key provided: deadbeef. You can find your API key at https://platform.openai.com/account/api-keys.
.venv/lib/python3.11/site-packages/openai/api_requestor.py:763: AuthenticationError
----------------------------- Captured stdout call -----------------------------
Aider v0.9.1-dev
Add .aider* to .gitignore (recommended)? yes
Added .aider* to .gitignore
Model: gpt-4
Creating empty file foo.txt
Git repo: .git
Repo-map: universal-ctags using 1024 tokens
Added foo.txt to the chat.
Adding foo.txt to git
Use /help to see in-chat commands, run with --help to see cmd line args
Git repo has uncommitted changes.
Commit before the chat proceeds [y/n/commit message]? yes
=========================== short test summary info ============================
FAILED tests/test_main.py::TestMain::test_main_with_empty_git_dir_new_file - ...
======================= 1 failed, 8 deselected in 1.53s ========================
ASSISTANT: The error message indicates that the API key provided ("deadbeef") is incorrect. You need to provide a valid OpenAI API key.
The API key is set in the `setUp
^C KeyboardInterrupt
USER: it's not supposed to be calling the real `get_commit_message`
ASSISTANT: I see, the test is supposed to mock the `get_commit_message` function, but it seems like the real function is being called instead. This could be due to the mock not being applied correctly.
In the `test_main_with_empty_git_dir_new_file` function in the `tests/test_main.py` file, the `get_commit_message` function is being patched. However, it seems like the patch is not being applied correctly.
Let's modify the test to ensure that the `get_commit_message` function is being mocked correctly.
Here is the change:
```python
tests/test_main.py
<<<<<<< ORIGINAL
@patch("aider.repo.AiderRepo.get_commit_message", return_value="commit message!")
def test_main_with_empty_dir_new_file(self, get_commit_message):
main(["foo.txt", "--yes","--no-git"], input=DummyInput(), output=DummyOutput())
self.assertTrue(os.path.exists("foo.txt"))
=======
@patch("aider.repo.AiderRepo.get_commit_message", return_value="commit message!")
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"))
>>>>>>> UPDATED
```
In the updated code, I've replaced `get_commit_message` with `_` in the function arguments. This is a common convention in Python when the variable is not going to be used. This change ensures that the mock object is not being confused with the actual function.
# Aider chat conversation:
USER: @patch("aider.repo.AiderRepo.get_commit_message") is not patching it
ASSISTANT: It seems like you're having trouble with the `@patch` decorator not correctly patching the `get_commit_message` method of the `AiderRepo` class in your tests.
To help you with this, I need to understand the context better. Could you please provide more details about the issue? Are you getting any error messages? If so, could you share them?
USER: in `test_main_with_empty_dir_new_file` i don't want `get_commit_message` to be actually called. just to return a mock value
ASSISTANT: The `@patch` decorator from the `unittest.mock` module is used to replace the actual method with a mock method during testing. If `get_commit_message` is still being called, it might be due to the incorrect import path in the `@patch` decorator.
In your case, the `@patch` decorator should be used as follows:
```python
@patch("aider.repo.AiderRepo.get_commit_message", return_value="commit message!")
```
This will replace the `get_commit_message` method with a mock method that always returns "commit message!".
Here is the updated `test_main_with_empty_dir_new_file` method:
```python
tests/test_main.py
<<<<<<< ORIGINAL
@patch("aider.repo.AiderRepo.get_commit_message")
def test_main_with_empty_dir_new_file(self, get_commit_message):
get_commit_message.return_value = "commit message!"
main(["foo.txt", "--yes","--no-git"], input=DummyInput(), output=DummyOutput())
self.assertTrue(os.path.exists("foo.txt"))
=======
@patch("aider.repo.AiderRepo.get_commit_message", return_value="commit message!")
def test_main_with_empty_dir_new_file(self, get_commit_message):
main(["foo.txt", "--yes","--no-git"], input=DummyInput(), output=DummyOutput())
self.assertTrue(os.path.exists("foo.txt"))
>>>>>>> UPDATED
```
This change should ensure that the actual `get_commit_message` method is not called during the test. Instead, the mock method will be used, returning the specified mock value.
# Aider chat conversation:
USER: replace all the `subprocess.run(git` calls that are making new repos and setting config with `make_repo` calls
ASSISTANT: To replace all the `subprocess.run(git` calls that are making new repos and setting config with `make_repo` calls, we need to modify the `tests/test_main.py` file.
Here are the changes:
```python
tests/test_main.py
<<<<<<< ORIGINAL
subprocess.run(["git", "init"])
subprocess.run(["git", "config", "user.email", "dummy@example.com"])
subprocess.run(["git", "config", "user.name", "Dummy User"])
=======
from tests.utils import make_repo
make_repo()
>>>>>>> UPDATED
```
```python
tests/test_main.py
<<<<<<< ORIGINAL
subprocess.run(["git", "init"])
subprocess.run(["git", "config", "user.email", "dummy@example.com"])
subprocess.run(["git", "config", "user.name", "Dummy User"])
=======
from tests.utils import make_repo
make_repo()
>>>>>>> UPDATED
```
```python
tests/test_main.py
<<<<<<< ORIGINAL
subprocess.run(["git", "init"])
subprocess.run(["git", "config", "user.email", "dummy@example.com"])
subprocess.run(["git", "config", "user.name", "Dummy User"])
=======
from tests.utils import make_repo
make_repo()
>>>>>>> UPDATED
```
Please note that you need to import the `make_repo` function from `tests.utils` in each test where it's used.
# Aider chat conversation:
USER: add tests for `setup_git`
ASSISTANT: To add tests for the `setup_git` function, we need to modify the `tests/test_main.py` file. We will add a new test function `test_setup_git` in the `TestMain` class.
Here is the change:
```python
tests/test_main.py
<<<<<<< ORIGINAL
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
=======
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
def test_setup_git(self):
with patch("aider.main.git.Repo.init") as MockRepoInit:
MockRepoInit.return_value = git.Repo(self.tempdir)
git_root = setup_git(None, self.io)
self.assertEqual(git_root, self.tempdir)
MockRepoInit.assert_called_once_with(self.tempdir)
>>>>>>> UPDATED
```
In the new test `test_setup_git`, we are mocking the `git.Repo.init` function to return a git repository at `self.tempdir`. Then we call `setup_git` with `None` as the `git_root` and check if it returns `self.tempdir` as the new `git_root`. We also check if `git.Repo.init` was called once with `self.tempdir` as the argument.
# Context:
USER: how can i move the patch to setUp so that it is active for all tests?
ASSISTANT: To move the patch to the `setUp` method and make it active for all tests, you can use the `patch` as a context manager in the `setUp` method and store the mock object as an instance variable. Then, in the `tearDown` method, you can stop the patch. Here's how to do it:
```python
tests/test_main.py
<<<<<<< ORIGINAL
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)
=======
def setUp(self):
os.environ["OPENAI_API_KEY"] = "deadbeef"
self.original_cwd = os.getcwd()
self.tempdir = tempfile.mkdtemp()
os.chdir(self.tempdir)
self.patcher = patch("aider.main.Coder.check_model_availability")
self.mock_check = self.patcher.start()
self.mock_check.return_value = True
def tearDown(self):
os.chdir(self.original_cwd)
shutil.rmtree(self.tempdir)
self.patcher.stop()
>>>>>>> UPDATED
```
Now the patch is active for all tests, and you can remove the `with` statement from the `test_main_with_empty_dir_no_files_on_command` method:
```python
tests/test_main.py
<<<<<<< ORIGINAL
def test_main_with_empty_dir_no_files_on_command(self):
with patch("aider.main.Coder.check_model_availability") as mock_check:
mock_check.return_value = True
pipe_input = create_input(StringIO(""))
main([], input=pipe_input, output=DummyOutput())
pipe_input.close()
=======
def test_main_with_empty_dir_no_files_on_command(self):
pipe_input = create_input(StringIO(""))
main([], input=pipe_input, output=DummyOutput())
pipe_input.close()
>>>>>>> UPDATED
```
# Context:
USER: the patch in `setUp` is not correcting forcing a return of true
ASSISTANT: To ensure that the patch in `setUp` correctly forces a return of `True`, you need to modify the `setUp` method in the `TestMain` class in the `tests/test_main.py` file. Here's the change:
```python
tests/test_main.py
<<<<<<< ORIGINAL
def setUp(self):
os.environ["OPENAI_API_KEY"] = "deadbeef"
self.original_cwd = os.getcwd()
self.tempdir = tempfile.mkdtemp()
os.chdir(self.tempdir)
with patch("aider.main.Coder.check_model_availability") as self.mock_check:
self.mock_check.return_value = True
super().setUp()
=======
def setUp(self):
os.environ["OPENAI_API_KEY"] = "deadbeef"
self.original_cwd = os.getcwd()
self.tempdir = tempfile.mkdtemp()
os.chdir(self.tempdir)
self.mock_check = MagicMock(return_value=True)
with patch("aider.main.Coder.check_model_availability", new=self.mock_check):
super().setUp()
>>>>>>> UPDATED
```
This change ensures that the `check_model_availability` method is patched with a `MagicMock` object that always returns `True`.
# Context:
USER: the patch in `setUp` is not correcting forcing a return of true
ASSISTANT: To fix the patch in `setUp`, you need to start the patch and set the return value to `True`. Here's the change you need to make in the `tests/test_main.py` file:
```python
tests/test_main.py
<<<<<<< ORIGINAL
self.mock_check = patch("aider.main.Coder.check_model_availability")
self.mock_check.return_value = True
=======
with patch("aider.main.Coder.check_model_availability") as self.mock_check:
self.mock_check.return_value = True
super().setUp()
>>>>>>> UPDATED
```
This change will ensure that the `check_model_availability` method returns `True` during the test setup.
# Context:
USER: E AttributeError: <class 'aider.coder.Coder'> does not have the attribute 'check_for_model_availability'
ASSISTANT: It seems like you are trying to access the `check_for_model_availability` method of the `Coder` class, but it is not defined as an instance method. To fix this issue, you need to move the `check_model_availability` method inside the `Coder` class and update the tests accordingly.
Here are the changes you need to make:
1. Move the `
^C KeyboardInterrupt
USER: fix the `setUp`
ASSISTANT: To fix the `setUp` method in the `TestMain` class, you need to update the way you are mocking the `check_for_model_availability` method. Here are the changes you need to make:
1. Modify the `setUp` method in the `TestMain` class to correctly mock the `check_for_model_availability` method of the `Coder` class.
```python
tests/test_main.py
<<<<<<< ORIGINAL
with patch("aider.coder.Coder.check_for_model_availability") as mock_check:
mock_check.return_value = True
self.mock_check = mock_check
=======
with patch("aider.coder.Coder.check_model_availability") as mock_check:
mock_check.return_value = True
self.mock_check = mock_check
>>>>>>> UPDATED
```
This change will ensure that the `check_for_model_availability` method is correctly mocked during the tests.