diff --git a/aider/args.py b/aider/args.py index 5b3fdf07faf..a6e189f8cf9 100644 --- a/aider/args.py +++ b/aider/args.py @@ -19,6 +19,19 @@ from .dump import dump # noqa: F401 +class YAMLConfigFileParserIgnoreProfiles(configargparse.YAMLConfigFileParser): + """ + A custom YAMLConfigFileParser that removes the top-level 'profiles' key from its result. We use + this to avoid having to set ignore_unknown_config_file_keys=True on our + configargparse.ArgumentParser, which would fail to flag invalid config keys to the user. + """ + + def parse(self, stream): + result = super().parse(stream) + result.pop("profiles", None) + return result + + def resolve_aiderignore_path(path_str, git_root=None): path = Path(path_str) if path.is_absolute(): @@ -37,7 +50,7 @@ def get_parser(default_config_files, git_root): description="aider is AI pair programming in your terminal", add_config_file_help=True, default_config_files=default_config_files, - config_file_parser_class=configargparse.YAMLConfigFileParser, + config_file_parser_class=YAMLConfigFileParserIgnoreProfiles, auto_env_var_prefix="AIDER_", ) # List of valid edit formats for argparse validation & shtab completion. @@ -129,6 +142,11 @@ def get_parser(default_config_files, git_root): default=".aider.model.metadata.json", help="Specify a file with context window and costs for unknown models", ).complete = shtab.FILE + group.add_argument( + "--profile", + metavar="PROFILE", + help="Include settings from a named profile in the 'profiles' section of the config", + ) group.add_argument( "--alias", action="append", diff --git a/aider/main.py b/aider/main.py index afb3f836624..fa3b6b4609b 100644 --- a/aider/main.py +++ b/aider/main.py @@ -15,6 +15,7 @@ import importlib_resources import shtab +import yaml from dotenv import load_dotenv from prompt_toolkit.enums import EditingMode @@ -332,6 +333,16 @@ def generate_search_path_list(default_file, git_root, command_line_file): return files +def load_profiles_from_config_files(config_files): + profiles = {} + for config_file in config_files: + if Path(config_file).exists(): + with open(config_file, "r") as f: + config = yaml.safe_load(f) or {} + profiles.update(config.get("profiles", {})) + return profiles + + def register_models(git_root, model_settings_fname, io, verbose=False): model_settings_files = generate_search_path_list( ".aider.model.settings.yml", git_root, model_settings_fname @@ -497,6 +508,31 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F args, unknown = parser.parse_known_args(argv) + # Include config from --profile + # - Do this early so profiles can contain settings like --env-file, and cli args override + # profile settings + if args.profile is not None: + profiles = load_profiles_from_config_files(default_config_files) + profile = profiles.get(args.profile) + if not profile: + print(f"Profile not found: {args.profile}", file=sys.stderr) + print(f"Available profiles: {", ".join(profiles.keys())}", file=sys.stderr) + return 1 + if args.verbose: + print(f"Using profile: {args.profile}") + profile_argv = [] + for k, v in profile.items(): + if v is None: + v = "" + if isinstance(v, bool): + profile_argv.append(f"--{k if v else f'no-{k}'}") + else: + profile_argv.append(f"--{k}={v}") + # Inject profile settings into argv (used here and below) + argv = profile_argv + argv + # Parse again with settings from profile + args, unknown = parser.parse_known_args(argv) + # Load the .env file specified in the arguments loaded_dotenvs = load_dotenv_files(git_root, args.env_file, args.encoding)