Merge branch 'main' into watch

This commit is contained in:
Paul Gauthier 2024-11-30 15:48:19 -08:00
commit c9a27ba6ac
36 changed files with 1496 additions and 993 deletions

View file

@ -24,6 +24,8 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v3

View file

@ -12,6 +12,8 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v3

View file

@ -36,7 +36,9 @@ jobs:
working-directory: aider/website working-directory: aider/website
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Ruby - name: Setup Ruby
uses: ruby/setup-ruby@v1 uses: ruby/setup-ruby@v1
with: with:

View file

@ -12,6 +12,8 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v5

View file

@ -25,6 +25,8 @@ jobs:
steps: steps:
- name: Check out repository - name: Check out repository
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5 uses: actions/setup-python@v5
@ -38,5 +40,7 @@ jobs:
pip install . pip install .
- name: Run tests - name: Run tests
env:
AIDER_ANALYTICS: false
run: | run: |
pytest pytest

View file

@ -25,6 +25,8 @@ jobs:
steps: steps:
- name: Check out repository - name: Check out repository
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5 uses: actions/setup-python@v5
@ -38,6 +40,8 @@ jobs:
pip install . pip install .
- name: Run tests - name: Run tests
env:
AIDER_ANALYTICS: false
run: | run: |
pytest pytest

1
.gitignore vendored
View file

@ -11,6 +11,7 @@ _site
.jekyll-cache/ .jekyll-cache/
.jekyll-metadata .jekyll-metadata
aider/__version__.py aider/__version__.py
aider/_version.py
.venv/ .venv/
.#* .#*
.gitattributes .gitattributes

View file

@ -3,12 +3,17 @@
### main branch ### main branch
- PDF support for Sonnet and Gemini models. - PDF support for Sonnet and Gemini models.
- Added `--voice-input-device` to select audio input device for voice recording, by @preynal.
- Added `--timeout` option to configure API call timeouts.
- Set cwd to repo root when running shell commands. - Set cwd to repo root when running shell commands.
- Improved error handling for failed .gitignore file operations. - Improved error handling for failed .gitignore file operations.
- Improved error handling for input history file permissions. - Improved error handling for input history file permissions.
- Improved error handling for analytics file access. - Improved error handling for analytics file access.
- Removed broken support for Dart. - Removed broken support for Dart.
- Aider wrote 85% of the code in this release. - Bugfix when scraping URLs found in chat messages.
- Better handling of __version__ import errors.
- Improved `/drop` command to support substring matching for non-glob patterns.
- Aider wrote 79% of the code in this release.
### Aider v0.65.1 ### Aider v0.65.1

View file

@ -1,6 +1,20 @@
from packaging import version
__version__ = "0.65.2.dev"
safe_version = __version__
try: try:
from aider.__version__ import __version__ from aider._version import __version__
except Exception: except Exception:
__version__ = "0.65.2.dev" __version__ = safe_version + "+import"
if type(__version__) is not str:
__version__ = safe_version + "+type"
else:
try:
if version.parse(__version__) < version.parse(safe_version):
__version__ = safe_version + "+less"
except Exception:
__version__ = safe_version + "+parse"
__all__ = [__version__] __all__ = [__version__]

View file

@ -214,7 +214,3 @@ class Analytics:
with open(self.logfile, "a") as f: with open(self.logfile, "a") as f:
json.dump(log_entry, f) json.dump(log_entry, f)
f.write("\n") f.write("\n")
def __del__(self):
if self.ph:
self.ph.shutdown()

View file

@ -205,6 +205,12 @@ def get_parser(default_config_files, git_root):
default=True, default=True,
help="Verify the SSL cert when connecting to models (default: True)", help="Verify the SSL cert when connecting to models (default: True)",
) )
group.add_argument(
"--timeout",
type=int,
default=None,
help="Timeout in seconds for API calls (default: None)",
)
group.add_argument( group.add_argument(
"--edit-format", "--edit-format",
"--chat-mode", "--chat-mode",
@ -559,7 +565,7 @@ def get_parser(default_config_files, git_root):
group.add_argument( group.add_argument(
"--test", "--test",
action="store_true", action="store_true",
help="Run tests and fix problems found", help="Run tests, fix problems found and then exit",
default=False, default=False,
) )
@ -770,6 +776,12 @@ def get_parser(default_config_files, git_root):
default="en", default="en",
help="Specify the language for voice using ISO 639-1 code (default: auto)", help="Specify the language for voice using ISO 639-1 code (default: auto)",
) )
group.add_argument(
"--voice-input-device",
metavar="VOICE_INPUT_DEVICE",
default=None,
help="Specify the input device name for voice recording",
)
return parser return parser

View file

@ -771,6 +771,7 @@ class Coder:
self.lint_outcome = None self.lint_outcome = None
self.test_outcome = None self.test_outcome = None
self.shell_commands = [] self.shell_commands = []
self.message_cost = 0
if self.repo: if self.repo:
self.commit_before_message.append(self.repo.get_head_commit_sha()) self.commit_before_message.append(self.repo.get_head_commit_sha())
@ -884,6 +885,7 @@ class Coder:
thresh = 2 # seconds thresh = 2 # seconds
if self.last_keyboard_interrupt and now - self.last_keyboard_interrupt < thresh: if self.last_keyboard_interrupt and now - self.last_keyboard_interrupt < thresh:
self.io.tool_warning("\n\n^C KeyboardInterrupt") self.io.tool_warning("\n\n^C KeyboardInterrupt")
self.event("exit", reason="Control-C")
sys.exit() sys.exit()
self.io.tool_warning("\n\n^C again to exit") self.io.tool_warning("\n\n^C again to exit")
@ -1185,6 +1187,8 @@ class Coder:
return chunks return chunks
def send_message(self, inp): def send_message(self, inp):
self.event("message_send_starting")
self.cur_messages += [ self.cur_messages += [
dict(role="user", content=inp), dict(role="user", content=inp),
] ]
@ -1264,6 +1268,7 @@ class Coder:
lines = traceback.format_exception(type(err), err, err.__traceback__) lines = traceback.format_exception(type(err), err, err.__traceback__)
self.io.tool_warning("".join(lines)) self.io.tool_warning("".join(lines))
self.io.tool_error(str(err)) self.io.tool_error(str(err))
self.event("message_send_exception", exception=str(err))
return return
finally: finally:
if self.mdstream: if self.mdstream:

View file

@ -808,15 +808,33 @@ class Commands:
# Expand tilde in the path # Expand tilde in the path
expanded_word = os.path.expanduser(word) expanded_word = os.path.expanduser(word)
# Handle read-only files separately, without glob_filtered_to_repo # Handle read-only files with substring matching and samefile check
read_only_matched = [f for f in self.coder.abs_read_only_fnames if expanded_word in f] read_only_matched = []
for f in self.coder.abs_read_only_fnames:
if expanded_word in f:
read_only_matched.append(f)
continue
if read_only_matched: # Try samefile comparison for relative paths
for matched_file in read_only_matched: try:
self.coder.abs_read_only_fnames.remove(matched_file) abs_word = os.path.abspath(expanded_word)
self.io.tool_output(f"Removed read-only file {matched_file} from the chat") if os.path.samefile(abs_word, f):
read_only_matched.append(f)
except (FileNotFoundError, OSError):
continue
matched_files = self.glob_filtered_to_repo(expanded_word) for matched_file in read_only_matched:
self.coder.abs_read_only_fnames.remove(matched_file)
self.io.tool_output(f"Removed read-only file {matched_file} from the chat")
# For editable files, use glob if word contains glob chars, otherwise use substring
if any(c in expanded_word for c in "*?[]"):
matched_files = self.glob_filtered_to_repo(expanded_word)
else:
# Use substring matching like we do for read-only files
matched_files = [
self.coder.get_rel_fname(f) for f in self.coder.abs_fnames if expanded_word in f
]
if not matched_files: if not matched_files:
matched_files.append(expanded_word) matched_files.append(expanded_word)
@ -904,11 +922,12 @@ class Commands:
def cmd_exit(self, args): def cmd_exit(self, args):
"Exit the application" "Exit the application"
self.coder.event("exit", reason="/exit")
sys.exit() sys.exit()
def cmd_quit(self, args): def cmd_quit(self, args):
"Exit the application" "Exit the application"
sys.exit() self.cmd_exit(args)
def cmd_ls(self, args): def cmd_ls(self, args):
"List all known files and indicate which are included in the chat session" "List all known files and indicate which are included in the chat session"
@ -1080,7 +1099,9 @@ class Commands:
self.io.tool_error("To use /voice you must provide an OpenAI API key.") self.io.tool_error("To use /voice you must provide an OpenAI API key.")
return return
try: try:
self.voice = voice.Voice(audio_format=self.args.voice_format) self.voice = voice.Voice(
audio_format=self.args.voice_format, device_name=self.args.voice_input_device
)
except voice.SoundDeviceError: except voice.SoundDeviceError:
self.io.tool_error( self.io.tool_error(
"Unable to import `sounddevice` and/or `soundfile`, is portaudio installed?" "Unable to import `sounddevice` and/or `soundfile`, is portaudio installed?"

View file

@ -468,6 +468,10 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F
litellm._lazy_module.client_session = httpx.Client(verify=False) litellm._lazy_module.client_session = httpx.Client(verify=False)
litellm._lazy_module.aclient_session = httpx.AsyncClient(verify=False) litellm._lazy_module.aclient_session = httpx.AsyncClient(verify=False)
if args.timeout:
litellm._load_litellm()
litellm._lazy_module.request_timeout = args.timeout
if args.dark_mode: if args.dark_mode:
args.user_input_color = "#32FF32" args.user_input_color = "#32FF32"
args.tool_error_color = "#FF3333" args.tool_error_color = "#FF3333"
@ -548,9 +552,11 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F
if args.gui and not return_coder: if args.gui and not return_coder:
if not check_streamlit_install(io): if not check_streamlit_install(io):
analytics.event("exit", reason="Streamlit not installed")
return return
analytics.event("gui session") analytics.event("gui session")
launch_gui(argv) launch_gui(argv)
analytics.event("exit", reason="GUI session ended")
return return
if args.verbose: if args.verbose:
@ -577,6 +583,7 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F
io.tool_output( io.tool_output(
"Provide either a single directory of a git repo, or a list of one or more files." "Provide either a single directory of a git repo, or a list of one or more files."
) )
analytics.event("exit", reason="Invalid directory input")
return 1 return 1
git_dname = None git_dname = None
@ -587,6 +594,7 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F
fnames = [] fnames = []
else: else:
io.tool_error(f"{all_files[0]} is a directory, but --no-git selected.") io.tool_error(f"{all_files[0]} is a directory, but --no-git selected.")
analytics.event("exit", reason="Directory with --no-git")
return 1 return 1
# We can't know the git repo for sure until after parsing the args. # We can't know the git repo for sure until after parsing the args.
@ -595,18 +603,22 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F
if args.git and not force_git_root: if args.git and not force_git_root:
right_repo_root = guessed_wrong_repo(io, git_root, fnames, git_dname) right_repo_root = guessed_wrong_repo(io, git_root, fnames, git_dname)
if right_repo_root: if right_repo_root:
analytics.event("exit", reason="Recursing with correct repo")
return main(argv, input, output, right_repo_root, return_coder=return_coder) return main(argv, input, output, right_repo_root, return_coder=return_coder)
if args.just_check_update: if args.just_check_update:
update_available = check_version(io, just_check=True, verbose=args.verbose) update_available = check_version(io, just_check=True, verbose=args.verbose)
analytics.event("exit", reason="Just checking update")
return 0 if not update_available else 1 return 0 if not update_available else 1
if args.install_main_branch: if args.install_main_branch:
success = install_from_main_branch(io) success = install_from_main_branch(io)
analytics.event("exit", reason="Installed main branch")
return 0 if success else 1 return 0 if success else 1
if args.upgrade: if args.upgrade:
success = install_upgrade(io) success = install_upgrade(io)
analytics.event("exit", reason="Upgrade completed")
return 0 if success else 1 return 0 if success else 1
if args.check_update: if args.check_update:
@ -614,6 +626,7 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F
if args.list_models: if args.list_models:
models.print_matching_models(io, args.list_models) models.print_matching_models(io, args.list_models)
analytics.event("exit", reason="Listed models")
return 0 return 0
if args.git: if args.git:
@ -657,6 +670,7 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F
if len(parts) != 2: if len(parts) != 2:
io.tool_error(f"Invalid alias format: {alias_def}") io.tool_error(f"Invalid alias format: {alias_def}")
io.tool_output("Format should be: alias:model-name") io.tool_output("Format should be: alias:model-name")
analytics.event("exit", reason="Invalid alias format error")
return 1 return 1
alias, model = parts alias, model = parts
models.MODEL_ALIASES[alias.strip()] = model.strip() models.MODEL_ALIASES[alias.strip()] = model.strip()
@ -685,6 +699,7 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F
lint_cmds = parse_lint_cmds(args.lint_cmd, io) lint_cmds = parse_lint_cmds(args.lint_cmd, io)
if lint_cmds is None: if lint_cmds is None:
analytics.event("exit", reason="Invalid lint command format")
return 1 return 1
if args.show_model_warnings: if args.show_model_warnings:
@ -697,6 +712,7 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F
io.offer_url(urls.model_warnings, "Open documentation url for more info?") io.offer_url(urls.model_warnings, "Open documentation url for more info?")
io.tool_output() io.tool_output()
except KeyboardInterrupt: except KeyboardInterrupt:
analytics.event("exit", reason="Keyboard interrupt during model warnings")
return 1 return 1
repo = None repo = None
@ -720,6 +736,7 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F
if not args.skip_sanity_check_repo: if not args.skip_sanity_check_repo:
if not sanity_check_repo(repo, io): if not sanity_check_repo(repo, io):
analytics.event("exit", reason="Repository sanity check failed")
return 1 return 1
if repo: if repo:
@ -787,12 +804,15 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F
except UnknownEditFormat as err: except UnknownEditFormat as err:
io.tool_error(str(err)) io.tool_error(str(err))
io.offer_url(urls.edit_formats, "Open documentation about edit formats?") io.offer_url(urls.edit_formats, "Open documentation about edit formats?")
analytics.event("exit", reason="Unknown edit format")
return 1 return 1
except ValueError as err: except ValueError as err:
io.tool_error(str(err)) io.tool_error(str(err))
analytics.event("exit", reason="ValueError during coder creation")
return 1 return 1
if return_coder: if return_coder:
analytics.event("exit", reason="Returning coder object")
return coder return coder
ignores = [] ignores = []
@ -810,6 +830,7 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F
] ]
messages = coder.format_messages().all_messages() messages = coder.format_messages().all_messages()
utils.show_messages(messages) utils.show_messages(messages)
analytics.event("exit", reason="Showed prompts")
return return
if args.lint: if args.lint:
@ -818,6 +839,7 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F
if args.test: if args.test:
if not args.test_cmd: if not args.test_cmd:
io.tool_error("No --test-cmd provided.") io.tool_error("No --test-cmd provided.")
analytics.event("exit", reason="No test command provided")
return 1 return 1
test_errors = coder.commands.cmd_test(args.test_cmd) test_errors = coder.commands.cmd_test(args.test_cmd)
if test_errors: if test_errors:
@ -830,32 +852,30 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F
coder.commands.cmd_commit() coder.commands.cmd_commit()
if args.lint or args.test or args.commit: if args.lint or args.test or args.commit:
analytics.event("exit", reason="Completed lint/test/commit")
return return
if args.show_repo_map: if args.show_repo_map:
repo_map = coder.get_repo_map() repo_map = coder.get_repo_map()
if repo_map: if repo_map:
io.tool_output(repo_map) io.tool_output(repo_map)
analytics.event("exit", reason="Showed repo map")
return return
if args.apply: if args.apply:
content = io.read_text(args.apply) content = io.read_text(args.apply)
if content is None: if content is None:
analytics.event("exit", reason="Failed to read apply content")
return return
coder.partial_response_content = content coder.partial_response_content = content
coder.apply_updates() coder.apply_updates()
analytics.event("exit", reason="Applied updates")
return return
if args.apply_clipboard_edits: if args.apply_clipboard_edits:
args.edit_format = main_model.editor_edit_format args.edit_format = main_model.editor_edit_format
args.message = "/paste" args.message = "/paste"
if "VSCODE_GIT_IPC_HANDLE" in os.environ:
args.pretty = False
io.tool_output("VSCode terminal detected, pretty output has been disabled.")
io.tool_output('Use /help <question> for help, run "aider --help" to see cmd line args')
if args.show_release_notes is True: if args.show_release_notes is True:
io.tool_output(f"Opening release notes: {urls.release_notes}") io.tool_output(f"Opening release notes: {urls.release_notes}")
io.tool_output() io.tool_output()
@ -887,6 +907,7 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F
coder.run(with_message=args.message) coder.run(with_message=args.message)
except SwitchCoder: except SwitchCoder:
pass pass
analytics.event("exit", reason="Completed --message")
return return
if args.message_file: if args.message_file:
@ -896,13 +917,18 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F
coder.run(with_message=message_from_file) coder.run(with_message=message_from_file)
except FileNotFoundError: except FileNotFoundError:
io.tool_error(f"Message file not found: {args.message_file}") io.tool_error(f"Message file not found: {args.message_file}")
analytics.event("exit", reason="Message file not found")
return 1 return 1
except IOError as e: except IOError as e:
io.tool_error(f"Error reading message file: {e}") io.tool_error(f"Error reading message file: {e}")
analytics.event("exit", reason="Message file IO error")
return 1 return 1
analytics.event("exit", reason="Completed --message-file")
return return
if args.exit: if args.exit:
analytics.event("exit", reason="Exit flag set")
return return
analytics.event("cli session", main_model=main_model, edit_format=main_model.edit_format) analytics.event("cli session", main_model=main_model, edit_format=main_model.edit_format)
@ -910,6 +936,7 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F
while True: while True:
try: try:
coder.run() coder.run()
analytics.event("exit", reason="Completed main CLI coder.run")
return return
except SwitchCoder as switch: except SwitchCoder as switch:
kwargs = dict(io=io, from_coder=coder) kwargs = dict(io=io, from_coder=coder)
@ -928,6 +955,10 @@ def is_first_run_of_new_version(io, verbose=False):
installs_file = Path.home() / ".aider" / "installs.json" installs_file = Path.home() / ".aider" / "installs.json"
key = (__version__, sys.executable) key = (__version__, sys.executable)
# Never show notes for .dev versions
if ".dev" in __version__:
return False
if verbose: if verbose:
io.tool_output( io.tool_output(
f"Checking imports for version {__version__} and executable {sys.executable}" f"Checking imports for version {__version__} and executable {sys.executable}"

View file

@ -34,7 +34,7 @@ class Voice:
threshold = 0.15 threshold = 0.15
def __init__(self, audio_format="wav"): def __init__(self, audio_format="wav", device_name=None):
if sf is None: if sf is None:
raise SoundDeviceError raise SoundDeviceError
try: try:
@ -42,6 +42,27 @@ class Voice:
import sounddevice as sd import sounddevice as sd
self.sd = sd self.sd = sd
devices = sd.query_devices()
if device_name:
# Find the device with matching name
device_id = None
for i, device in enumerate(devices):
if device_name in device["name"]:
device_id = i
break
if device_id is None:
available_inputs = [d["name"] for d in devices if d["max_input_channels"] > 0]
raise ValueError(f"Device '{device_name}' not found. Available input devices: {available_inputs}")
print(f"Using input device: {device_name} (ID: {device_id})")
self.device_id = device_id
else:
self.device_id = None
except (OSError, ModuleNotFoundError): except (OSError, ModuleNotFoundError):
raise SoundDeviceError raise SoundDeviceError
if audio_format not in ["wav", "mp3", "webm"]: if audio_format not in ["wav", "mp3", "webm"]:
@ -93,7 +114,7 @@ class Voice:
temp_wav = tempfile.mktemp(suffix=".wav") temp_wav = tempfile.mktemp(suffix=".wav")
try: try:
sample_rate = int(self.sd.query_devices(None, "input")["default_samplerate"]) sample_rate = int(self.sd.query_devices(self.device_id, "input")["default_samplerate"])
except (TypeError, ValueError): except (TypeError, ValueError):
sample_rate = 16000 # fallback to 16kHz if unable to query device sample_rate = 16000 # fallback to 16kHz if unable to query device
except self.sd.PortAudioError: except self.sd.PortAudioError:
@ -104,7 +125,7 @@ class Voice:
self.start_time = time.time() self.start_time = time.time()
try: try:
with self.sd.InputStream(samplerate=sample_rate, channels=1, callback=self.callback): with self.sd.InputStream(samplerate=sample_rate, channels=1, callback=self.callback, device=self.device_id):
prompt(self.get_prompt, refresh_interval=0.1) prompt(self.get_prompt, refresh_interval=0.1)
except self.sd.PortAudioError as err: except self.sd.PortAudioError as err:
raise SoundDeviceError(f"Error accessing audio input device: {err}") raise SoundDeviceError(f"Error accessing audio input device: {err}")

View file

@ -27,12 +27,17 @@ cog.out(text)
### main branch ### main branch
- PDF support for Sonnet and Gemini models. - PDF support for Sonnet and Gemini models.
- Added `--voice-input-device` to select audio input device for voice recording, by @preynal.
- Added `--timeout` option to configure API call timeouts.
- Set cwd to repo root when running shell commands. - Set cwd to repo root when running shell commands.
- Improved error handling for failed .gitignore file operations. - Improved error handling for failed .gitignore file operations.
- Improved error handling for input history file permissions. - Improved error handling for input history file permissions.
- Improved error handling for analytics file access. - Improved error handling for analytics file access.
- Removed broken support for Dart. - Removed broken support for Dart.
- Aider wrote 85% of the code in this release. - Bugfix when scraping URLs found in chat messages.
- Better handling of __version__ import errors.
- Improved `/drop` command to support substring matching for non-glob patterns.
- Aider wrote 79% of the code in this release.
### Aider v0.65.1 ### Aider v0.65.1

View file

@ -1945,4 +1945,78 @@
command: aider --model gemini/gemini-exp-1114 command: aider --model gemini/gemini-exp-1114
date: 2024-11-15 date: 2024-11-15
versions: 0.63.2.dev versions: 0.63.2.dev
seconds_per_case: 38.6 seconds_per_case: 38.6
- dirname: 2024-11-27-07-41-51--qwen2.5-coder-14b-whole-1
test_cases: 133
model: ollama/qwen2.5-coder:14b
edit_format: whole
commit_hash: 200295e
pass_rate_1: 53.4
pass_rate_2: 61.7
percent_cases_well_formed: 98.5
error_outputs: 4
num_malformed_responses: 4
num_with_malformed_responses: 2
user_asks: 48
lazy_comments: 0
syntax_errors: 2
indentation_errors: 2
exhausted_context_windows: 0
test_timeouts: 2
command: aider --model ollama/qwen2.5-coder:14b
date: 2024-11-27
versions: 0.65.2.dev
seconds_per_case: 58.0
total_cost: 0.0000
- dirname: 2024-11-28-07-42-56--qwen2.5-coder-32b-whole-4
test_cases: 133
model: ollama/qwen2.5-coder:32b
edit_format: whole
commit_hash: 200295e
pass_rate_1: 58.6
pass_rate_2: 72.9
percent_cases_well_formed: 100.0
num_malformed_responses: 0
num_with_malformed_responses: 0
lazy_comments: 0
syntax_errors: 0
indentation_errors: 0
exhausted_context_windows: 0
command: aider --model ollama/qwen2.5-coder:32b
date: 2024-11-28
versions: 0.65.2.dev
seconds_per_case: 147.5
total_cost: 0.0000
- dirname: 2024-11-28-13-14-00--tulu3-whole-2
test_cases: 133
model: ollama/tulu3
edit_format: whole
commit_hash: 200295e
pass_rate_1: 21.8
pass_rate_2: 26.3
percent_cases_well_formed: 100.0
error_outputs: 0
num_malformed_responses: 0
num_with_malformed_responses: 0
exhausted_context_windows: 0
command: aider --model ollama/tulu3
date: 2024-11-28
versions: 0.65.2.dev
seconds_per_case: 35.8
total_cost: 0.0000
- dirname: 2024-11-28-14-41-46--granite3-dense-8b-whole-1
test_cases: 133
model: ollama/granite3-dense:8b
edit_format: whole
commit_hash: 200295e
pass_rate_1: 17.3
pass_rate_2: 20.3
percent_cases_well_formed: 78.9
exhausted_context_windows: 0
command: aider --model ollama/granite3-dense:8b
date: 2024-11-28
versions: 0.65.2.dev
seconds_per_case: 38.1
total_cost: 0.0000

View file

@ -81,6 +81,9 @@ if run with Ollama's default 2k context window.
## Benchmark results ## Benchmark results
{: .note :}
These are results from single benchmark runs, so expect normal variance of +/- 1-2%.
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script> <script>
{% include quant-chart.js %} {% include quant-chart.js %}

File diff suppressed because it is too large Load diff

View file

@ -97,6 +97,9 @@
## Verify the SSL cert when connecting to models (default: True) ## Verify the SSL cert when connecting to models (default: True)
#verify-ssl: true #verify-ssl: true
## Timeout in seconds for API calls (default: None)
#timeout: xxx
## Specify what edit format the LLM should use (default depends on model) ## Specify what edit format the LLM should use (default depends on model)
#edit-format: xxx #edit-format: xxx
@ -273,7 +276,7 @@
## Enable/disable automatic testing after changes (default: False) ## Enable/disable automatic testing after changes (default: False)
#auto-test: false #auto-test: false
## Run tests and fix problems found ## Run tests, fix problems found and then exit
#test: false #test: false
############ ############
@ -390,3 +393,6 @@
## Specify the language for voice using ISO 639-1 code (default: auto) ## Specify the language for voice using ISO 639-1 code (default: auto)
#voice-language: en #voice-language: en
## Specify the input device name for voice recording
#voice-input-device: xxx

View file

@ -96,6 +96,9 @@
## Verify the SSL cert when connecting to models (default: True) ## Verify the SSL cert when connecting to models (default: True)
#AIDER_VERIFY_SSL=true #AIDER_VERIFY_SSL=true
## Timeout in seconds for API calls (default: None)
#AIDER_TIMEOUT=
## Specify what edit format the LLM should use (default depends on model) ## Specify what edit format the LLM should use (default depends on model)
#AIDER_EDIT_FORMAT= #AIDER_EDIT_FORMAT=
@ -267,7 +270,7 @@
## Enable/disable automatic testing after changes (default: False) ## Enable/disable automatic testing after changes (default: False)
#AIDER_AUTO_TEST=false #AIDER_AUTO_TEST=false
## Run tests and fix problems found ## Run tests, fix problems found and then exit
#AIDER_TEST=false #AIDER_TEST=false
############ ############
@ -368,3 +371,6 @@
## Specify the language for voice using ISO 639-1 code (default: auto) ## Specify the language for voice using ISO 639-1 code (default: auto)
#AIDER_VOICE_LANGUAGE=en #AIDER_VOICE_LANGUAGE=en
## Specify the input device name for voice recording
#AIDER_VOICE_INPUT_DEVICE=

View file

@ -153,6 +153,9 @@ cog.outl("```")
## Verify the SSL cert when connecting to models (default: True) ## Verify the SSL cert when connecting to models (default: True)
#verify-ssl: true #verify-ssl: true
## Timeout in seconds for API calls (default: None)
#timeout: xxx
## Specify what edit format the LLM should use (default depends on model) ## Specify what edit format the LLM should use (default depends on model)
#edit-format: xxx #edit-format: xxx
@ -329,7 +332,7 @@ cog.outl("```")
## Enable/disable automatic testing after changes (default: False) ## Enable/disable automatic testing after changes (default: False)
#auto-test: false #auto-test: false
## Run tests and fix problems found ## Run tests, fix problems found and then exit
#test: false #test: false
############ ############
@ -446,5 +449,8 @@ cog.outl("```")
## Specify the language for voice using ISO 639-1 code (default: auto) ## Specify the language for voice using ISO 639-1 code (default: auto)
#voice-language: en #voice-language: en
## Specify the input device name for voice recording
#voice-input-device: xxx
``` ```
<!--[[[end]]]--> <!--[[[end]]]-->

View file

@ -138,6 +138,9 @@ cog.outl("```")
## Verify the SSL cert when connecting to models (default: True) ## Verify the SSL cert when connecting to models (default: True)
#AIDER_VERIFY_SSL=true #AIDER_VERIFY_SSL=true
## Timeout in seconds for API calls (default: None)
#AIDER_TIMEOUT=
## Specify what edit format the LLM should use (default depends on model) ## Specify what edit format the LLM should use (default depends on model)
#AIDER_EDIT_FORMAT= #AIDER_EDIT_FORMAT=
@ -309,7 +312,7 @@ cog.outl("```")
## Enable/disable automatic testing after changes (default: False) ## Enable/disable automatic testing after changes (default: False)
#AIDER_AUTO_TEST=false #AIDER_AUTO_TEST=false
## Run tests and fix problems found ## Run tests, fix problems found and then exit
#AIDER_TEST=false #AIDER_TEST=false
############ ############
@ -410,7 +413,8 @@ cog.outl("```")
## Specify the language for voice using ISO 639-1 code (default: auto) ## Specify the language for voice using ISO 639-1 code (default: auto)
#AIDER_VOICE_LANGUAGE=en #AIDER_VOICE_LANGUAGE=en
## Specify the input device name for voice recording
#AIDER_VOICE_INPUT_DEVICE=
``` ```
<!--[[[end]]]--> <!--[[[end]]]-->

View file

@ -32,7 +32,7 @@ usage: aider [-h] [--openai-api-key] [--anthropic-api-key] [--model]
[--openai-api-type] [--openai-api-version] [--openai-api-type] [--openai-api-version]
[--openai-api-deployment-id] [--openai-organization-id] [--openai-api-deployment-id] [--openai-organization-id]
[--model-settings-file] [--model-metadata-file] [--model-settings-file] [--model-metadata-file]
[--alias] [--verify-ssl | --no-verify-ssl] [--alias] [--verify-ssl | --no-verify-ssl] [--timeout]
[--edit-format] [--architect] [--weak-model] [--edit-format] [--architect] [--weak-model]
[--editor-model] [--editor-edit-format] [--editor-model] [--editor-edit-format]
[--show-model-warnings | --no-show-model-warnings] [--show-model-warnings | --no-show-model-warnings]
@ -76,6 +76,7 @@ usage: aider [-h] [--openai-api-key] [--anthropic-api-key] [--model]
[--fancy-input | --no-fancy-input] [--fancy-input | --no-fancy-input]
[--detect-urls | --no-detect-urls] [--editor] [--detect-urls | --no-detect-urls] [--editor]
[--voice-format] [--voice-language] [--voice-format] [--voice-language]
[--voice-input-device]
``` ```
@ -204,6 +205,10 @@ Aliases:
- `--verify-ssl` - `--verify-ssl`
- `--no-verify-ssl` - `--no-verify-ssl`
### `--timeout VALUE`
Timeout in seconds for API calls (default: None)
Environment variable: `AIDER_TIMEOUT`
### `--edit-format EDIT_FORMAT` ### `--edit-format EDIT_FORMAT`
Specify what edit format the LLM should use (default depends on model) Specify what edit format the LLM should use (default depends on model)
Environment variable: `AIDER_EDIT_FORMAT` Environment variable: `AIDER_EDIT_FORMAT`
@ -509,7 +514,7 @@ Aliases:
- `--no-auto-test` - `--no-auto-test`
### `--test` ### `--test`
Run tests and fix problems found Run tests, fix problems found and then exit
Default: False Default: False
Environment variable: `AIDER_TEST` Environment variable: `AIDER_TEST`
@ -701,4 +706,8 @@ Environment variable: `AIDER_VOICE_FORMAT`
Specify the language for voice using ISO 639-1 code (default: auto) Specify the language for voice using ISO 639-1 code (default: auto)
Default: en Default: en
Environment variable: `AIDER_VOICE_LANGUAGE` Environment variable: `AIDER_VOICE_LANGUAGE`
### `--voice-input-device VOICE_INPUT_DEVICE`
Specify the input device name for voice recording
Environment variable: `AIDER_VOICE_INPUT_DEVICE`
<!--[[[end]]]--> <!--[[[end]]]-->

View file

@ -53,6 +53,7 @@ Installing PortAudio is completely optional, but can usually be accomplished lik
- For Windows, there is no need to install PortAudio. - For Windows, there is no need to install PortAudio.
- For Mac, do `brew install portaudio` - For Mac, do `brew install portaudio`
- For Linux, do `sudo apt-get install libportaudio2` - For Linux, do `sudo apt-get install libportaudio2`
- Some linux environments may also need `sudo apt install libasound2-plugins`
## Add aider to your editor ## Add aider to your editor

View file

@ -181,6 +181,6 @@ mod_dates = [get_last_modified_date(file) for file in files]
latest_mod_date = max(mod_dates) latest_mod_date = max(mod_dates)
cog.out(f"{latest_mod_date.strftime('%B %d, %Y.')}") cog.out(f"{latest_mod_date.strftime('%B %d, %Y.')}")
]]]--> ]]]-->
November 24, 2024. November 28, 2024.
<!--[[[end]]]--> <!--[[[end]]]-->
</p> </p>

View file

@ -47,10 +47,44 @@ def find_latest_benchmark_dir():
print("Error: No benchmark directories found under tmp.benchmarks.") print("Error: No benchmark directories found under tmp.benchmarks.")
sys.exit(1) sys.exit(1)
latest_dir = max( # Get current time and 24 hours ago
benchmark_dirs, now = datetime.datetime.now()
key=lambda d: next((f.stat().st_mtime for f in d.rglob("*.md") if f.is_file()), 0), day_ago = now - datetime.timedelta(days=1)
)
# Filter directories by name pattern YYYY-MM-DD-HH-MM-SS--
recent_dirs = []
for d in benchmark_dirs:
try:
# Extract datetime from directory name
date_str = d.name[:19] # Takes YYYY-MM-DD-HH-MM-SS
dir_date = datetime.datetime.strptime(date_str, "%Y-%m-%d-%H-%M-%S")
if dir_date >= day_ago:
recent_dirs.append(d)
except ValueError:
# Skip directories that don't match the expected format
continue
if not recent_dirs:
print("Error: No benchmark directories found from the last 24 hours.")
sys.exit(1)
# Find directory with most recently modified .md file
latest_dir = None
latest_time = 0
for d in recent_dirs:
# Look for .md files in subdirectories
for md_file in d.glob("*/.*.md"):
if md_file.is_file():
mtime = md_file.stat().st_mtime
if mtime > latest_time:
latest_time = mtime
latest_dir = d
if not latest_dir:
print("Error: No .md files found in recent benchmark directories.")
sys.exit(1)
print(f"Using the most recently updated benchmark directory: {latest_dir.name}") print(f"Using the most recently updated benchmark directory: {latest_dir.name}")
return latest_dir return latest_dir

View file

@ -67,7 +67,7 @@ requires = ["setuptools>=68", "setuptools_scm[toml]>=8"]
build-backend = "setuptools.build_meta" build-backend = "setuptools.build_meta"
[tool.setuptools_scm] [tool.setuptools_scm]
write_to = "aider/__version__.py" write_to = "aider/_version.py"
[tool.codespell] [tool.codespell]
skip = "*.svg,Gemfile.lock" skip = "*.svg,Gemfile.lock"

View file

@ -6,5 +6,7 @@ testpaths =
tests/help tests/help
tests/browser tests/browser
tests/scrape tests/scrape
env =
AIDER_ANALYTICS=false

View file

@ -3,6 +3,7 @@
# pip-compile --output-file=requirements-dev.txt requirements-dev.in --upgrade # pip-compile --output-file=requirements-dev.txt requirements-dev.in --upgrade
# #
pytest pytest
pytest-env
pip-tools pip-tools
lox lox
matplotlib matplotlib

View file

@ -145,6 +145,10 @@ pyproject-hooks==1.2.0
# build # build
# pip-tools # pip-tools
pytest==8.3.3 pytest==8.3.3
# via
# -r requirements/requirements-dev.in
# pytest-env
pytest-env==1.1.5
# via -r requirements/requirements-dev.in # via -r requirements/requirements-dev.in
python-dateutil==2.9.0.post0 python-dateutil==2.9.0.post0
# via # via

View file

@ -22,7 +22,9 @@ def has_been_reopened(issue_number):
# Load environment variables from .env file # Load environment variables from .env file
load_dotenv() load_dotenv()
BOT_SUFFIX = """Note: A [bot script](https://github.com/Aider-AI/aider/blob/main/scripts/issues.py) made these updates to the issue. BOT_SUFFIX = """
Note: [A bot script](https://github.com/Aider-AI/aider/blob/main/scripts/issues.py) made these updates to the issue.
""" # noqa """ # noqa
DUPLICATE_COMMENT = ( DUPLICATE_COMMENT = (

View file

@ -37,10 +37,39 @@ def main():
# Get the git log output # Get the git log output
diff_content = run_git_log() diff_content = run_git_log()
# Save to temporary file # Extract relevant portion of HISTORY.md
with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".diff") as tmp: base_ver = get_base_version()
tmp.write(diff_content) with open("HISTORY.md", "r") as f:
tmp_path = tmp.name history_content = f.read()
# Find the section for this version
version_header = f"### Aider v{base_ver}"
start_idx = history_content.find("# Release history")
if start_idx == -1:
raise ValueError("Could not find start of release history")
# Find where this version's section ends
version_idx = history_content.find(version_header, start_idx)
if version_idx == -1:
raise ValueError(f"Could not find version header: {version_header}")
# Find the next version header after this one
next_version_idx = history_content.find("\n### Aider v", version_idx + len(version_header))
if next_version_idx == -1:
# No next version found, use the rest of the file
relevant_history = history_content[start_idx:]
else:
# Extract just up to the next version
relevant_history = history_content[start_idx:next_version_idx]
# Save relevant portions to temporary files
with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".diff") as tmp_diff:
tmp_diff.write(diff_content)
diff_path = tmp_diff.name
with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".md") as tmp_hist:
tmp_hist.write(relevant_history)
hist_path = tmp_hist.name
# Run blame to get aider percentage # Run blame to get aider percentage
blame_result = subprocess.run(["python3", "scripts/blame.py"], capture_output=True, text=True) blame_result = subprocess.run(["python3", "scripts/blame.py"], capture_output=True, text=True)
@ -58,14 +87,38 @@ Also, add this as the last bullet under the "### main branch" section:
{aider_line} {aider_line}
""" # noqa """ # noqa
cmd = ["aider", "HISTORY.md", "--read", tmp_path, "--msg", message, "--no-auto-commit"] cmd = ["aider", hist_path, "--read", diff_path, "--msg", message, "--no-auto-commit"]
subprocess.run(cmd) subprocess.run(cmd)
# Read back the updated history
with open(hist_path, "r") as f:
updated_history = f.read()
# Find where the next version section would start
if next_version_idx == -1:
# No next version found, use the rest of the file
full_history = history_content[:start_idx] + updated_history
else:
# Splice the updated portion back in between the unchanged parts
full_history = (
history_content[:start_idx]
+ updated_history # Keep unchanged header
+ history_content[next_version_idx:] # Add updated portion # Keep older entries
)
# Write back the full history
with open("HISTORY.md", "w") as f:
f.write(full_history)
# Run update-docs.sh after aider # Run update-docs.sh after aider
subprocess.run(["scripts/update-docs.sh"]) subprocess.run(["scripts/update-docs.sh"])
# Cleanup # Cleanup
os.unlink(tmp_path) os.unlink(diff_path)
os.unlink(hist_path)
# Show git diff of HISTORY.md
subprocess.run(["git", "diff", "HISTORY.md"])
if __name__ == "__main__": if __name__ == "__main__":

View file

@ -236,7 +236,7 @@ class TestCommands(TestCase):
self.assertIn(str(Path("test_dir/test_file2.txt").resolve()), coder.abs_fnames) self.assertIn(str(Path("test_dir/test_file2.txt").resolve()), coder.abs_fnames)
self.assertIn(str(Path("test_dir/another_dir/test_file.txt").resolve()), coder.abs_fnames) self.assertIn(str(Path("test_dir/another_dir/test_file.txt").resolve()), coder.abs_fnames)
commands.cmd_drop("test_dir/another_dir") commands.cmd_drop(str(Path("test_dir/another_dir")))
self.assertIn(str(Path("test_dir/test_file1.txt").resolve()), coder.abs_fnames) self.assertIn(str(Path("test_dir/test_file1.txt").resolve()), coder.abs_fnames)
self.assertIn(str(Path("test_dir/test_file2.txt").resolve()), coder.abs_fnames) self.assertIn(str(Path("test_dir/test_file2.txt").resolve()), coder.abs_fnames)
self.assertNotIn( self.assertNotIn(
@ -272,6 +272,7 @@ class TestCommands(TestCase):
coder = Coder.create(self.GPT35, None, io) coder = Coder.create(self.GPT35, None, io)
commands = Commands(io, coder) commands = Commands(io, coder)
# Create test files in root and subdirectory
subdir = Path("subdir") subdir = Path("subdir")
subdir.mkdir() subdir.mkdir()
(subdir / "subtest1.py").touch() (subdir / "subtest1.py").touch()
@ -279,17 +280,50 @@ class TestCommands(TestCase):
Path("test1.py").touch() Path("test1.py").touch()
Path("test2.py").touch() Path("test2.py").touch()
Path("test3.txt").touch()
# Add some files to the chat session # Add all Python files to the chat session
commands.cmd_add("*.py") commands.cmd_add("*.py")
initial_count = len(coder.abs_fnames)
self.assertEqual(initial_count, 2) # Only root .py files should be added
self.assertEqual(len(coder.abs_fnames), 2) # Test dropping with glob pattern
# Call the cmd_drop method with a glob pattern
commands.cmd_drop("*2.py") commands.cmd_drop("*2.py")
self.assertIn(str(Path("test1.py").resolve()), coder.abs_fnames) self.assertIn(str(Path("test1.py").resolve()), coder.abs_fnames)
self.assertNotIn(str(Path("test2.py").resolve()), coder.abs_fnames) self.assertNotIn(str(Path("test2.py").resolve()), coder.abs_fnames)
self.assertEqual(len(coder.abs_fnames), initial_count - 1)
def test_cmd_drop_without_glob(self):
# Initialize the Commands and InputOutput objects
io = InputOutput(pretty=False, fancy_input=False, yes=True)
from aider.coders import Coder
coder = Coder.create(self.GPT35, None, io)
commands = Commands(io, coder)
# Create test files
test_files = ["file1.txt", "file2.txt", "file3.py"]
for fname in test_files:
Path(fname).touch()
# Add all files to the chat session
for fname in test_files:
commands.cmd_add(fname)
initial_count = len(coder.abs_fnames)
self.assertEqual(initial_count, 3)
# Test dropping individual files without glob
commands.cmd_drop("file1.txt")
self.assertNotIn(str(Path("file1.txt").resolve()), coder.abs_fnames)
self.assertIn(str(Path("file2.txt").resolve()), coder.abs_fnames)
self.assertEqual(len(coder.abs_fnames), initial_count - 1)
# Test dropping multiple files without glob
commands.cmd_drop("file2.txt file3.py")
self.assertNotIn(str(Path("file2.txt").resolve()), coder.abs_fnames)
self.assertNotIn(str(Path("file3.py").resolve()), coder.abs_fnames)
self.assertEqual(len(coder.abs_fnames), 0)
def test_cmd_add_bad_encoding(self): def test_cmd_add_bad_encoding(self):
# Initialize the Commands and InputOutput objects # Initialize the Commands and InputOutput objects
@ -1397,6 +1431,43 @@ class TestCommands(TestCase):
finally: finally:
os.unlink(external_file_path) os.unlink(external_file_path)
def test_cmd_drop_read_only_with_relative_path(self):
with ChdirTemporaryDirectory() as repo_dir:
test_file = Path("test_file.txt")
test_file.write_text("Test content")
# Create a test file in a subdirectory
subdir = Path(repo_dir) / "subdir"
subdir.mkdir()
os.chdir(subdir)
io = InputOutput(pretty=False, fancy_input=False, yes=False)
coder = Coder.create(self.GPT35, None, io)
commands = Commands(io, coder)
# Add the file as read-only using absolute path
rel_path = str(Path("..") / "test_file.txt")
commands.cmd_read_only(rel_path)
self.assertEqual(len(coder.abs_read_only_fnames), 1)
# Try to drop using relative path from different working directories
commands.cmd_drop("test_file.txt")
self.assertEqual(len(coder.abs_read_only_fnames), 0)
# Add it again
commands.cmd_read_only(rel_path)
self.assertEqual(len(coder.abs_read_only_fnames), 1)
commands.cmd_drop(rel_path)
self.assertEqual(len(coder.abs_read_only_fnames), 0)
# Add it one more time
commands.cmd_read_only(rel_path)
self.assertEqual(len(coder.abs_read_only_fnames), 1)
commands.cmd_drop("test_file.txt")
self.assertEqual(len(coder.abs_read_only_fnames), 0)
def test_cmd_read_only_with_multiple_files(self): def test_cmd_read_only_with_multiple_files(self):
with GitTemporaryDirectory() as repo_dir: with GitTemporaryDirectory() as repo_dir:
io = InputOutput(pretty=False, fancy_input=False, yes=False) io = InputOutput(pretty=False, fancy_input=False, yes=False)

View file

@ -667,6 +667,10 @@ class TestMain(TestCase):
) )
self.assertTrue(coder.detect_urls) self.assertTrue(coder.detect_urls)
def test_pytest_env_vars(self):
# Verify that environment variables from pytest.ini are properly set
self.assertEqual(os.environ.get("AIDER_ANALYTICS"), "false")
def test_invalid_edit_format(self): def test_invalid_edit_format(self):
with GitTemporaryDirectory(): with GitTemporaryDirectory():
with patch("aider.io.InputOutput.offer_url") as mock_offer_url: with patch("aider.io.InputOutput.offer_url") as mock_offer_url:

96
tests/basic/test_voice.py Normal file
View file

@ -0,0 +1,96 @@
import os
import queue
from unittest.mock import patch
import numpy as np
import pytest
from aider.voice import SoundDeviceError, Voice
@pytest.fixture
def mock_sounddevice():
with patch("sounddevice.query_devices") as mock_query:
mock_query.return_value = [
{"name": "test_device", "max_input_channels": 2},
{"name": "another_device", "max_input_channels": 1},
]
yield mock_query
@pytest.fixture
def mock_soundfile():
with patch("soundfile.SoundFile") as mock_sf:
yield mock_sf
def test_voice_init_default_device(mock_sounddevice):
voice = Voice()
assert voice.device_id is None
assert voice.audio_format == "wav"
def test_voice_init_specific_device(mock_sounddevice):
voice = Voice(device_name="test_device")
assert voice.device_id == 0
def test_voice_init_invalid_device(mock_sounddevice):
with pytest.raises(ValueError) as exc:
Voice(device_name="nonexistent_device")
assert "Device" in str(exc.value)
assert "not found" in str(exc.value)
def test_voice_init_invalid_format():
with pytest.raises(ValueError) as exc:
Voice(audio_format="invalid")
assert "Unsupported audio format" in str(exc.value)
def test_callback_processing():
voice = Voice()
voice.q = queue.Queue()
# Test with silence (low amplitude)
test_data = np.zeros((1000, 1))
voice.callback(test_data, None, None, None)
assert voice.pct == 0.5 # When range is too small (<=0.001), pct is set to 0.5
# Test with loud signal (high amplitude)
test_data = np.ones((1000, 1))
voice.callback(test_data, None, None, None)
assert voice.pct > 0.9
# Verify data is queued
assert not voice.q.empty()
def test_get_prompt():
voice = Voice()
voice.start_time = voice.start_time = os.times().elapsed
voice.pct = 0.5 # 50% volume level
prompt = voice.get_prompt()
assert "Recording" in prompt
assert "sec" in prompt
assert "" in prompt # Should contain some filled blocks
assert "" in prompt # Should contain some empty blocks
@patch("sounddevice.InputStream")
def test_record_and_transcribe_keyboard_interrupt(mock_stream):
voice = Voice()
mock_stream.side_effect = KeyboardInterrupt()
result = voice.record_and_transcribe()
assert result is None
@patch("sounddevice.InputStream")
def test_record_and_transcribe_device_error(mock_stream):
voice = Voice()
mock_stream.side_effect = SoundDeviceError("Test error")
result = voice.record_and_transcribe()
assert result is None