Merge branch 'main' into call-graph

This commit is contained in:
Paul Gauthier 2023-05-29 11:18:30 -07:00
commit 27bd993cab
26 changed files with 788 additions and 202 deletions

39
.github/workflows/python-tests.yml vendored Normal file
View 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

View file

@ -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"]

View file

@ -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.
![aider screenshot](screenshot.gif) ![aider screenshot](assets/screenshot.gif)
- [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.

View file

@ -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">

View file

@ -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)

View file

@ -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.")

View file

@ -1,5 +1,5 @@
import traceback
import json import json
import traceback
def cvt(s): def cvt(s):

View file

@ -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:

View file

@ -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()

View file

@ -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:

View file

@ -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:]))

View file

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View file

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Before After
Before After

View file

@ -1,5 +1,5 @@
# Improving GPT-4's codebase understanding with a map # Improving GPT-4's codebase understanding with ctags
![robot flowchat](../assets/robot-flowchart.png) ![robot flowchat](../assets/robot-flowchart.png)

View file

@ -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
View 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

View file

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

View file

@ -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
View 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()

View file

@ -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
View 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()

View file

@ -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

View file

@ -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 = [

View file

@ -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()