mirror of
https://github.com/Aider-AI/aider.git
synced 2025-05-30 09:14:59 +00:00
Merge branch 'main' into call-graph
This commit is contained in:
commit
27bd993cab
26 changed files with 788 additions and 202 deletions
39
.github/workflows/python-tests.yml
vendored
Normal file
39
.github/workflows/python-tests.yml
vendored
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
name: Python Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
python-version: ["3.8", "3.9", "3.10", "3.11"]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Check out repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
|
- name: Install universal ctags
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y universal-ctags
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: |
|
||||||
|
python -m unittest discover
|
|
@ -9,3 +9,8 @@ repos:
|
||||||
hooks:
|
hooks:
|
||||||
- id: flake8
|
- id: flake8
|
||||||
args: ["--show-source"]
|
args: ["--show-source"]
|
||||||
|
- repo: https://github.com/pycqa/isort
|
||||||
|
rev: 5.12.0
|
||||||
|
hooks:
|
||||||
|
- id: isort
|
||||||
|
args: ["--profile", "black"]
|
||||||
|
|
81
README.md
81
README.md
|
@ -2,12 +2,20 @@
|
||||||
|
|
||||||
`aider` is a command-line chat tool that allows you to write and edit
|
`aider` is a command-line chat tool that allows you to write and edit
|
||||||
code with GPT-4. You can ask GPT to help you start
|
code with GPT-4. You can ask GPT to help you start
|
||||||
a new project, or add features to your existing code.
|
a new project, or modify code in your existing git repo.
|
||||||
`aider` is well suited for working with code in a git repo,
|
Aider has features to
|
||||||
as it has features to easily
|
[help GPT understand and modify larger codebases](https://aider.chat/docs/ctags.html)
|
||||||
commit, diff and undo changes proposed by GPT.
|
and aider makes it easy to git commit, diff & undo changes proposed by GPT.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
- [Example chat transcripts](#example-chat-transcripts)
|
||||||
|
- [Features](#features)
|
||||||
|
- [Installation](#installation)
|
||||||
|
- [Usage](#usage)
|
||||||
|
- [In chat commands](#in-chat-commands)
|
||||||
|
- [Tips](#tips)
|
||||||
|
- [Limitations](#limitations)
|
||||||
|
|
||||||
## Example chat transcripts
|
## Example chat transcripts
|
||||||
|
|
||||||
|
@ -22,13 +30,14 @@ Here are some example transcripts that show how you can chat with `aider` to wri
|
||||||
* [**Create a Black Box Test Case**](https://aider.chat/examples/add-test.html): GPT creates a "black box" test case without access to the source of the method being tested, using only a
|
* [**Create a Black Box Test Case**](https://aider.chat/examples/add-test.html): GPT creates a "black box" test case without access to the source of the method being tested, using only a
|
||||||
[high level map of the repository based on ctags](https://aider.chat/docs/ctags.html).
|
[high level map of the repository based on ctags](https://aider.chat/docs/ctags.html).
|
||||||
|
|
||||||
* [**Download, analyze and plot US Census data**](https://aider.chat/examples/census.html): GPT-4 downloads census data, suggests some hypotheses to test, tests one and then summarizes and plots a graph of the results.
|
* [**Honor the NO_COLOR env var**](https://aider.chat/examples/no-color.html): The user pastes the NO_COLOR spec from no-color.org into the chat, and GPT-4 modifies the application to conform.
|
||||||
|
|
||||||
You can find more chat transcripts on the [examples page](https://aider.chat/examples/).
|
You can find more chat transcripts on the [examples page](https://aider.chat/examples/).
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
* Chat with GPT-4 about your code by launching `aider` from the command line with set of source files to discuss and edit together.
|
* Chat with GPT-4 about your code by launching `aider` from the command line with set of source files to discuss and edit together. GPT can see and edit the content of those files.
|
||||||
|
* If you or GPT mention other filenames that are part of the git repo, `aider` will ask if you'd like to add them to the chat. See also the in-chat `/add` and `/drop` commands described below, which are other ways to manage which files are part of the chat context.
|
||||||
* Request new features, changes, improvements, or bug fixes to your code. Ask for new test cases, updated documentation or code refactors.
|
* Request new features, changes, improvements, or bug fixes to your code. Ask for new test cases, updated documentation or code refactors.
|
||||||
* `aider` will apply the edits suggested by GPT-4 directly to your source files.
|
* `aider` will apply the edits suggested by GPT-4 directly to your source files.
|
||||||
* `aider` will automatically commit each changeset to your local git repo with a descriptive commit message. These frequent, automatic commits provide a safety net. It's easy to undo `aider` changes or use standard git workflows to manage longer sequences of changes.
|
* `aider` will automatically commit each changeset to your local git repo with a descriptive commit message. These frequent, automatic commits provide a safety net. It's easy to undo `aider` changes or use standard git workflows to manage longer sequences of changes.
|
||||||
|
@ -50,7 +59,7 @@ which helps it understand and modify large codebases.
|
||||||
* From GitHub: `pip install git+https://github.com/paul-gauthier/aider.git`
|
* From GitHub: `pip install git+https://github.com/paul-gauthier/aider.git`
|
||||||
* From your local copy of the repo in develop mode to pick up local edits immediately: `pip install -e .`
|
* From your local copy of the repo in develop mode to pick up local edits immediately: `pip install -e .`
|
||||||
|
|
||||||
2. Set up your OpenAI API key as an environment variable `OPENAI_API_KEY` or by including it in a `.env` file.
|
2. Set up your OpenAI API key as an environment variable `OPENAI_API_KEY` or by including it in an `.aider.config.yml` file (see `aider --help`).
|
||||||
|
|
||||||
3. Optionally, install [universal ctags](https://github.com/universal-ctags/ctags). This is helpful if you plan to work with repositories with more than a handful of files. This allows `aider --ctags` to build a [map of your entire git repo](https://aider.chat/docs/ctags.html) and share it with GPT to help it better understand and modify large codebases.
|
3. Optionally, install [universal ctags](https://github.com/universal-ctags/ctags). This is helpful if you plan to work with repositories with more than a handful of files. This allows `aider --ctags` to build a [map of your entire git repo](https://aider.chat/docs/ctags.html) and share it with GPT to help it better understand and modify large codebases.
|
||||||
|
|
||||||
|
@ -64,46 +73,42 @@ aider <file1> <file2> ...
|
||||||
|
|
||||||
Replace `<file1>`, `<file2>`, etc., with the paths to the source code files you want to work on. These files will be added to the chat session.
|
Replace `<file1>`, `<file2>`, etc., with the paths to the source code files you want to work on. These files will be added to the chat session.
|
||||||
|
|
||||||
You can also just launch `aider` anywhere in a git repo without naming files on the command line.
|
You can also just launch `aider` anywhere in a git repo without naming
|
||||||
It will discover all the files in the repo.
|
files on the command line. It will discover all the files in the
|
||||||
You can then add and remove individual files in the chat session with the `/add` and `/drop` chat commands described below.
|
repo. You can then add and remove individual files in the chat
|
||||||
|
session with the `/add` and `/drop` chat commands described below.
|
||||||
|
If you or GPT mention one of the repo's filenames in the conversation,
|
||||||
|
`aider` will ask if you'd like to add it to the chat.
|
||||||
|
|
||||||
You can also use additional command-line options, environment variables or configuration file
|
You can also use additional command-line options, environment variables or configuration file
|
||||||
to set many options:
|
to set many options:
|
||||||
|
|
||||||
```
|
```
|
||||||
-h, --help show this help message and exit
|
|
||||||
-c CONFIG_FILE, --config CONFIG_FILE
|
-c CONFIG_FILE, --config CONFIG_FILE
|
||||||
Specify the config file (default: search for
|
Specify the config file (default: search for .aider.conf.yml in git root or home directory)
|
||||||
.aider.conf.yml in git root or home directory)
|
|
||||||
--input-history-file INPUT_HISTORY_FILE
|
--input-history-file INPUT_HISTORY_FILE
|
||||||
Specify the chat input history file (default:
|
Specify the chat input history file (default: .aider.input.history) [env var: AIDER_INPUT_HISTORY_FILE]
|
||||||
.aider.input.history) [env var: AIDER_INPUT_HISTORY_FILE]
|
|
||||||
--chat-history-file CHAT_HISTORY_FILE
|
--chat-history-file CHAT_HISTORY_FILE
|
||||||
Specify the chat history file (default:
|
Specify the chat history file (default: .aider.chat.history.md) [env var: AIDER_CHAT_HISTORY_FILE]
|
||||||
.aider.chat.history.md) [env var: AIDER_CHAT_HISTORY_FILE]
|
--model MODEL Specify the model to use for the main chat (default: gpt-4) [env var: AIDER_MODEL]
|
||||||
--model MODEL Specify the model to use for the main chat (default: gpt-4)
|
|
||||||
[env var: AIDER_MODEL]
|
|
||||||
-3 Use gpt-3.5-turbo model for the main chat (not advised)
|
-3 Use gpt-3.5-turbo model for the main chat (not advised)
|
||||||
--pretty Enable pretty, colorized output (default: True) [env var:
|
--pretty Enable pretty, colorized output (default: True) [env var: AIDER_PRETTY]
|
||||||
AIDER_PRETTY]
|
--no-pretty Disable pretty, colorized output [env var: AIDER_NO_PRETTY]
|
||||||
--no-pretty Disable pretty, colorized output
|
--apply FILE Apply the changes from the given file instead of running the chat (debug) [env var: AIDER_APPLY]
|
||||||
--apply FILE Apply the changes from the given file instead of running
|
--auto-commits Enable auto commit of changes (default: True) [env var: AIDER_AUTO_COMMITS]
|
||||||
the chat (debug)
|
--no-auto-commits Disable auto commit of changes [env var: AIDER_NO_AUTO_COMMITS]
|
||||||
--auto-commits Enable auto commit of changes (default: True) [env var:
|
--dirty-commits Enable dirty commit of changes [env var: AIDER_DIRTY_COMMITS]
|
||||||
AIDER_AUTO_COMMIT]
|
--no-dirty-commits Disable dirty commit of changes [env var: AIDER_NO_DIRTY_COMMITS]
|
||||||
--no-auto-commits Disable auto commit of changes
|
--openai-api-key OPENAI_API_KEY
|
||||||
--dry-run Perform a dry run without applying changes (default: False)
|
Specify the OpenAI API key [env var: OPENAI_API_KEY]
|
||||||
--show-diffs Show diffs when committing changes (default: False) [env
|
--dry-run Perform a dry run without applying changes (default: False) [env var: AIDER_DRY_RUN]
|
||||||
var: AIDER_SHOW_DIFFS]
|
--show-diffs Show diffs when committing changes (default: False) [env var: AIDER_SHOW_DIFFS]
|
||||||
--ctags [CTAGS] Add ctags to the chat to help GPT understand the codebase
|
--ctags [CTAGS] Add ctags to the chat to help GPT understand the codebase (default: check for ctags executable) [env var: AIDER_CTAGS]
|
||||||
(default: check for ctags executable) [env var:
|
--yes Always say yes to every confirmation [env var: AIDER_YES]
|
||||||
AIDER_CTAGS]
|
-v, --verbose Enable verbose output [env var: AIDER_VERBOSE]
|
||||||
--yes Always say yes to every confirmation
|
|
||||||
-v, --verbose Enable verbose output
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Chat commands
|
## In chat commands
|
||||||
|
|
||||||
`aider` supports the following commands from within the chat:
|
`aider` supports the following commands from within the chat:
|
||||||
|
|
||||||
|
@ -120,7 +125,7 @@ To use a command, simply type it in the chat input followed by any required argu
|
||||||
|
|
||||||
## Tips
|
## Tips
|
||||||
|
|
||||||
* Large changes are best performed as a sequence of bite sized steps. Same as if you were undertaking them by yourself.
|
* Large changes are best performed as a sequence of thoughtful bite sized steps, where you plan out the approach and overall design. Don't ask GPT to "build a house" all in one go. Instead, think about the architecture you want and then ask it to "build a foundation", "erect the walls", "run the wiring", etc.
|
||||||
* Use Control-C to safely interrupt `aider` if it isn't providing a useful response. The partial response remains in the conversation, so you can refer to it when you reply with more information or direction.
|
* Use Control-C to safely interrupt `aider` if it isn't providing a useful response. The partial response remains in the conversation, so you can refer to it when you reply with more information or direction.
|
||||||
* Use the `/run` command to run tests, linters, etc and show the output to GPT so it can fix any issues.
|
* Use the `/run` command to run tests, linters, etc and show the output to GPT so it can fix any issues.
|
||||||
* Enter a multiline chat message by entering `{` alone on the first line. End the multiline message with `}` alone on the last line.
|
* Enter a multiline chat message by entering `{` alone on the first line. End the multiline message with `}` alone on the last line.
|
||||||
|
|
|
@ -4,8 +4,8 @@
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
|
|
||||||
{% seo %}
|
{% seo %}
|
||||||
<meta property="og:image" content="{{ site.baseurl }}/screenshot.png">
|
<meta property="og:image" content="{{ site.baseurl }}/assets/screenshot.png">
|
||||||
<meta property="twitter:image" content="{{ site.baseurl }}/screenshot.png">
|
<meta property="twitter:image" content="{{ site.baseurl }}/assets/screenshot.png">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com">
|
<link rel="preconnect" href="https://fonts.gstatic.com">
|
||||||
<link rel="preload" href="https://fonts.googleapis.com/css?family=Open+Sans:400,700&display=swap" as="style" type="text/css" crossorigin>
|
<link rel="preload" href="https://fonts.googleapis.com/css?family=Open+Sans:400,700&display=swap" as="style" type="text/css" crossorigin>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
101
aider/coder.py
101
aider/coder.py
|
@ -2,25 +2,25 @@
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
|
||||||
import time
|
import time
|
||||||
from openai.error import RateLimitError
|
import traceback
|
||||||
|
|
||||||
from rich.console import Console
|
|
||||||
from rich.live import Live
|
|
||||||
from rich.markdown import Markdown
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import git
|
import git
|
||||||
import openai
|
import openai
|
||||||
|
from openai.error import RateLimitError
|
||||||
|
from rich.console import Console
|
||||||
|
from rich.live import Live
|
||||||
|
from rich.markdown import Markdown
|
||||||
|
|
||||||
# from aider.dump import dump
|
# from aider.dump import dump
|
||||||
from aider import utils
|
from aider import prompts, utils
|
||||||
from aider import prompts
|
|
||||||
from aider.commands import Commands
|
from aider.commands import Commands
|
||||||
from aider.repomap import RepoMap
|
from aider.repomap import RepoMap
|
||||||
|
|
||||||
openai.api_key = os.getenv("OPENAI_API_KEY")
|
|
||||||
|
class MissingAPIKeyError(ValueError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Coder:
|
class Coder:
|
||||||
|
@ -37,10 +37,17 @@ class Coder:
|
||||||
pretty=True,
|
pretty=True,
|
||||||
show_diffs=False,
|
show_diffs=False,
|
||||||
auto_commits=True,
|
auto_commits=True,
|
||||||
|
dirty_commits=True,
|
||||||
dry_run=False,
|
dry_run=False,
|
||||||
use_ctags=False,
|
use_ctags=False,
|
||||||
verbose=False,
|
verbose=False,
|
||||||
|
openai_api_key=None,
|
||||||
):
|
):
|
||||||
|
if openai_api_key:
|
||||||
|
openai.api_key = openai_api_key
|
||||||
|
else:
|
||||||
|
raise MissingAPIKeyError("No OpenAI API key provided.")
|
||||||
|
|
||||||
self.verbose = verbose
|
self.verbose = verbose
|
||||||
self.abs_fnames = set()
|
self.abs_fnames = set()
|
||||||
self.cur_messages = []
|
self.cur_messages = []
|
||||||
|
@ -48,6 +55,7 @@ class Coder:
|
||||||
|
|
||||||
self.io = io
|
self.io = io
|
||||||
self.auto_commits = auto_commits
|
self.auto_commits = auto_commits
|
||||||
|
self.dirty_commits = dirty_commits
|
||||||
self.dry_run = dry_run
|
self.dry_run = dry_run
|
||||||
|
|
||||||
if pretty:
|
if pretty:
|
||||||
|
@ -66,7 +74,7 @@ class Coder:
|
||||||
|
|
||||||
if self.repo:
|
if self.repo:
|
||||||
rel_repo_dir = os.path.relpath(self.repo.git_dir, os.getcwd())
|
rel_repo_dir = os.path.relpath(self.repo.git_dir, os.getcwd())
|
||||||
self.io.tool("Using git repo:", rel_repo_dir)
|
self.io.tool_output("Using git repo:", rel_repo_dir)
|
||||||
else:
|
else:
|
||||||
self.io.tool_error("No suitable git repo, will not automatically commit edits.")
|
self.io.tool_error("No suitable git repo, will not automatically commit edits.")
|
||||||
self.find_common_root()
|
self.find_common_root()
|
||||||
|
@ -83,7 +91,7 @@ class Coder:
|
||||||
else:
|
else:
|
||||||
self.root = os.getcwd()
|
self.root = os.getcwd()
|
||||||
|
|
||||||
self.io.tool(f"Common root directory: {self.root}")
|
self.io.tool_output(f"Common root directory: {self.root}")
|
||||||
|
|
||||||
def set_repo(self, cmd_line_fnames):
|
def set_repo(self, cmd_line_fnames):
|
||||||
if not cmd_line_fnames:
|
if not cmd_line_fnames:
|
||||||
|
@ -93,7 +101,7 @@ class Coder:
|
||||||
for fname in cmd_line_fnames:
|
for fname in cmd_line_fnames:
|
||||||
fname = Path(fname)
|
fname = Path(fname)
|
||||||
if not fname.exists():
|
if not fname.exists():
|
||||||
self.io.tool(f"Creating empty file {fname}")
|
self.io.tool_output(f"Creating empty file {fname}")
|
||||||
fname.parent.mkdir(parents=True, exist_ok=True)
|
fname.parent.mkdir(parents=True, exist_ok=True)
|
||||||
fname.touch()
|
fname.touch()
|
||||||
|
|
||||||
|
@ -106,7 +114,7 @@ class Coder:
|
||||||
if fname.is_dir():
|
if fname.is_dir():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
self.io.tool(f"Added {fname} to the chat")
|
self.io.tool_output(f"Added {fname} to the chat")
|
||||||
|
|
||||||
fname = fname.resolve()
|
fname = fname.resolve()
|
||||||
self.abs_fnames.add(str(fname))
|
self.abs_fnames.add(str(fname))
|
||||||
|
@ -135,18 +143,18 @@ class Coder:
|
||||||
if new_files:
|
if new_files:
|
||||||
rel_repo_dir = os.path.relpath(repo.git_dir, os.getcwd())
|
rel_repo_dir = os.path.relpath(repo.git_dir, os.getcwd())
|
||||||
|
|
||||||
self.io.tool(f"Files not tracked in {rel_repo_dir}:")
|
self.io.tool_output(f"Files not tracked in {rel_repo_dir}:")
|
||||||
for fn in new_files:
|
for fn in new_files:
|
||||||
self.io.tool(f" - {fn}")
|
self.io.tool_output(f" - {fn}")
|
||||||
if self.io.confirm_ask("Add them?"):
|
if self.io.confirm_ask("Add them?"):
|
||||||
for relative_fname in new_files:
|
for relative_fname in new_files:
|
||||||
repo.git.add(relative_fname)
|
repo.git.add(relative_fname)
|
||||||
self.io.tool(f"Added {relative_fname} to the git repo")
|
self.io.tool_output(f"Added {relative_fname} to the git repo")
|
||||||
show_files = ", ".join(new_files)
|
show_files = ", ".join(new_files)
|
||||||
commit_message = f"Added new files to the git repo: {show_files}"
|
commit_message = f"Added new files to the git repo: {show_files}"
|
||||||
repo.git.commit("-m", commit_message, "--no-verify")
|
repo.git.commit("-m", commit_message, "--no-verify")
|
||||||
commit_hash = repo.head.commit.hexsha[:7]
|
commit_hash = repo.head.commit.hexsha[:7]
|
||||||
self.io.tool(f"Commit {commit_hash} {commit_message}")
|
self.io.tool_output(f"Commit {commit_hash} {commit_message}")
|
||||||
else:
|
else:
|
||||||
self.io.tool_error("Skipped adding new files to the git repo.")
|
self.io.tool_error("Skipped adding new files to the git repo.")
|
||||||
return
|
return
|
||||||
|
@ -216,7 +224,7 @@ class Coder:
|
||||||
if is_commit_command:
|
if is_commit_command:
|
||||||
return
|
return
|
||||||
|
|
||||||
if not self.auto_commits:
|
if not self.dirty_commits:
|
||||||
return
|
return
|
||||||
if not self.repo:
|
if not self.repo:
|
||||||
return
|
return
|
||||||
|
@ -227,7 +235,12 @@ class Coder:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def run_loop(self):
|
def run_loop(self):
|
||||||
inp = self.io.get_input(self.abs_fnames, self.commands)
|
inp = self.io.get_input(
|
||||||
|
self.root,
|
||||||
|
self.get_inchat_relative_files(),
|
||||||
|
self.get_addable_relative_files(),
|
||||||
|
self.commands,
|
||||||
|
)
|
||||||
|
|
||||||
self.num_control_c = 0
|
self.num_control_c = 0
|
||||||
|
|
||||||
|
@ -243,7 +256,7 @@ class Coder:
|
||||||
self.cur_messages = []
|
self.cur_messages = []
|
||||||
|
|
||||||
if inp.strip():
|
if inp.strip():
|
||||||
self.io.tool("Use up-arrow to retry previous command:", inp)
|
self.io.tool_output("Use up-arrow to retry previous command:", inp)
|
||||||
return
|
return
|
||||||
|
|
||||||
if not inp:
|
if not inp:
|
||||||
|
@ -252,6 +265,8 @@ class Coder:
|
||||||
if self.commands.is_command(inp):
|
if self.commands.is_command(inp):
|
||||||
return self.commands.run(inp)
|
return self.commands.run(inp)
|
||||||
|
|
||||||
|
self.check_for_file_mentions(inp)
|
||||||
|
|
||||||
return self.send_new_user_message(inp)
|
return self.send_new_user_message(inp)
|
||||||
|
|
||||||
def send_new_user_message(self, inp):
|
def send_new_user_message(self, inp):
|
||||||
|
@ -279,7 +294,7 @@ class Coder:
|
||||||
dict(role="assistant", content=content),
|
dict(role="assistant", content=content),
|
||||||
]
|
]
|
||||||
|
|
||||||
self.io.tool()
|
self.io.tool_output()
|
||||||
if interrupted:
|
if interrupted:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -327,20 +342,29 @@ class Coder:
|
||||||
quotes = "".join(['"', "'", "`"])
|
quotes = "".join(['"', "'", "`"])
|
||||||
words = set(word.strip(quotes) for word in words)
|
words = set(word.strip(quotes) for word in words)
|
||||||
|
|
||||||
addable_rel_fnames = set(self.get_all_relative_files()) - set(
|
addable_rel_fnames = self.get_addable_relative_files()
|
||||||
self.get_inchat_relative_files()
|
|
||||||
)
|
|
||||||
|
|
||||||
mentioned_rel_fnames = set()
|
mentioned_rel_fnames = set()
|
||||||
for word in words:
|
fname_to_rel_fnames = {}
|
||||||
if word in addable_rel_fnames:
|
for rel_fname in addable_rel_fnames:
|
||||||
mentioned_rel_fnames.add(word)
|
fname = os.path.basename(rel_fname)
|
||||||
|
if fname not in fname_to_rel_fnames:
|
||||||
|
fname_to_rel_fnames[fname] = []
|
||||||
|
fname_to_rel_fnames[fname].append(rel_fname)
|
||||||
|
|
||||||
|
for fname, rel_fnames in fname_to_rel_fnames.items():
|
||||||
|
if len(rel_fnames) == 1 and fname in words:
|
||||||
|
mentioned_rel_fnames.add(rel_fnames[0])
|
||||||
|
else:
|
||||||
|
for rel_fname in rel_fnames:
|
||||||
|
if rel_fname in words:
|
||||||
|
mentioned_rel_fnames.add(rel_fname)
|
||||||
|
|
||||||
if not mentioned_rel_fnames:
|
if not mentioned_rel_fnames:
|
||||||
return
|
return
|
||||||
|
|
||||||
for rel_fname in mentioned_rel_fnames:
|
for rel_fname in mentioned_rel_fnames:
|
||||||
self.io.tool(rel_fname)
|
self.io.tool_output(rel_fname)
|
||||||
|
|
||||||
if not self.io.confirm_ask("Add these files to the chat?"):
|
if not self.io.confirm_ask("Add these files to the chat?"):
|
||||||
return
|
return
|
||||||
|
@ -450,9 +474,9 @@ class Coder:
|
||||||
edited.add(path)
|
edited.add(path)
|
||||||
if utils.do_replace(full_path, original, updated, self.dry_run):
|
if utils.do_replace(full_path, original, updated, self.dry_run):
|
||||||
if self.dry_run:
|
if self.dry_run:
|
||||||
self.io.tool(f"Dry run, did not apply edit to {path}")
|
self.io.tool_output(f"Dry run, did not apply edit to {path}")
|
||||||
else:
|
else:
|
||||||
self.io.tool(f"Applied edit to {path}")
|
self.io.tool_output(f"Applied edit to {path}")
|
||||||
else:
|
else:
|
||||||
self.io.tool_error(f"Failed to apply edit to {path}")
|
self.io.tool_error(f"Failed to apply edit to {path}")
|
||||||
|
|
||||||
|
@ -490,7 +514,9 @@ class Coder:
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
commit_message = commit_message.strip().strip('"').strip()
|
commit_message = commit_message.strip()
|
||||||
|
if commit_message and commit_message[0] == '"' and commit_message[-1] == '"':
|
||||||
|
commit_message = commit_message[1:-1].strip()
|
||||||
|
|
||||||
if interrupted:
|
if interrupted:
|
||||||
self.io.tool_error(
|
self.io.tool_error(
|
||||||
|
@ -548,7 +574,7 @@ class Coder:
|
||||||
raise ValueError(f"Invalid value for 'which': {which}")
|
raise ValueError(f"Invalid value for 'which': {which}")
|
||||||
|
|
||||||
if self.show_diffs or ask:
|
if self.show_diffs or ask:
|
||||||
# don't use io.tool() because we don't want to log or further colorize
|
# don't use io.tool_output() because we don't want to log or further colorize
|
||||||
print(diffs)
|
print(diffs)
|
||||||
|
|
||||||
context = self.get_context_from_history(history)
|
context = self.get_context_from_history(history)
|
||||||
|
@ -562,9 +588,9 @@ class Coder:
|
||||||
|
|
||||||
if ask:
|
if ask:
|
||||||
if which == "repo_files":
|
if which == "repo_files":
|
||||||
self.io.tool("Git repo has uncommitted changes.")
|
self.io.tool_output("Git repo has uncommitted changes.")
|
||||||
else:
|
else:
|
||||||
self.io.tool("Files have uncommitted changes.")
|
self.io.tool_output("Files have uncommitted changes.")
|
||||||
|
|
||||||
res = self.io.prompt_ask(
|
res = self.io.prompt_ask(
|
||||||
"Commit before the chat proceeds [y/n/commit message]?",
|
"Commit before the chat proceeds [y/n/commit message]?",
|
||||||
|
@ -572,7 +598,7 @@ class Coder:
|
||||||
).strip()
|
).strip()
|
||||||
self.last_asked_for_commit_time = self.get_last_modified()
|
self.last_asked_for_commit_time = self.get_last_modified()
|
||||||
|
|
||||||
self.io.tool()
|
self.io.tool_output()
|
||||||
|
|
||||||
if res.lower() in ["n", "no"]:
|
if res.lower() in ["n", "no"]:
|
||||||
self.io.tool_error("Skipped commmit.")
|
self.io.tool_error("Skipped commmit.")
|
||||||
|
@ -585,7 +611,7 @@ class Coder:
|
||||||
full_commit_message = commit_message + "\n\n" + context
|
full_commit_message = commit_message + "\n\n" + context
|
||||||
repo.git.commit("-m", full_commit_message, "--no-verify")
|
repo.git.commit("-m", full_commit_message, "--no-verify")
|
||||||
commit_hash = repo.head.commit.hexsha[:7]
|
commit_hash = repo.head.commit.hexsha[:7]
|
||||||
self.io.tool(f"Commit {commit_hash} {commit_message}")
|
self.io.tool_output(f"Commit {commit_hash} {commit_message}")
|
||||||
|
|
||||||
return commit_hash, commit_message
|
return commit_hash, commit_message
|
||||||
|
|
||||||
|
@ -615,6 +641,9 @@ class Coder:
|
||||||
return 0
|
return 0
|
||||||
return max(Path(path).stat().st_mtime for path in files)
|
return max(Path(path).stat().st_mtime for path in files)
|
||||||
|
|
||||||
|
def get_addable_relative_files(self):
|
||||||
|
return set(self.get_all_relative_files()) - set(self.get_inchat_relative_files())
|
||||||
|
|
||||||
def apply_updates(self, content, inp):
|
def apply_updates(self, content, inp):
|
||||||
try:
|
try:
|
||||||
edited = self.update_files(content, inp)
|
edited = self.update_files(content, inp)
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import sys
|
|
||||||
import os
|
import os
|
||||||
import git
|
|
||||||
import subprocess
|
|
||||||
import shlex
|
import shlex
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import git
|
||||||
from prompt_toolkit.completion import Completion
|
from prompt_toolkit.completion import Completion
|
||||||
|
|
||||||
from aider import prompts
|
from aider import prompts
|
||||||
|
|
||||||
|
|
||||||
|
@ -37,7 +39,7 @@ class Commands:
|
||||||
if cmd_method:
|
if cmd_method:
|
||||||
return cmd_method(args)
|
return cmd_method(args)
|
||||||
else:
|
else:
|
||||||
self.io.tool(f"Error: Command {cmd_name} not found.")
|
self.io.tool_output(f"Error: Command {cmd_name} not found.")
|
||||||
|
|
||||||
def run(self, inp):
|
def run(self, inp):
|
||||||
words = inp.strip().split()
|
words = inp.strip().split()
|
||||||
|
@ -110,7 +112,7 @@ class Commands:
|
||||||
self.io.tool_error("The last commit was not made by aider in this chat session.")
|
self.io.tool_error("The last commit was not made by aider in this chat session.")
|
||||||
return
|
return
|
||||||
self.coder.repo.git.reset("--hard", "HEAD~1")
|
self.coder.repo.git.reset("--hard", "HEAD~1")
|
||||||
self.io.tool(
|
self.io.tool_output(
|
||||||
f"{last_commit.message.strip()}\n"
|
f"{last_commit.message.strip()}\n"
|
||||||
f"The above commit {self.coder.last_aider_commit_hash} "
|
f"The above commit {self.coder.last_aider_commit_hash} "
|
||||||
"was reset and removed from git.\n"
|
"was reset and removed from git.\n"
|
||||||
|
@ -131,7 +133,7 @@ class Commands:
|
||||||
commits = f"{self.coder.last_aider_commit_hash}~1"
|
commits = f"{self.coder.last_aider_commit_hash}~1"
|
||||||
diff = self.coder.get_diffs(commits, self.coder.last_aider_commit_hash)
|
diff = self.coder.get_diffs(commits, self.coder.last_aider_commit_hash)
|
||||||
|
|
||||||
# don't use io.tool() because we don't want to log or further colorize
|
# don't use io.tool_output() because we don't want to log or further colorize
|
||||||
print(diff)
|
print(diff)
|
||||||
|
|
||||||
def completions_add(self, partial):
|
def completions_add(self, partial):
|
||||||
|
@ -177,7 +179,7 @@ class Commands:
|
||||||
abs_file_path = os.path.abspath(os.path.join(self.coder.root, matched_file))
|
abs_file_path = os.path.abspath(os.path.join(self.coder.root, matched_file))
|
||||||
if abs_file_path not in self.coder.abs_fnames:
|
if abs_file_path not in self.coder.abs_fnames:
|
||||||
self.coder.abs_fnames.add(abs_file_path)
|
self.coder.abs_fnames.add(abs_file_path)
|
||||||
self.io.tool(f"Added {matched_file} to the chat")
|
self.io.tool_output(f"Added {matched_file} to the chat")
|
||||||
added_fnames.append(matched_file)
|
added_fnames.append(matched_file)
|
||||||
else:
|
else:
|
||||||
self.io.tool_error(f"{matched_file} is already in the chat")
|
self.io.tool_error(f"{matched_file} is already in the chat")
|
||||||
|
@ -214,7 +216,7 @@ class Commands:
|
||||||
for matched_file in matched_files:
|
for matched_file in matched_files:
|
||||||
relative_fname = os.path.relpath(matched_file, self.coder.root)
|
relative_fname = os.path.relpath(matched_file, self.coder.root)
|
||||||
self.coder.abs_fnames.remove(matched_file)
|
self.coder.abs_fnames.remove(matched_file)
|
||||||
self.io.tool(f"Removed {relative_fname} from the chat")
|
self.io.tool_output(f"Removed {relative_fname} from the chat")
|
||||||
|
|
||||||
def cmd_run(self, args):
|
def cmd_run(self, args):
|
||||||
"Run a shell command and optionally add the output to the chat"
|
"Run a shell command and optionally add the output to the chat"
|
||||||
|
@ -229,15 +231,15 @@ class Commands:
|
||||||
|
|
||||||
print(combined_output)
|
print(combined_output)
|
||||||
|
|
||||||
ok = self.io.confirm_ask("Add the output to the chat?", default="y")
|
if self.io.confirm_ask("Add the output to the chat?", default="y"):
|
||||||
if not ok:
|
for line in combined_output.splitlines():
|
||||||
return
|
self.io.tool_output(line, log_only=True)
|
||||||
|
|
||||||
msg = prompts.run_output.format(
|
msg = prompts.run_output.format(
|
||||||
command=args,
|
command=args,
|
||||||
output=combined_output,
|
output=combined_output,
|
||||||
)
|
)
|
||||||
return msg
|
return msg
|
||||||
|
|
||||||
def cmd_exit(self, args):
|
def cmd_exit(self, args):
|
||||||
"Exit the application"
|
"Exit the application"
|
||||||
|
@ -258,14 +260,14 @@ class Commands:
|
||||||
other_files.append(file)
|
other_files.append(file)
|
||||||
|
|
||||||
if chat_files:
|
if chat_files:
|
||||||
self.io.tool("Files in chat:\n")
|
self.io.tool_output("Files in chat:\n")
|
||||||
for file in chat_files:
|
for file in chat_files:
|
||||||
self.io.tool(f" {file}")
|
self.io.tool_output(f" {file}")
|
||||||
|
|
||||||
if other_files:
|
if other_files:
|
||||||
self.io.tool("\nRepo files not in the chat:\n")
|
self.io.tool_output("\nRepo files not in the chat:\n")
|
||||||
for file in other_files:
|
for file in other_files:
|
||||||
self.io.tool(f" {file}")
|
self.io.tool_output(f" {file}")
|
||||||
|
|
||||||
def cmd_help(self, args):
|
def cmd_help(self, args):
|
||||||
"Show help about all commands"
|
"Show help about all commands"
|
||||||
|
@ -275,6 +277,6 @@ class Commands:
|
||||||
cmd_method = getattr(self, cmd_method_name, None)
|
cmd_method = getattr(self, cmd_method_name, None)
|
||||||
if cmd_method:
|
if cmd_method:
|
||||||
description = cmd_method.__doc__
|
description = cmd_method.__doc__
|
||||||
self.io.tool(f"{cmd} {description}")
|
self.io.tool_output(f"{cmd} {description}")
|
||||||
else:
|
else:
|
||||||
self.io.tool(f"{cmd} No description available.")
|
self.io.tool_output(f"{cmd} No description available.")
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import traceback
|
|
||||||
import json
|
import json
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
|
||||||
def cvt(s):
|
def cvt(s):
|
||||||
|
|
106
aider/io.py
106
aider/io.py
|
@ -1,24 +1,49 @@
|
||||||
import os
|
import os
|
||||||
from prompt_toolkit.styles import Style
|
from collections import defaultdict
|
||||||
from prompt_toolkit.shortcuts import PromptSession, prompt
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from prompt_toolkit.completion import Completer, Completion
|
from prompt_toolkit.completion import Completer, Completion
|
||||||
from prompt_toolkit.history import FileHistory
|
from prompt_toolkit.history import FileHistory
|
||||||
from prompt_toolkit.shortcuts import CompleteStyle
|
from prompt_toolkit.shortcuts import CompleteStyle, PromptSession, prompt
|
||||||
|
from prompt_toolkit.styles import Style
|
||||||
|
from pygments.lexers import guess_lexer_for_filename
|
||||||
|
from pygments.token import Token
|
||||||
|
from pygments.util import ClassNotFound
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.text import Text
|
from rich.text import Text
|
||||||
from pathlib import Path
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from aider import utils
|
|
||||||
|
|
||||||
|
|
||||||
class FileContentCompleter(Completer):
|
class FileContentCompleter(Completer):
|
||||||
def __init__(self, fnames, commands):
|
def __init__(self, root, rel_fnames, addable_rel_fnames, commands):
|
||||||
self.commands = commands
|
self.commands = commands
|
||||||
|
self.addable_rel_fnames = addable_rel_fnames
|
||||||
|
self.rel_fnames = rel_fnames
|
||||||
|
|
||||||
|
fname_to_rel_fnames = defaultdict(list)
|
||||||
|
for rel_fname in addable_rel_fnames:
|
||||||
|
fname = os.path.basename(rel_fname)
|
||||||
|
if fname != rel_fname:
|
||||||
|
fname_to_rel_fnames[fname].append(rel_fname)
|
||||||
|
self.fname_to_rel_fnames = fname_to_rel_fnames
|
||||||
|
|
||||||
self.words = set()
|
self.words = set()
|
||||||
for fname in fnames:
|
|
||||||
self.words.update(utils.get_name_identifiers(fname))
|
for rel_fname in addable_rel_fnames:
|
||||||
|
self.words.add(rel_fname)
|
||||||
|
|
||||||
|
for rel_fname in rel_fnames:
|
||||||
|
self.words.add(rel_fname)
|
||||||
|
|
||||||
|
fname = os.path.join(root, rel_fname)
|
||||||
|
with open(fname, "r") as f:
|
||||||
|
content = f.read()
|
||||||
|
try:
|
||||||
|
lexer = guess_lexer_for_filename(fname, content)
|
||||||
|
except ClassNotFound:
|
||||||
|
continue
|
||||||
|
tokens = list(lexer.get_tokens(content))
|
||||||
|
self.words.update(token[1] for token in tokens if token[0] in Token.Name)
|
||||||
|
|
||||||
def get_completions(self, document, complete_event):
|
def get_completions(self, document, complete_event):
|
||||||
text = document.text_before_cursor
|
text = document.text_before_cursor
|
||||||
|
@ -35,11 +60,17 @@ class FileContentCompleter(Completer):
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
candidates = self.words
|
candidates = self.words
|
||||||
|
candidates.update(set(self.fname_to_rel_fnames))
|
||||||
|
|
||||||
last_word = words[-1]
|
last_word = words[-1]
|
||||||
for word in candidates:
|
for word in candidates:
|
||||||
if word.lower().startswith(last_word.lower()):
|
if word.lower().startswith(last_word.lower()):
|
||||||
yield Completion(word, start_position=-len(last_word))
|
rel_fnames = self.fname_to_rel_fnames.get(word, [])
|
||||||
|
if rel_fnames:
|
||||||
|
for rel_fname in rel_fnames:
|
||||||
|
yield Completion(rel_fname, start_position=-len(last_word))
|
||||||
|
else:
|
||||||
|
yield Completion(word, start_position=-len(last_word))
|
||||||
|
|
||||||
|
|
||||||
class InputOutput:
|
class InputOutput:
|
||||||
|
@ -51,7 +82,18 @@ class InputOutput:
|
||||||
chat_history_file=None,
|
chat_history_file=None,
|
||||||
input=None,
|
input=None,
|
||||||
output=None,
|
output=None,
|
||||||
|
user_input_color="blue",
|
||||||
|
tool_output_color=None,
|
||||||
|
tool_error_color="red",
|
||||||
):
|
):
|
||||||
|
no_color = os.environ.get("NO_COLOR")
|
||||||
|
if no_color is not None and no_color != "":
|
||||||
|
pretty = False
|
||||||
|
|
||||||
|
self.user_input_color = user_input_color if pretty else None
|
||||||
|
self.tool_output_color = tool_output_color if pretty else None
|
||||||
|
self.tool_error_color = tool_error_color if pretty else None
|
||||||
|
|
||||||
self.input = input
|
self.input = input
|
||||||
self.output = output
|
self.output = output
|
||||||
self.pretty = pretty
|
self.pretty = pretty
|
||||||
|
@ -65,29 +107,20 @@ class InputOutput:
|
||||||
if pretty:
|
if pretty:
|
||||||
self.console = Console()
|
self.console = Console()
|
||||||
else:
|
else:
|
||||||
self.console = Console(force_terminal=True, no_color=True)
|
self.console = Console(no_color=True)
|
||||||
|
|
||||||
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
self.append_chat_history(f"\n# aider chat started at {current_time}\n\n")
|
self.append_chat_history(f"\n# aider chat started at {current_time}\n\n")
|
||||||
|
|
||||||
def get_input(self, fnames, commands):
|
def get_input(self, root, rel_fnames, addable_rel_fnames, commands):
|
||||||
if self.pretty:
|
if self.pretty:
|
||||||
self.console.rule()
|
style = dict(style=self.user_input_color) if self.user_input_color else dict()
|
||||||
|
self.console.rule(**style)
|
||||||
else:
|
else:
|
||||||
print()
|
print()
|
||||||
|
|
||||||
fnames = list(fnames)
|
rel_fnames = list(rel_fnames)
|
||||||
if len(fnames) > 1:
|
show = " ".join(rel_fnames)
|
||||||
common_prefix = os.path.commonpath(fnames)
|
|
||||||
if not common_prefix.endswith(os.path.sep):
|
|
||||||
common_prefix += os.path.sep
|
|
||||||
short_fnames = [fname.replace(common_prefix, "", 1) for fname in fnames]
|
|
||||||
elif len(fnames):
|
|
||||||
short_fnames = [os.path.basename(fnames[0])]
|
|
||||||
else:
|
|
||||||
short_fnames = []
|
|
||||||
|
|
||||||
show = " ".join(short_fnames)
|
|
||||||
if len(show) > 10:
|
if len(show) > 10:
|
||||||
show += "\n"
|
show += "\n"
|
||||||
show += "> "
|
show += "> "
|
||||||
|
@ -95,22 +128,29 @@ class InputOutput:
|
||||||
inp = ""
|
inp = ""
|
||||||
multiline_input = False
|
multiline_input = False
|
||||||
|
|
||||||
style = Style.from_dict({"": "green"})
|
if self.user_input_color:
|
||||||
|
style = Style.from_dict({"": self.user_input_color})
|
||||||
|
else:
|
||||||
|
style = None
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
completer_instance = FileContentCompleter(fnames, commands)
|
completer_instance = FileContentCompleter(
|
||||||
|
root, rel_fnames, addable_rel_fnames, commands
|
||||||
|
)
|
||||||
if multiline_input:
|
if multiline_input:
|
||||||
show = ". "
|
show = ". "
|
||||||
|
|
||||||
session_kwargs = {
|
session_kwargs = {
|
||||||
"message": show,
|
"message": show,
|
||||||
"completer": completer_instance,
|
"completer": completer_instance,
|
||||||
"style": style,
|
|
||||||
"reserve_space_for_menu": 4,
|
"reserve_space_for_menu": 4,
|
||||||
"complete_style": CompleteStyle.MULTI_COLUMN,
|
"complete_style": CompleteStyle.MULTI_COLUMN,
|
||||||
"input": self.input,
|
"input": self.input,
|
||||||
"output": self.output,
|
"output": self.output,
|
||||||
}
|
}
|
||||||
|
if style:
|
||||||
|
session_kwargs["style"] = style
|
||||||
|
|
||||||
if self.input_history_file is not None:
|
if self.input_history_file is not None:
|
||||||
session_kwargs["history"] = FileHistory(self.input_history_file)
|
session_kwargs["history"] = FileHistory(self.input_history_file)
|
||||||
|
|
||||||
|
@ -180,9 +220,10 @@ class InputOutput:
|
||||||
self.append_chat_history(hist, linebreak=True, blockquote=True)
|
self.append_chat_history(hist, linebreak=True, blockquote=True)
|
||||||
|
|
||||||
message = Text(message)
|
message = Text(message)
|
||||||
self.console.print(message, style="red")
|
style = dict(style=self.tool_error_color) if self.tool_error_color else dict()
|
||||||
|
self.console.print(message, **style)
|
||||||
|
|
||||||
def tool(self, *messages, log_only=False):
|
def tool_output(self, *messages, log_only=False):
|
||||||
if messages:
|
if messages:
|
||||||
hist = " ".join(messages)
|
hist = " ".join(messages)
|
||||||
hist = f"{hist.strip()}"
|
hist = f"{hist.strip()}"
|
||||||
|
@ -190,7 +231,8 @@ class InputOutput:
|
||||||
|
|
||||||
if not log_only:
|
if not log_only:
|
||||||
messages = list(map(Text, messages))
|
messages = list(map(Text, messages))
|
||||||
self.console.print(*messages)
|
style = dict(style=self.tool_output_color) if self.tool_output_color else dict()
|
||||||
|
self.console.print(*messages, **style)
|
||||||
|
|
||||||
def append_chat_history(self, text, linebreak=False, blockquote=False):
|
def append_chat_history(self, text, linebreak=False, blockquote=False):
|
||||||
if blockquote:
|
if blockquote:
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import git
|
|
||||||
import configargparse
|
import configargparse
|
||||||
from dotenv import load_dotenv
|
import git
|
||||||
|
|
||||||
from aider.coder import Coder
|
from aider.coder import Coder
|
||||||
from aider.io import InputOutput
|
from aider.io import InputOutput
|
||||||
|
|
||||||
|
@ -19,13 +20,11 @@ def main(args=None, input=None, output=None):
|
||||||
if args is None:
|
if args is None:
|
||||||
args = sys.argv[1:]
|
args = sys.argv[1:]
|
||||||
|
|
||||||
load_dotenv()
|
git_root = get_git_root()
|
||||||
env_prefix = "AIDER_"
|
|
||||||
|
|
||||||
default_config_files = [
|
default_config_files = [
|
||||||
os.path.expanduser("~/.aider.conf.yml"),
|
os.path.expanduser("~/.aider.conf.yml"),
|
||||||
]
|
]
|
||||||
git_root = get_git_root()
|
|
||||||
if git_root:
|
if git_root:
|
||||||
default_config_files.insert(0, os.path.join(git_root, ".aider.conf.yml"))
|
default_config_files.insert(0, os.path.join(git_root, ".aider.conf.yml"))
|
||||||
|
|
||||||
|
@ -34,6 +33,7 @@ def main(args=None, input=None, output=None):
|
||||||
add_config_file_help=True,
|
add_config_file_help=True,
|
||||||
default_config_files=default_config_files,
|
default_config_files=default_config_files,
|
||||||
config_file_parser_class=configargparse.YAMLConfigFileParser,
|
config_file_parser_class=configargparse.YAMLConfigFileParser,
|
||||||
|
auto_env_var_prefix="AIDER_",
|
||||||
)
|
)
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
|
@ -63,21 +63,18 @@ def main(args=None, input=None, output=None):
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--input-history-file",
|
"--input-history-file",
|
||||||
metavar="INPUT_HISTORY_FILE",
|
metavar="INPUT_HISTORY_FILE",
|
||||||
env_var=f"{env_prefix}INPUT_HISTORY_FILE",
|
|
||||||
default=default_input_history_file,
|
default=default_input_history_file,
|
||||||
help=f"Specify the chat input history file (default: {default_input_history_file})",
|
help=f"Specify the chat input history file (default: {default_input_history_file})",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--chat-history-file",
|
"--chat-history-file",
|
||||||
metavar="CHAT_HISTORY_FILE",
|
metavar="CHAT_HISTORY_FILE",
|
||||||
env_var=f"{env_prefix}CHAT_HISTORY_FILE",
|
|
||||||
default=default_chat_history_file,
|
default=default_chat_history_file,
|
||||||
help=f"Specify the chat history file (default: {default_chat_history_file})",
|
help=f"Specify the chat history file (default: {default_chat_history_file})",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--model",
|
"--model",
|
||||||
metavar="MODEL",
|
metavar="MODEL",
|
||||||
env_var=f"{env_prefix}MODEL",
|
|
||||||
default="gpt-4",
|
default="gpt-4",
|
||||||
help="Specify the model to use for the main chat (default: gpt-4)",
|
help="Specify the model to use for the main chat (default: gpt-4)",
|
||||||
)
|
)
|
||||||
|
@ -91,17 +88,30 @@ def main(args=None, input=None, output=None):
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--pretty",
|
"--pretty",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
env_var=f"{env_prefix}PRETTY",
|
|
||||||
default=True,
|
default=True,
|
||||||
help="Enable pretty, colorized output (default: True)",
|
help="Enable pretty, colorized output (default: True)",
|
||||||
)
|
)
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--no-pretty",
|
"--no-pretty",
|
||||||
action="store_false",
|
action="store_false",
|
||||||
dest="pretty",
|
dest="pretty",
|
||||||
help="Disable pretty, colorized output",
|
help="Disable pretty, colorized output",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--user-input-color",
|
||||||
|
default="green",
|
||||||
|
help="Set the color for user input (default: green)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--tool-output-color",
|
||||||
|
default=None,
|
||||||
|
help="Set the color for tool output (default: None)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--tool-error-color",
|
||||||
|
default="red",
|
||||||
|
help="Set the color for tool error messages (default: red)",
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--apply",
|
"--apply",
|
||||||
metavar="FILE",
|
metavar="FILE",
|
||||||
|
@ -110,7 +120,7 @@ def main(args=None, input=None, output=None):
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--auto-commits",
|
"--auto-commits",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
env_var=f"{env_prefix}AUTO_COMMIT",
|
dest="auto_commits",
|
||||||
default=True,
|
default=True,
|
||||||
help="Enable auto commit of changes (default: True)",
|
help="Enable auto commit of changes (default: True)",
|
||||||
)
|
)
|
||||||
|
@ -118,9 +128,28 @@ def main(args=None, input=None, output=None):
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--no-auto-commits",
|
"--no-auto-commits",
|
||||||
action="store_false",
|
action="store_false",
|
||||||
dest="auto_commit",
|
dest="auto_commits",
|
||||||
help="Disable auto commit of changes",
|
help="Disable auto commit of changes",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--dirty-commits",
|
||||||
|
action="store_true",
|
||||||
|
dest="dirty_commits",
|
||||||
|
help="Enable dirty commit of changes",
|
||||||
|
default=True,
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--no-dirty-commits",
|
||||||
|
action="store_false",
|
||||||
|
dest="dirty_commits",
|
||||||
|
help="Disable dirty commit of changes",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--openai-api-key",
|
||||||
|
metavar="OPENAI_API_KEY",
|
||||||
|
help="Specify the OpenAI API key",
|
||||||
|
env_var="OPENAI_API_KEY",
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--dry-run",
|
"--dry-run",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
|
@ -130,7 +159,6 @@ def main(args=None, input=None, output=None):
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--show-diffs",
|
"--show-diffs",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
env_var=f"{env_prefix}SHOW_DIFFS",
|
|
||||||
help="Show diffs when committing changes (default: False)",
|
help="Show diffs when committing changes (default: False)",
|
||||||
default=False,
|
default=False,
|
||||||
)
|
)
|
||||||
|
@ -140,7 +168,6 @@ def main(args=None, input=None, output=None):
|
||||||
nargs="?",
|
nargs="?",
|
||||||
const=True,
|
const=True,
|
||||||
default=None,
|
default=None,
|
||||||
env_var=f"{env_prefix}CTAGS",
|
|
||||||
help=(
|
help=(
|
||||||
"Add ctags to the chat to help GPT understand the codebase (default: check for ctags"
|
"Add ctags to the chat to help GPT understand the codebase (default: check for ctags"
|
||||||
" executable)"
|
" executable)"
|
||||||
|
@ -168,9 +195,23 @@ def main(args=None, input=None, output=None):
|
||||||
args.chat_history_file,
|
args.chat_history_file,
|
||||||
input=input,
|
input=input,
|
||||||
output=output,
|
output=output,
|
||||||
|
user_input_color=args.user_input_color,
|
||||||
|
tool_output_color=args.tool_output_color,
|
||||||
|
tool_error_color=args.tool_error_color,
|
||||||
)
|
)
|
||||||
|
|
||||||
io.tool(*sys.argv, log_only=True)
|
if args.verbose:
|
||||||
|
show = parser.format_values()
|
||||||
|
io.tool_output(show)
|
||||||
|
io.tool_output("Option settings:")
|
||||||
|
for arg, val in sorted(vars(args).items()):
|
||||||
|
io.tool_output(f" - {arg}: {val}")
|
||||||
|
|
||||||
|
io.tool_output(*sys.argv, log_only=True)
|
||||||
|
|
||||||
|
if not args.openai_api_key:
|
||||||
|
io.tool_error("No OpenAI API key provided. Use --openai-api-key or env OPENAI_API_KEY.")
|
||||||
|
return 1
|
||||||
|
|
||||||
coder = Coder(
|
coder = Coder(
|
||||||
io,
|
io,
|
||||||
|
@ -179,12 +220,15 @@ def main(args=None, input=None, output=None):
|
||||||
pretty=args.pretty,
|
pretty=args.pretty,
|
||||||
show_diffs=args.show_diffs,
|
show_diffs=args.show_diffs,
|
||||||
auto_commits=args.auto_commits,
|
auto_commits=args.auto_commits,
|
||||||
|
dirty_commits=args.dirty_commits,
|
||||||
dry_run=args.dry_run,
|
dry_run=args.dry_run,
|
||||||
use_ctags=args.ctags,
|
use_ctags=args.ctags,
|
||||||
verbose=args.verbose,
|
verbose=args.verbose,
|
||||||
|
openai_api_key=args.openai_api_key,
|
||||||
)
|
)
|
||||||
if args.auto_commits:
|
|
||||||
coder.commit(ask=True, prefix="wip: ", which="repo_files")
|
if args.dirty_commits:
|
||||||
|
coder.commit(ask=True, which="repo_files")
|
||||||
|
|
||||||
if args.apply:
|
if args.apply:
|
||||||
with open(args.apply, "r") as f:
|
with open(args.apply, "r") as f:
|
||||||
|
@ -192,6 +236,7 @@ def main(args=None, input=None, output=None):
|
||||||
coder.update_files(content, inp="")
|
coder.update_files(content, inp="")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
io.tool_output("Use /help to see in-chat commands.")
|
||||||
coder.run()
|
coder.run()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ Take requests for changes to the supplied code.
|
||||||
If the request is ambiguous, ask questions.
|
If the request is ambiguous, ask questions.
|
||||||
|
|
||||||
Once you understand the request you MUST:
|
Once you understand the request you MUST:
|
||||||
1. List the files you need to modify. If they are *read-only* ask the user to make them *read-write* using the file's full path name.
|
1. List the files you need to modify. Do not suggest changes to *read-only* files. You *MUST* ask the user to make them *read-write* using the file's full path name. End your reply and wait for their approval.
|
||||||
2. Think step-by-step and explain the needed changes.
|
2. Think step-by-step and explain the needed changes.
|
||||||
3. Describe each change with an *edit block* per the example below.
|
3. Describe each change with an *edit block* per the example below.
|
||||||
"""
|
"""
|
||||||
|
@ -74,7 +74,7 @@ Reply with JUST the commit message, without quotes, comments, questions, etc!
|
||||||
# COMMANDS
|
# COMMANDS
|
||||||
undo_command_reply = "I did `git reset --hard HEAD~1` to discard the last edits."
|
undo_command_reply = "I did `git reset --hard HEAD~1` to discard the last edits."
|
||||||
|
|
||||||
added_files = "I added the content of these additional files: {fnames}"
|
added_files = "I added these *read-write* files: {fnames}"
|
||||||
|
|
||||||
|
|
||||||
run_output = """I ran this command:
|
run_output = """I ran this command:
|
||||||
|
|
|
@ -1,15 +1,14 @@
|
||||||
import os
|
|
||||||
import json
|
import json
|
||||||
import sys
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import tiktoken
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
from collections import defaultdict, Counter
|
from collections import Counter, defaultdict
|
||||||
|
|
||||||
import networkx as nx
|
import networkx as nx
|
||||||
|
import tiktoken
|
||||||
|
|
||||||
from aider import prompts, utils
|
from aider import prompts, utils
|
||||||
from aider.dump import dump
|
|
||||||
|
|
||||||
|
|
||||||
# Global cache for tags
|
# Global cache for tags
|
||||||
TAGS_CACHE = {}
|
TAGS_CACHE = {}
|
||||||
|
@ -203,6 +202,7 @@ def find_py_files(directory):
|
||||||
|
|
||||||
def call_map():
|
def call_map():
|
||||||
import random
|
import random
|
||||||
|
|
||||||
import graphviz
|
import graphviz
|
||||||
|
|
||||||
fnames = sys.argv[1:]
|
fnames = sys.argv[1:]
|
||||||
|
@ -322,3 +322,4 @@ def call_map():
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
call_map()
|
call_map()
|
||||||
|
# print(rm.get_tags_map(sys.argv[1:]))
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import re
|
|
||||||
import math
|
import math
|
||||||
|
import re
|
||||||
from difflib import SequenceMatcher
|
from difflib import SequenceMatcher
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from pygments.util import ClassNotFound
|
|
||||||
from pygments.lexers import guess_lexer_for_filename
|
from pygments.lexers import guess_lexer_for_filename
|
||||||
from pygments.token import Token
|
from pygments.token import Token
|
||||||
|
from pygments.util import ClassNotFound
|
||||||
|
|
||||||
# from aider.dump import dump
|
# from aider.dump import dump
|
||||||
|
|
||||||
|
@ -234,10 +234,15 @@ def find_original_update_blocks(content):
|
||||||
processed.append(cur) # original_marker
|
processed.append(cur) # original_marker
|
||||||
|
|
||||||
filename = processed[-2].splitlines()[-1].strip()
|
filename = processed[-2].splitlines()[-1].strip()
|
||||||
if not len(filename) or "`" in filename:
|
try:
|
||||||
filename = processed[-2].splitlines()[-2].strip()
|
|
||||||
if not len(filename) or "`" in filename:
|
if not len(filename) or "`" in filename:
|
||||||
raise ValueError(f"Bad/missing filename. It should go right above {ORIGINAL}")
|
filename = processed[-2].splitlines()[-2].strip()
|
||||||
|
if not len(filename) or "`" in filename:
|
||||||
|
raise ValueError(
|
||||||
|
f"Bad/missing filename. It should go right above {ORIGINAL}"
|
||||||
|
)
|
||||||
|
except IndexError:
|
||||||
|
raise ValueError(f"Bad/missing filename. It should go right above {ORIGINAL}")
|
||||||
|
|
||||||
original_text = pieces.pop()
|
original_text = pieces.pop()
|
||||||
processed.append(original_text)
|
processed.append(original_text)
|
||||||
|
@ -248,8 +253,10 @@ def find_original_update_blocks(content):
|
||||||
raise ValueError(f"Expected {DIVIDER}")
|
raise ValueError(f"Expected {DIVIDER}")
|
||||||
|
|
||||||
updated_text = pieces.pop()
|
updated_text = pieces.pop()
|
||||||
|
processed.append(updated_text)
|
||||||
|
|
||||||
updated_marker = pieces.pop()
|
updated_marker = pieces.pop()
|
||||||
|
processed.append(updated_marker)
|
||||||
if updated_marker.strip() != UPDATED:
|
if updated_marker.strip() != UPDATED:
|
||||||
raise ValueError(f"Expected {UPDATED}")
|
raise ValueError(f"Expected {UPDATED}")
|
||||||
|
|
||||||
|
|
BIN
assets/screenshot.gif
Normal file
BIN
assets/screenshot.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
@ -1,5 +1,5 @@
|
||||||
|
|
||||||
# Improving GPT-4's codebase understanding with a map
|
# Improving GPT-4's codebase understanding with ctags
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,8 @@ In the chats, you'll see a varity of coding tasks like generating new code, edit
|
||||||
|
|
||||||
* [**Create a Black Box Test Case**](https://aider.chat/examples/add-test.html): GPT creates a "black box" test case without access to the source of the method being tested, using only a [high level map of the repository based on ctags](https://aider.chat/docs/ctags.html).
|
* [**Create a Black Box Test Case**](https://aider.chat/examples/add-test.html): GPT creates a "black box" test case without access to the source of the method being tested, using only a [high level map of the repository based on ctags](https://aider.chat/docs/ctags.html).
|
||||||
|
|
||||||
|
* [**Honor the NO_COLOR env var**](https://aider.chat/examples/no-color.html): The user pastes the NO_COLOR spec from no-color.org into the chat, and GPT-4 modifies the application to conform.
|
||||||
|
|
||||||
* [**Download, analyze and plot US Census data**](https://aider.chat/examples/census.html): GPT-4 downloads census data, suggests some hypotheses to test, tests one and then summarizes and plots a graph of the results.
|
* [**Download, analyze and plot US Census data**](https://aider.chat/examples/census.html): GPT-4 downloads census data, suggests some hypotheses to test, tests one and then summarizes and plots a graph of the results.
|
||||||
|
|
||||||
* [**Semantic Search & Replace**](semantic-search-replace.md): Updating a collection of function calls, which requires dealing with various formatting and semantic differences in the various function call sites.
|
* [**Semantic Search & Replace**](semantic-search-replace.md): Updating a collection of function calls, which requires dealing with various formatting and semantic differences in the various function call sites.
|
||||||
|
|
156
examples/no-color.md
Normal file
156
examples/no-color.md
Normal file
|
@ -0,0 +1,156 @@
|
||||||
|
|
||||||
|
# Honor the NO_COLOR environment variable
|
||||||
|
|
||||||
|
The user pastes the specification of the NO_COLOR environment variable protocol
|
||||||
|
from [no-color.org](https://no-color.org) into the chat with GPT-4.
|
||||||
|
|
||||||
|
GPT figures out which files need to be edited, and asks permission to make changes.
|
||||||
|
It then disables all the color and pretty-printing functions if NO_COLOR is
|
||||||
|
set, so the tool conforms to the specification.
|
||||||
|
|
||||||
|
The user then asks for a test case, which GPT adds as well.
|
||||||
|
|
||||||
|
## Chat transcript
|
||||||
|
> $ aider
|
||||||
|
> Using git repo: .git
|
||||||
|
> Use /help to see in-chat commands.
|
||||||
|
|
||||||
|
#### Command-line software which adds ANSI color to its output by default should check for a NO_COLOR environment variable that, when present and not an empty string (regardless of its value), prevents the addition of ANSI color.
|
||||||
|
|
||||||
|
To implement this feature, we need to modify the `aider/io.py` file. Please make it *read-write* so I can provide the necessary changes.
|
||||||
|
|
||||||
|
> aider/io.py
|
||||||
|
> Add these files to the chat? y
|
||||||
|
|
||||||
|
Great! Now, I will make the necessary changes to check for the `NO_COLOR` environment variable and disable ANSI color when it is present and not an empty string.
|
||||||
|
|
||||||
|
```python
|
||||||
|
aider/io.py
|
||||||
|
<<<<<<< ORIGINAL
|
||||||
|
import os
|
||||||
|
=======
|
||||||
|
import os
|
||||||
|
import os.path
|
||||||
|
>>>>>>> UPDATED
|
||||||
|
```
|
||||||
|
|
||||||
|
```python
|
||||||
|
aider/io.py
|
||||||
|
<<<<<<< ORIGINAL
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
pretty=True,
|
||||||
|
yes=False,
|
||||||
|
input_history_file=None,
|
||||||
|
chat_history_file=None,
|
||||||
|
input=None,
|
||||||
|
output=None,
|
||||||
|
user_input_color="blue",
|
||||||
|
tool_output_color=None,
|
||||||
|
tool_error_color="red",
|
||||||
|
):
|
||||||
|
self.user_input_color = user_input_color
|
||||||
|
self.tool_output_color = tool_output_color
|
||||||
|
self.tool_error_color = tool_error_color
|
||||||
|
self.input = input
|
||||||
|
self.output = output
|
||||||
|
self.pretty = pretty
|
||||||
|
self.yes = yes
|
||||||
|
self.input_history_file = input_history_file
|
||||||
|
if chat_history_file is not None:
|
||||||
|
self.chat_history_file = Path(chat_history_file)
|
||||||
|
else:
|
||||||
|
self.chat_history_file = None
|
||||||
|
|
||||||
|
if pretty:
|
||||||
|
self.console = Console()
|
||||||
|
else:
|
||||||
|
self.console = Console(force_terminal=True, no_color=True)
|
||||||
|
=======
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
pretty=True,
|
||||||
|
yes=False,
|
||||||
|
input_history_file=None,
|
||||||
|
chat_history_file=None,
|
||||||
|
input=None,
|
||||||
|
output=None,
|
||||||
|
user_input_color="blue",
|
||||||
|
tool_output_color=None,
|
||||||
|
tool_error_color="red",
|
||||||
|
):
|
||||||
|
no_color = os.environ.get("NO_COLOR")
|
||||||
|
if no_color is not None and no_color != "":
|
||||||
|
pretty = False
|
||||||
|
|
||||||
|
self.user_input_color = user_input_color if pretty else None
|
||||||
|
self.tool_output_color = tool_output_color if pretty else None
|
||||||
|
self.tool_error_color = tool_error_color if pretty else None
|
||||||
|
self.input = input
|
||||||
|
self.output = output
|
||||||
|
self.pretty = pretty
|
||||||
|
self.yes = yes
|
||||||
|
self.input_history_file = input_history_file
|
||||||
|
if chat_history_file is not None:
|
||||||
|
self.chat_history_file = Path(chat_history_file)
|
||||||
|
else:
|
||||||
|
self.chat_history_file = None
|
||||||
|
|
||||||
|
self.console = Console(force_terminal=not pretty, no_color=not pretty)
|
||||||
|
>>>>>>> UPDATED
|
||||||
|
```
|
||||||
|
|
||||||
|
These changes will make the software check for the `NO_COLOR` environment variable and disable ANSI color when it is present and not an empty string.
|
||||||
|
|
||||||
|
> Applied edit to aider/io.py
|
||||||
|
> Applied edit to aider/io.py
|
||||||
|
> Commit 3218242 aider: Added check for NO_COLOR environment variable to disable ANSI color.
|
||||||
|
|
||||||
|
#### add a test
|
||||||
|
|
||||||
|
To add a test for the `NO_COLOR` environment variable, we will create a new test function in the `tests/test_io.py` file. Since this file does not exist, we will create it first.
|
||||||
|
|
||||||
|
```python
|
||||||
|
tests/test_io.py
|
||||||
|
<<<<<<< ORIGINAL
|
||||||
|
=======
|
||||||
|
import os
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
from aider.io import InputOutput
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
|
>>>>>>> UPDATED
|
||||||
|
```
|
||||||
|
|
||||||
|
This test function checks if the `pretty` attribute of the `InputOutput` class is set to `False` when the `NO_COLOR` environment variable is set.
|
||||||
|
|
||||||
|
> Allow creation of new file tests/test_io.py? y
|
||||||
|
> Add tests/test_io.py to git? y
|
||||||
|
> Applied edit to tests/test_io.py
|
||||||
|
> Commit 52a9df5 aider: Added a test function to check if the `pretty` attribute of the `InputOutput` class is set to `False` when the `NO_COLOR` environment variable is set.
|
||||||
|
|
||||||
|
#### /run pytest
|
||||||
|
|
||||||
|
> ============================= test session starts ==============================
|
||||||
|
> platform darwin -- Python 3.11.3, pytest-7.3.1, pluggy-1.0.0
|
||||||
|
> rootdir: /Users/gauthier/Projects/aider
|
||||||
|
> collected 25 items
|
||||||
|
>
|
||||||
|
> tests/test_coder.py ...... [ 24%]
|
||||||
|
> tests/test_commands.py . [ 28%]
|
||||||
|
> tests/test_io.py . [ 32%]
|
||||||
|
> tests/test_main.py .... [ 48%]
|
||||||
|
> tests/test_repomap.py ... [ 60%]
|
||||||
|
> tests/test_utils.py .......... [100%]
|
||||||
|
>
|
||||||
|
> ============================== 25 passed in 1.41s ==============================
|
||||||
|
>
|
||||||
|
> Add the output to the chat? y
|
|
@ -14,7 +14,6 @@ multidict==6.0.4
|
||||||
openai==0.27.6
|
openai==0.27.6
|
||||||
prompt-toolkit==3.0.38
|
prompt-toolkit==3.0.38
|
||||||
Pygments==2.15.1
|
Pygments==2.15.1
|
||||||
python-dotenv==1.0.0
|
|
||||||
requests==2.30.0
|
requests==2.30.0
|
||||||
rich==13.3.5
|
rich==13.3.5
|
||||||
smmap==5.0.0
|
smmap==5.0.0
|
||||||
|
|
BIN
screenshot.gif
BIN
screenshot.gif
Binary file not shown.
Before Width: | Height: | Size: 66 KiB |
2
setup.py
2
setup.py
|
@ -1,4 +1,4 @@
|
||||||
from setuptools import setup, find_packages
|
from setuptools import find_packages, setup
|
||||||
|
|
||||||
with open("requirements.txt") as f:
|
with open("requirements.txt") as f:
|
||||||
requirements = f.read().splitlines()
|
requirements = f.read().splitlines()
|
||||||
|
|
114
tests/test_coder.py
Normal file
114
tests/test_coder.py
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
import os
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
from aider.coder import Coder
|
||||||
|
|
||||||
|
|
||||||
|
class TestCoder(unittest.TestCase):
|
||||||
|
def test_check_for_file_mentions(self):
|
||||||
|
# Mock the IO object
|
||||||
|
mock_io = MagicMock()
|
||||||
|
|
||||||
|
# Initialize the Coder object with the mocked IO and mocked repo
|
||||||
|
coder = Coder(io=mock_io, openai_api_key="fake_key")
|
||||||
|
|
||||||
|
# Mock the git repo
|
||||||
|
mock_repo = MagicMock()
|
||||||
|
mock_repo.git.ls_files.return_value = "file1.txt\nfile2.py"
|
||||||
|
coder.repo = mock_repo
|
||||||
|
|
||||||
|
# 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 = {os.path.abspath("file1.txt"), os.path.abspath("file2.py")}
|
||||||
|
self.assertEqual(coder.abs_fnames, expected_files)
|
||||||
|
|
||||||
|
def test_check_for_filename_mentions_of_longer_paths(self):
|
||||||
|
# Mock the IO object
|
||||||
|
mock_io = MagicMock()
|
||||||
|
|
||||||
|
# Initialize the Coder object with the mocked IO and mocked repo
|
||||||
|
coder = Coder(io=mock_io, openai_api_key="fake_key")
|
||||||
|
|
||||||
|
# Mock the git repo
|
||||||
|
mock_repo = MagicMock()
|
||||||
|
mock_repo.git.ls_files.return_value = "./file1.txt\n./file2.py"
|
||||||
|
coder.repo = mock_repo
|
||||||
|
|
||||||
|
# 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 = {os.path.abspath("file1.txt"), os.path.abspath("file2.py")}
|
||||||
|
self.assertEqual(coder.abs_fnames, expected_files)
|
||||||
|
|
||||||
|
def test_check_for_ambiguous_filename_mentions_of_longer_paths(self):
|
||||||
|
# Mock the IO object
|
||||||
|
mock_io = MagicMock()
|
||||||
|
|
||||||
|
# Initialize the Coder object with the mocked IO and mocked repo
|
||||||
|
coder = Coder(io=mock_io, openai_api_key="fake_key")
|
||||||
|
|
||||||
|
# Mock the git repo
|
||||||
|
mock_repo = MagicMock()
|
||||||
|
mock_repo.git.ls_files.return_value = "./file1.txt\n./other/file1.txt"
|
||||||
|
coder.repo = mock_repo
|
||||||
|
|
||||||
|
# Call the check_for_file_mentions method
|
||||||
|
coder.check_for_file_mentions("Please check file1.txt!")
|
||||||
|
|
||||||
|
self.assertEqual(coder.abs_fnames, set())
|
||||||
|
|
||||||
|
def test_get_commit_message(self):
|
||||||
|
# Mock the IO object
|
||||||
|
mock_io = MagicMock()
|
||||||
|
|
||||||
|
# Initialize the Coder object with the mocked IO and mocked repo
|
||||||
|
coder = Coder(io=mock_io, openai_api_key="fake_key")
|
||||||
|
|
||||||
|
# Mock the send method to return a tuple with a message and False
|
||||||
|
coder.send = MagicMock(return_value=("a good commit message", False))
|
||||||
|
|
||||||
|
# Call the get_commit_message method with dummy diff and context
|
||||||
|
result = coder.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_commit_message_strip_quotes(self):
|
||||||
|
# Mock the IO object
|
||||||
|
mock_io = MagicMock()
|
||||||
|
|
||||||
|
# Initialize the Coder object with the mocked IO and mocked repo
|
||||||
|
coder = Coder(io=mock_io, openai_api_key="fake_key")
|
||||||
|
|
||||||
|
# Mock the send method to return a tuple with a message and False
|
||||||
|
coder.send = MagicMock(return_value=('"a good commit message"', False))
|
||||||
|
|
||||||
|
# Call the get_commit_message method with dummy diff and context
|
||||||
|
result = coder.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_commit_message_no_strip_unmatched_quotes(self):
|
||||||
|
# Mock the IO object
|
||||||
|
mock_io = MagicMock()
|
||||||
|
|
||||||
|
# Initialize the Coder object with the mocked IO and mocked repo
|
||||||
|
coder = Coder(io=mock_io, openai_api_key="fake_key")
|
||||||
|
|
||||||
|
# Mock the send method to return a tuple with a message and False
|
||||||
|
coder.send = MagicMock(return_value=('a good "commit message"', False))
|
||||||
|
|
||||||
|
# Call the get_commit_message method with dummy diff and context
|
||||||
|
result = coder.get_commit_message("dummy diff", "dummy context")
|
||||||
|
|
||||||
|
# Assert that the returned message is the expected one
|
||||||
|
self.assertEqual(result, 'a good "commit message"')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
|
@ -1,27 +1,33 @@
|
||||||
import os
|
import os
|
||||||
|
import shutil
|
||||||
import tempfile
|
import tempfile
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
from unittest.mock import MagicMock
|
|
||||||
from aider.commands import Commands
|
from aider.commands import Commands
|
||||||
from aider.io import InputOutput
|
from aider.io import InputOutput
|
||||||
|
|
||||||
|
|
||||||
class TestCommands(TestCase):
|
class TestCommands(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
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 test_cmd_add(self):
|
def test_cmd_add(self):
|
||||||
# Create a temporary directory and change the current working directory
|
# Initialize the Commands and InputOutput objects
|
||||||
with tempfile.TemporaryDirectory() as tmpdir:
|
io = InputOutput(pretty=False, yes=True)
|
||||||
os.chdir(tmpdir)
|
from aider.coder import Coder
|
||||||
|
|
||||||
# Initialize the Commands and InputOutput objects
|
coder = Coder(io, openai_api_key="deadbeef")
|
||||||
io = InputOutput(pretty=False, yes=True)
|
commands = Commands(io, coder)
|
||||||
from aider.coder import Coder
|
|
||||||
|
|
||||||
coder = Coder(io)
|
# Call the cmd_add method with 'foo.txt' and 'bar.txt' as a single string
|
||||||
commands = Commands(io, coder)
|
commands.cmd_add("foo.txt bar.txt")
|
||||||
|
|
||||||
# Call the cmd_add method with 'foo.txt' and 'bar.txt' as a single string
|
# Check if both files have been created in the temporary directory
|
||||||
commands.cmd_add("foo.txt bar.txt")
|
self.assertTrue(os.path.exists("foo.txt"))
|
||||||
|
self.assertTrue(os.path.exists("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"))
|
|
||||||
|
|
16
tests/test_io.py
Normal file
16
tests/test_io.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import os
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from aider.io import InputOutput
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
|
@ -1,34 +1,82 @@
|
||||||
import os
|
import os
|
||||||
import tempfile
|
import shutil
|
||||||
from unittest import TestCase
|
|
||||||
from aider.main import main
|
|
||||||
import subprocess
|
import subprocess
|
||||||
from prompt_toolkit.input import create_input
|
import tempfile
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
|
from unittest import TestCase
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from prompt_toolkit.input import create_input
|
||||||
from prompt_toolkit.output import DummyOutput
|
from prompt_toolkit.output import DummyOutput
|
||||||
|
|
||||||
|
from aider.main import main
|
||||||
|
|
||||||
|
|
||||||
class TestMain(TestCase):
|
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)
|
||||||
|
|
||||||
def test_main_with_empty_dir_no_files_on_command(self):
|
def test_main_with_empty_dir_no_files_on_command(self):
|
||||||
with tempfile.TemporaryDirectory() as temp_dir:
|
pipe_input = create_input(StringIO(""))
|
||||||
os.chdir(temp_dir)
|
main([], input=pipe_input, output=DummyOutput())
|
||||||
pipe_input = create_input(StringIO(""))
|
pipe_input.close()
|
||||||
main([], input=pipe_input, output=DummyOutput())
|
|
||||||
pipe_input.close()
|
|
||||||
|
|
||||||
def test_main_with_empty_dir_new_file(self):
|
def test_main_with_empty_dir_new_file(self):
|
||||||
with tempfile.TemporaryDirectory() as temp_dir:
|
pipe_input = create_input(StringIO(""))
|
||||||
os.chdir(temp_dir)
|
main(["foo.txt"], input=pipe_input, output=DummyOutput())
|
||||||
pipe_input = create_input(StringIO(""))
|
pipe_input.close()
|
||||||
main(["foo.txt"], input=pipe_input, output=DummyOutput())
|
self.assertTrue(os.path.exists("foo.txt"))
|
||||||
pipe_input.close()
|
|
||||||
self.assertTrue(os.path.exists("foo.txt"))
|
|
||||||
|
|
||||||
def test_main_with_empty_git_dir_new_file(self):
|
def test_main_with_empty_git_dir_new_file(self):
|
||||||
with tempfile.TemporaryDirectory() as temp_dir:
|
subprocess.run(["git", "init"])
|
||||||
os.chdir(temp_dir)
|
subprocess.run(["git", "config", "user.email", "dummy@example.com"])
|
||||||
subprocess.run(["git", "init"], cwd=temp_dir)
|
subprocess.run(["git", "config", "user.name", "Dummy User"])
|
||||||
pipe_input = create_input(StringIO(""))
|
pipe_input = create_input(StringIO(""))
|
||||||
main(["--yes", "foo.txt"], input=pipe_input, output=DummyOutput())
|
main(["--yes", "foo.txt"], input=pipe_input, output=DummyOutput())
|
||||||
pipe_input.close()
|
pipe_input.close()
|
||||||
self.assertTrue(os.path.exists("foo.txt"))
|
self.assertTrue(os.path.exists("foo.txt"))
|
||||||
|
|
||||||
|
def test_main_args(self):
|
||||||
|
with patch("aider.main.Coder") as MockCoder:
|
||||||
|
main(["--no-auto-commits"])
|
||||||
|
_, kwargs = MockCoder.call_args
|
||||||
|
assert kwargs["auto_commits"] is False
|
||||||
|
|
||||||
|
with patch("aider.main.Coder") as MockCoder:
|
||||||
|
main(["--auto-commits"])
|
||||||
|
_, kwargs = MockCoder.call_args
|
||||||
|
assert kwargs["auto_commits"] is True
|
||||||
|
|
||||||
|
with patch("aider.main.Coder") as MockCoder:
|
||||||
|
main([])
|
||||||
|
_, 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") as MockCoder:
|
||||||
|
main(["--no-pretty"])
|
||||||
|
_, kwargs = MockCoder.call_args
|
||||||
|
assert kwargs["pretty"] is False
|
||||||
|
|
||||||
|
with patch("aider.main.Coder") as MockCoder:
|
||||||
|
main(["--pretty"])
|
||||||
|
_, kwargs = MockCoder.call_args
|
||||||
|
assert kwargs["pretty"] is True
|
||||||
|
|
||||||
|
with patch("aider.main.Coder") as MockCoder:
|
||||||
|
main(["--no-dirty-commits"])
|
||||||
|
_, kwargs = MockCoder.call_args
|
||||||
|
assert kwargs["dirty_commits"] is False
|
||||||
|
|
||||||
|
with patch("aider.main.Coder") as MockCoder:
|
||||||
|
main(["--dirty-commits"])
|
||||||
|
_, kwargs = MockCoder.call_args
|
||||||
|
assert kwargs["dirty_commits"] is True
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
import os
|
import os
|
||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
|
from subprocess import CompletedProcess
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
from aider.repomap import RepoMap
|
from aider.repomap import RepoMap
|
||||||
|
|
||||||
|
|
||||||
|
@ -55,6 +58,27 @@ def my_function(arg1, arg2):
|
||||||
self.assertIn("my_method", result)
|
self.assertIn("my_method", result)
|
||||||
self.assertIn("my_function", result)
|
self.assertIn("my_function", result)
|
||||||
|
|
||||||
|
def test_check_for_ctags_failure(self):
|
||||||
|
with patch("subprocess.run") as mock_run:
|
||||||
|
mock_run.side_effect = Exception("ctags not found")
|
||||||
|
repo_map = RepoMap(use_ctags=True)
|
||||||
|
result = repo_map.check_for_ctags()
|
||||||
|
self.assertFalse(result)
|
||||||
|
|
||||||
|
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=(
|
||||||
|
b'{"_type": "tag", "name": "status", "path": "aider/main.py", "pattern": "/^ '
|
||||||
|
b' status = main()$/", "kind": "variable"}'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
repo_map = RepoMap(use_ctags=True)
|
||||||
|
result = repo_map.check_for_ctags()
|
||||||
|
self.assertTrue(result)
|
||||||
|
|
||||||
def test_get_tags_map_without_ctags(self):
|
def test_get_tags_map_without_ctags(self):
|
||||||
# Create a temporary directory with a sample Python file containing identifiers
|
# Create a temporary directory with a sample Python file containing identifiers
|
||||||
test_files = [
|
test_files = [
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
# flake8: noqa: E501
|
# flake8: noqa: E501
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from aider import utils
|
from aider import utils
|
||||||
|
|
||||||
|
|
||||||
|
@ -153,6 +154,51 @@ aider/coder.py
|
||||||
# Should not raise a ValueError
|
# Should not raise a ValueError
|
||||||
list(utils.find_original_update_blocks(edit))
|
list(utils.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
|
||||||
|
<<<<<<< ORIGINAL
|
||||||
|
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")
|
||||||
|
>>>>>>> UPDATED
|
||||||
|
|
||||||
|
<<<<<<< ORIGINAL
|
||||||
|
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"
|
||||||
|
}'''
|
||||||
|
>>>>>>> UPDATED
|
||||||
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
with self.assertRaises(ValueError) as cm:
|
||||||
|
list(utils.find_original_update_blocks(edit))
|
||||||
|
self.assertIn("missing filename", str(cm.exception))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue