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:
|
||||
- id: flake8
|
||||
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
|
||||
code with GPT-4. You can ask GPT to help you start
|
||||
a new project, or add features to your existing code.
|
||||
`aider` is well suited for working with code in a git repo,
|
||||
as it has features to easily
|
||||
commit, diff and undo changes proposed by GPT.
|
||||
a new project, or modify code in your existing git repo.
|
||||
Aider has features to
|
||||
[help GPT understand and modify larger codebases](https://aider.chat/docs/ctags.html)
|
||||
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
|
||||
|
||||
|
@ -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
|
||||
[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/).
|
||||
|
||||
## 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.
|
||||
* `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.
|
||||
|
@ -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 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.
|
||||
|
||||
|
@ -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.
|
||||
|
||||
You can also just launch `aider` anywhere in a git repo without naming files on the command line.
|
||||
It will discover all the files in the repo.
|
||||
You can then add and remove individual files in the chat session with the `/add` and `/drop` chat commands described below.
|
||||
You can also just launch `aider` anywhere in a git repo without naming
|
||||
files on the command line. It will discover all the files in the
|
||||
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
|
||||
to set many options:
|
||||
|
||||
```
|
||||
-h, --help show this help message and exit
|
||||
-c CONFIG_FILE, --config CONFIG_FILE
|
||||
Specify the config file (default: search for
|
||||
.aider.conf.yml in git root or home directory)
|
||||
Specify the config file (default: search for .aider.conf.yml in git root or home directory)
|
||||
--input-history-file INPUT_HISTORY_FILE
|
||||
Specify the chat input history file (default:
|
||||
.aider.input.history) [env var: AIDER_INPUT_HISTORY_FILE]
|
||||
Specify the chat input history file (default: .aider.input.history) [env var: AIDER_INPUT_HISTORY_FILE]
|
||||
--chat-history-file CHAT_HISTORY_FILE
|
||||
Specify the chat history file (default:
|
||||
.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]
|
||||
Specify the chat history file (default: .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]
|
||||
-3 Use gpt-3.5-turbo model for the main chat (not advised)
|
||||
--pretty Enable pretty, colorized output (default: True) [env var:
|
||||
AIDER_PRETTY]
|
||||
--no-pretty Disable pretty, colorized output
|
||||
--apply FILE Apply the changes from the given file instead of running
|
||||
the chat (debug)
|
||||
--auto-commits Enable auto commit of changes (default: True) [env var:
|
||||
AIDER_AUTO_COMMIT]
|
||||
--no-auto-commits Disable auto commit of changes
|
||||
--dry-run Perform a dry run without applying changes (default: False)
|
||||
--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
|
||||
(default: check for ctags executable) [env var:
|
||||
AIDER_CTAGS]
|
||||
--yes Always say yes to every confirmation
|
||||
-v, --verbose Enable verbose output
|
||||
--pretty Enable pretty, colorized output (default: True) [env var: AIDER_PRETTY]
|
||||
--no-pretty Disable pretty, colorized output [env var: AIDER_NO_PRETTY]
|
||||
--apply FILE Apply the changes from the given file instead of running the chat (debug) [env var: AIDER_APPLY]
|
||||
--auto-commits Enable auto commit of changes (default: True) [env var: AIDER_AUTO_COMMITS]
|
||||
--no-auto-commits Disable auto commit of changes [env var: AIDER_NO_AUTO_COMMITS]
|
||||
--dirty-commits Enable dirty commit of changes [env var: AIDER_DIRTY_COMMITS]
|
||||
--no-dirty-commits Disable dirty commit of changes [env var: AIDER_NO_DIRTY_COMMITS]
|
||||
--openai-api-key OPENAI_API_KEY
|
||||
Specify the OpenAI API key [env var: OPENAI_API_KEY]
|
||||
--dry-run Perform a dry run without applying changes (default: False) [env var: AIDER_DRY_RUN]
|
||||
--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 (default: check for ctags executable) [env var: AIDER_CTAGS]
|
||||
--yes Always say yes to every confirmation [env var: AIDER_YES]
|
||||
-v, --verbose Enable verbose output [env var: AIDER_VERBOSE]
|
||||
```
|
||||
|
||||
## Chat commands
|
||||
## In chat commands
|
||||
|
||||
`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
|
||||
|
||||
* 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 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.
|
||||
|
|
|
@ -4,8 +4,8 @@
|
|||
<meta charset="UTF-8">
|
||||
|
||||
{% seo %}
|
||||
<meta property="og:image" content="{{ site.baseurl }}/screenshot.png">
|
||||
<meta property="twitter:image" content="{{ site.baseurl }}/screenshot.png">
|
||||
<meta property="og:image" content="{{ site.baseurl }}/assets/screenshot.png">
|
||||
<meta property="twitter:image" content="{{ site.baseurl }}/assets/screenshot.png">
|
||||
<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>
|
||||
<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 sys
|
||||
import traceback
|
||||
import time
|
||||
from openai.error import RateLimitError
|
||||
|
||||
from rich.console import Console
|
||||
from rich.live import Live
|
||||
from rich.markdown import Markdown
|
||||
import traceback
|
||||
from pathlib import Path
|
||||
|
||||
import git
|
||||
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 import utils
|
||||
from aider import prompts
|
||||
from aider import prompts, utils
|
||||
from aider.commands import Commands
|
||||
from aider.repomap import RepoMap
|
||||
|
||||
openai.api_key = os.getenv("OPENAI_API_KEY")
|
||||
|
||||
class MissingAPIKeyError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
class Coder:
|
||||
|
@ -37,10 +37,17 @@ class Coder:
|
|||
pretty=True,
|
||||
show_diffs=False,
|
||||
auto_commits=True,
|
||||
dirty_commits=True,
|
||||
dry_run=False,
|
||||
use_ctags=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.abs_fnames = set()
|
||||
self.cur_messages = []
|
||||
|
@ -48,6 +55,7 @@ class Coder:
|
|||
|
||||
self.io = io
|
||||
self.auto_commits = auto_commits
|
||||
self.dirty_commits = dirty_commits
|
||||
self.dry_run = dry_run
|
||||
|
||||
if pretty:
|
||||
|
@ -66,7 +74,7 @@ class Coder:
|
|||
|
||||
if self.repo:
|
||||
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:
|
||||
self.io.tool_error("No suitable git repo, will not automatically commit edits.")
|
||||
self.find_common_root()
|
||||
|
@ -83,7 +91,7 @@ class Coder:
|
|||
else:
|
||||
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):
|
||||
if not cmd_line_fnames:
|
||||
|
@ -93,7 +101,7 @@ class Coder:
|
|||
for fname in cmd_line_fnames:
|
||||
fname = Path(fname)
|
||||
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.touch()
|
||||
|
||||
|
@ -106,7 +114,7 @@ class Coder:
|
|||
if fname.is_dir():
|
||||
continue
|
||||
|
||||
self.io.tool(f"Added {fname} to the chat")
|
||||
self.io.tool_output(f"Added {fname} to the chat")
|
||||
|
||||
fname = fname.resolve()
|
||||
self.abs_fnames.add(str(fname))
|
||||
|
@ -135,18 +143,18 @@ class Coder:
|
|||
if new_files:
|
||||
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:
|
||||
self.io.tool(f" - {fn}")
|
||||
self.io.tool_output(f" - {fn}")
|
||||
if self.io.confirm_ask("Add them?"):
|
||||
for relative_fname in new_files:
|
||||
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)
|
||||
commit_message = f"Added new files to the git repo: {show_files}"
|
||||
repo.git.commit("-m", commit_message, "--no-verify")
|
||||
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:
|
||||
self.io.tool_error("Skipped adding new files to the git repo.")
|
||||
return
|
||||
|
@ -216,7 +224,7 @@ class Coder:
|
|||
if is_commit_command:
|
||||
return
|
||||
|
||||
if not self.auto_commits:
|
||||
if not self.dirty_commits:
|
||||
return
|
||||
if not self.repo:
|
||||
return
|
||||
|
@ -227,7 +235,12 @@ class Coder:
|
|||
return True
|
||||
|
||||
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
|
||||
|
||||
|
@ -243,7 +256,7 @@ class Coder:
|
|||
self.cur_messages = []
|
||||
|
||||
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
|
||||
|
||||
if not inp:
|
||||
|
@ -252,6 +265,8 @@ class Coder:
|
|||
if self.commands.is_command(inp):
|
||||
return self.commands.run(inp)
|
||||
|
||||
self.check_for_file_mentions(inp)
|
||||
|
||||
return self.send_new_user_message(inp)
|
||||
|
||||
def send_new_user_message(self, inp):
|
||||
|
@ -279,7 +294,7 @@ class Coder:
|
|||
dict(role="assistant", content=content),
|
||||
]
|
||||
|
||||
self.io.tool()
|
||||
self.io.tool_output()
|
||||
if interrupted:
|
||||
return
|
||||
|
||||
|
@ -327,20 +342,29 @@ class Coder:
|
|||
quotes = "".join(['"', "'", "`"])
|
||||
words = set(word.strip(quotes) for word in words)
|
||||
|
||||
addable_rel_fnames = set(self.get_all_relative_files()) - set(
|
||||
self.get_inchat_relative_files()
|
||||
)
|
||||
addable_rel_fnames = self.get_addable_relative_files()
|
||||
|
||||
mentioned_rel_fnames = set()
|
||||
for word in words:
|
||||
if word in addable_rel_fnames:
|
||||
mentioned_rel_fnames.add(word)
|
||||
fname_to_rel_fnames = {}
|
||||
for rel_fname in addable_rel_fnames:
|
||||
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:
|
||||
return
|
||||
|
||||
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?"):
|
||||
return
|
||||
|
@ -450,9 +474,9 @@ class Coder:
|
|||
edited.add(path)
|
||||
if utils.do_replace(full_path, original, updated, 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:
|
||||
self.io.tool(f"Applied edit to {path}")
|
||||
self.io.tool_output(f"Applied edit to {path}")
|
||||
else:
|
||||
self.io.tool_error(f"Failed to apply edit to {path}")
|
||||
|
||||
|
@ -490,7 +514,9 @@ class Coder:
|
|||
)
|
||||
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:
|
||||
self.io.tool_error(
|
||||
|
@ -548,7 +574,7 @@ class Coder:
|
|||
raise ValueError(f"Invalid value for 'which': {which}")
|
||||
|
||||
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)
|
||||
|
||||
context = self.get_context_from_history(history)
|
||||
|
@ -562,9 +588,9 @@ class Coder:
|
|||
|
||||
if ask:
|
||||
if which == "repo_files":
|
||||
self.io.tool("Git repo has uncommitted changes.")
|
||||
self.io.tool_output("Git repo has uncommitted changes.")
|
||||
else:
|
||||
self.io.tool("Files have uncommitted changes.")
|
||||
self.io.tool_output("Files have uncommitted changes.")
|
||||
|
||||
res = self.io.prompt_ask(
|
||||
"Commit before the chat proceeds [y/n/commit message]?",
|
||||
|
@ -572,7 +598,7 @@ class Coder:
|
|||
).strip()
|
||||
self.last_asked_for_commit_time = self.get_last_modified()
|
||||
|
||||
self.io.tool()
|
||||
self.io.tool_output()
|
||||
|
||||
if res.lower() in ["n", "no"]:
|
||||
self.io.tool_error("Skipped commmit.")
|
||||
|
@ -585,7 +611,7 @@ class Coder:
|
|||
full_commit_message = commit_message + "\n\n" + context
|
||||
repo.git.commit("-m", full_commit_message, "--no-verify")
|
||||
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
|
||||
|
||||
|
@ -615,6 +641,9 @@ class Coder:
|
|||
return 0
|
||||
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):
|
||||
try:
|
||||
edited = self.update_files(content, inp)
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import sys
|
||||
import os
|
||||
import git
|
||||
import subprocess
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import git
|
||||
from prompt_toolkit.completion import Completion
|
||||
|
||||
from aider import prompts
|
||||
|
||||
|
||||
|
@ -37,7 +39,7 @@ class Commands:
|
|||
if cmd_method:
|
||||
return cmd_method(args)
|
||||
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):
|
||||
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.")
|
||||
return
|
||||
self.coder.repo.git.reset("--hard", "HEAD~1")
|
||||
self.io.tool(
|
||||
self.io.tool_output(
|
||||
f"{last_commit.message.strip()}\n"
|
||||
f"The above commit {self.coder.last_aider_commit_hash} "
|
||||
"was reset and removed from git.\n"
|
||||
|
@ -131,7 +133,7 @@ class Commands:
|
|||
commits = f"{self.coder.last_aider_commit_hash}~1"
|
||||
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)
|
||||
|
||||
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))
|
||||
if abs_file_path not in self.coder.abs_fnames:
|
||||
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)
|
||||
else:
|
||||
self.io.tool_error(f"{matched_file} is already in the chat")
|
||||
|
@ -214,7 +216,7 @@ class Commands:
|
|||
for matched_file in matched_files:
|
||||
relative_fname = os.path.relpath(matched_file, self.coder.root)
|
||||
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):
|
||||
"Run a shell command and optionally add the output to the chat"
|
||||
|
@ -229,9 +231,9 @@ class Commands:
|
|||
|
||||
print(combined_output)
|
||||
|
||||
ok = self.io.confirm_ask("Add the output to the chat?", default="y")
|
||||
if not ok:
|
||||
return
|
||||
if self.io.confirm_ask("Add the output to the chat?", default="y"):
|
||||
for line in combined_output.splitlines():
|
||||
self.io.tool_output(line, log_only=True)
|
||||
|
||||
msg = prompts.run_output.format(
|
||||
command=args,
|
||||
|
@ -258,14 +260,14 @@ class Commands:
|
|||
other_files.append(file)
|
||||
|
||||
if chat_files:
|
||||
self.io.tool("Files in chat:\n")
|
||||
self.io.tool_output("Files in chat:\n")
|
||||
for file in chat_files:
|
||||
self.io.tool(f" {file}")
|
||||
self.io.tool_output(f" {file}")
|
||||
|
||||
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:
|
||||
self.io.tool(f" {file}")
|
||||
self.io.tool_output(f" {file}")
|
||||
|
||||
def cmd_help(self, args):
|
||||
"Show help about all commands"
|
||||
|
@ -275,6 +277,6 @@ class Commands:
|
|||
cmd_method = getattr(self, cmd_method_name, None)
|
||||
if cmd_method:
|
||||
description = cmd_method.__doc__
|
||||
self.io.tool(f"{cmd} {description}")
|
||||
self.io.tool_output(f"{cmd} {description}")
|
||||
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 traceback
|
||||
|
||||
|
||||
def cvt(s):
|
||||
|
|
104
aider/io.py
104
aider/io.py
|
@ -1,24 +1,49 @@
|
|||
import os
|
||||
from prompt_toolkit.styles import Style
|
||||
from prompt_toolkit.shortcuts import PromptSession, prompt
|
||||
from collections import defaultdict
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from prompt_toolkit.completion import Completer, Completion
|
||||
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.text import Text
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
from aider import utils
|
||||
|
||||
|
||||
class FileContentCompleter(Completer):
|
||||
def __init__(self, fnames, commands):
|
||||
def __init__(self, root, rel_fnames, addable_rel_fnames, 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()
|
||||
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):
|
||||
text = document.text_before_cursor
|
||||
|
@ -35,10 +60,16 @@ class FileContentCompleter(Completer):
|
|||
return
|
||||
else:
|
||||
candidates = self.words
|
||||
candidates.update(set(self.fname_to_rel_fnames))
|
||||
|
||||
last_word = words[-1]
|
||||
for word in candidates:
|
||||
if word.lower().startswith(last_word.lower()):
|
||||
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))
|
||||
|
||||
|
||||
|
@ -51,7 +82,18 @@ class InputOutput:
|
|||
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
|
||||
|
@ -65,29 +107,20 @@ class InputOutput:
|
|||
if pretty:
|
||||
self.console = Console()
|
||||
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")
|
||||
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:
|
||||
self.console.rule()
|
||||
style = dict(style=self.user_input_color) if self.user_input_color else dict()
|
||||
self.console.rule(**style)
|
||||
else:
|
||||
print()
|
||||
|
||||
fnames = list(fnames)
|
||||
if len(fnames) > 1:
|
||||
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)
|
||||
rel_fnames = list(rel_fnames)
|
||||
show = " ".join(rel_fnames)
|
||||
if len(show) > 10:
|
||||
show += "\n"
|
||||
show += "> "
|
||||
|
@ -95,22 +128,29 @@ class InputOutput:
|
|||
inp = ""
|
||||
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:
|
||||
completer_instance = FileContentCompleter(fnames, commands)
|
||||
completer_instance = FileContentCompleter(
|
||||
root, rel_fnames, addable_rel_fnames, commands
|
||||
)
|
||||
if multiline_input:
|
||||
show = ". "
|
||||
|
||||
session_kwargs = {
|
||||
"message": show,
|
||||
"completer": completer_instance,
|
||||
"style": style,
|
||||
"reserve_space_for_menu": 4,
|
||||
"complete_style": CompleteStyle.MULTI_COLUMN,
|
||||
"input": self.input,
|
||||
"output": self.output,
|
||||
}
|
||||
if style:
|
||||
session_kwargs["style"] = style
|
||||
|
||||
if self.input_history_file is not None:
|
||||
session_kwargs["history"] = FileHistory(self.input_history_file)
|
||||
|
||||
|
@ -180,9 +220,10 @@ class InputOutput:
|
|||
self.append_chat_history(hist, linebreak=True, blockquote=True)
|
||||
|
||||
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:
|
||||
hist = " ".join(messages)
|
||||
hist = f"{hist.strip()}"
|
||||
|
@ -190,7 +231,8 @@ class InputOutput:
|
|||
|
||||
if not log_only:
|
||||
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):
|
||||
if blockquote:
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import os
|
||||
import sys
|
||||
import git
|
||||
|
||||
import configargparse
|
||||
from dotenv import load_dotenv
|
||||
import git
|
||||
|
||||
from aider.coder import Coder
|
||||
from aider.io import InputOutput
|
||||
|
||||
|
@ -19,13 +20,11 @@ def main(args=None, input=None, output=None):
|
|||
if args is None:
|
||||
args = sys.argv[1:]
|
||||
|
||||
load_dotenv()
|
||||
env_prefix = "AIDER_"
|
||||
git_root = get_git_root()
|
||||
|
||||
default_config_files = [
|
||||
os.path.expanduser("~/.aider.conf.yml"),
|
||||
]
|
||||
git_root = get_git_root()
|
||||
if git_root:
|
||||
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,
|
||||
default_config_files=default_config_files,
|
||||
config_file_parser_class=configargparse.YAMLConfigFileParser,
|
||||
auto_env_var_prefix="AIDER_",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
|
@ -63,21 +63,18 @@ def main(args=None, input=None, output=None):
|
|||
parser.add_argument(
|
||||
"--input-history-file",
|
||||
metavar="INPUT_HISTORY_FILE",
|
||||
env_var=f"{env_prefix}INPUT_HISTORY_FILE",
|
||||
default=default_input_history_file,
|
||||
help=f"Specify the chat input history file (default: {default_input_history_file})",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--chat-history-file",
|
||||
metavar="CHAT_HISTORY_FILE",
|
||||
env_var=f"{env_prefix}CHAT_HISTORY_FILE",
|
||||
default=default_chat_history_file,
|
||||
help=f"Specify the chat history file (default: {default_chat_history_file})",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--model",
|
||||
metavar="MODEL",
|
||||
env_var=f"{env_prefix}MODEL",
|
||||
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(
|
||||
"--pretty",
|
||||
action="store_true",
|
||||
env_var=f"{env_prefix}PRETTY",
|
||||
default=True,
|
||||
help="Enable pretty, colorized output (default: True)",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--no-pretty",
|
||||
action="store_false",
|
||||
dest="pretty",
|
||||
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(
|
||||
"--apply",
|
||||
metavar="FILE",
|
||||
|
@ -110,7 +120,7 @@ def main(args=None, input=None, output=None):
|
|||
parser.add_argument(
|
||||
"--auto-commits",
|
||||
action="store_true",
|
||||
env_var=f"{env_prefix}AUTO_COMMIT",
|
||||
dest="auto_commits",
|
||||
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(
|
||||
"--no-auto-commits",
|
||||
action="store_false",
|
||||
dest="auto_commit",
|
||||
dest="auto_commits",
|
||||
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(
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
|
@ -130,7 +159,6 @@ def main(args=None, input=None, output=None):
|
|||
parser.add_argument(
|
||||
"--show-diffs",
|
||||
action="store_true",
|
||||
env_var=f"{env_prefix}SHOW_DIFFS",
|
||||
help="Show diffs when committing changes (default: False)",
|
||||
default=False,
|
||||
)
|
||||
|
@ -140,7 +168,6 @@ def main(args=None, input=None, output=None):
|
|||
nargs="?",
|
||||
const=True,
|
||||
default=None,
|
||||
env_var=f"{env_prefix}CTAGS",
|
||||
help=(
|
||||
"Add ctags to the chat to help GPT understand the codebase (default: check for ctags"
|
||||
" executable)"
|
||||
|
@ -168,9 +195,23 @@ def main(args=None, input=None, output=None):
|
|||
args.chat_history_file,
|
||||
input=input,
|
||||
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(
|
||||
io,
|
||||
|
@ -179,12 +220,15 @@ def main(args=None, input=None, output=None):
|
|||
pretty=args.pretty,
|
||||
show_diffs=args.show_diffs,
|
||||
auto_commits=args.auto_commits,
|
||||
dirty_commits=args.dirty_commits,
|
||||
dry_run=args.dry_run,
|
||||
use_ctags=args.ctags,
|
||||
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:
|
||||
with open(args.apply, "r") as f:
|
||||
|
@ -192,6 +236,7 @@ def main(args=None, input=None, output=None):
|
|||
coder.update_files(content, inp="")
|
||||
return
|
||||
|
||||
io.tool_output("Use /help to see in-chat commands.")
|
||||
coder.run()
|
||||
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ Take requests for changes to the supplied code.
|
|||
If the request is ambiguous, ask questions.
|
||||
|
||||
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.
|
||||
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
|
||||
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:
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
import os
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
import subprocess
|
||||
import tiktoken
|
||||
import sys
|
||||
import tempfile
|
||||
from collections import defaultdict, Counter
|
||||
from collections import Counter, defaultdict
|
||||
|
||||
import networkx as nx
|
||||
import tiktoken
|
||||
|
||||
from aider import prompts, utils
|
||||
from aider.dump import dump
|
||||
|
||||
|
||||
# Global cache for tags
|
||||
TAGS_CACHE = {}
|
||||
|
@ -203,6 +202,7 @@ def find_py_files(directory):
|
|||
|
||||
def call_map():
|
||||
import random
|
||||
|
||||
import graphviz
|
||||
|
||||
fnames = sys.argv[1:]
|
||||
|
@ -322,3 +322,4 @@ def call_map():
|
|||
|
||||
if __name__ == "__main__":
|
||||
call_map()
|
||||
# print(rm.get_tags_map(sys.argv[1:]))
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import re
|
||||
import math
|
||||
|
||||
import re
|
||||
from difflib import SequenceMatcher
|
||||
from pathlib import Path
|
||||
from pygments.util import ClassNotFound
|
||||
|
||||
from pygments.lexers import guess_lexer_for_filename
|
||||
from pygments.token import Token
|
||||
from pygments.util import ClassNotFound
|
||||
|
||||
# from aider.dump import dump
|
||||
|
||||
|
@ -234,9 +234,14 @@ def find_original_update_blocks(content):
|
|||
processed.append(cur) # original_marker
|
||||
|
||||
filename = processed[-2].splitlines()[-1].strip()
|
||||
try:
|
||||
if not len(filename) or "`" in filename:
|
||||
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()
|
||||
|
@ -248,8 +253,10 @@ def find_original_update_blocks(content):
|
|||
raise ValueError(f"Expected {DIVIDER}")
|
||||
|
||||
updated_text = pieces.pop()
|
||||
processed.append(updated_text)
|
||||
|
||||
updated_marker = pieces.pop()
|
||||
processed.append(updated_marker)
|
||||
if updated_marker.strip() != 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).
|
||||
|
||||
* [**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.
|
||||
|
||||
* [**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
|
||||
prompt-toolkit==3.0.38
|
||||
Pygments==2.15.1
|
||||
python-dotenv==1.0.0
|
||||
requests==2.30.0
|
||||
rich==13.3.5
|
||||
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:
|
||||
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,22 +1,28 @@
|
|||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
from unittest import TestCase
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from aider.commands import Commands
|
||||
from aider.io import InputOutput
|
||||
|
||||
|
||||
class TestCommands(TestCase):
|
||||
def test_cmd_add(self):
|
||||
# Create a temporary directory and change the current working directory
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
os.chdir(tmpdir)
|
||||
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):
|
||||
# Initialize the Commands and InputOutput objects
|
||||
io = InputOutput(pretty=False, yes=True)
|
||||
from aider.coder import Coder
|
||||
|
||||
coder = Coder(io)
|
||||
coder = Coder(io, openai_api_key="deadbeef")
|
||||
commands = Commands(io, coder)
|
||||
|
||||
# Call the cmd_add method with 'foo.txt' and 'bar.txt' as a single string
|
||||
|
|
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 tempfile
|
||||
from unittest import TestCase
|
||||
from aider.main import main
|
||||
import shutil
|
||||
import subprocess
|
||||
from prompt_toolkit.input import create_input
|
||||
import tempfile
|
||||
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 aider.main import main
|
||||
|
||||
|
||||
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):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
os.chdir(temp_dir)
|
||||
pipe_input = create_input(StringIO(""))
|
||||
main([], input=pipe_input, output=DummyOutput())
|
||||
pipe_input.close()
|
||||
|
||||
def test_main_with_empty_dir_new_file(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
os.chdir(temp_dir)
|
||||
pipe_input = create_input(StringIO(""))
|
||||
main(["foo.txt"], input=pipe_input, output=DummyOutput())
|
||||
pipe_input.close()
|
||||
self.assertTrue(os.path.exists("foo.txt"))
|
||||
|
||||
def test_main_with_empty_git_dir_new_file(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
os.chdir(temp_dir)
|
||||
subprocess.run(["git", "init"], cwd=temp_dir)
|
||||
subprocess.run(["git", "init"])
|
||||
subprocess.run(["git", "config", "user.email", "dummy@example.com"])
|
||||
subprocess.run(["git", "config", "user.name", "Dummy User"])
|
||||
pipe_input = create_input(StringIO(""))
|
||||
main(["--yes", "foo.txt"], input=pipe_input, output=DummyOutput())
|
||||
pipe_input.close()
|
||||
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 tempfile
|
||||
import unittest
|
||||
from subprocess import CompletedProcess
|
||||
from unittest.mock import patch
|
||||
|
||||
from aider.repomap import RepoMap
|
||||
|
||||
|
||||
|
@ -55,6 +58,27 @@ def my_function(arg1, arg2):
|
|||
self.assertIn("my_method", 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):
|
||||
# Create a temporary directory with a sample Python file containing identifiers
|
||||
test_files = [
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# flake8: noqa: E501
|
||||
|
||||
import unittest
|
||||
|
||||
from aider import utils
|
||||
|
||||
|
||||
|
@ -153,6 +154,51 @@ aider/coder.py
|
|||
# Should not raise a ValueError
|
||||
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__":
|
||||
unittest.main()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue