From f2b1c41cdf83a9d8aec031523c4408634c6d2ae3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Saugat=20Pachhai=20=28=E0=A4=B8=E0=A5=8C=E0=A4=97=E0=A4=BE?= =?UTF-8?q?=E0=A4=A4=29?= Date: Tue, 7 Oct 2025 14:34:39 +0545 Subject: [PATCH] fix remote config completion --- dvc/cli/completion.py | 67 ++++++++++++++++++++++++++++++++++++++---- dvc/commands/config.py | 12 ++++++++ dvc/commands/remote.py | 4 ++- dvc/config_schema.py | 47 ++++++++++++++++++++--------- 4 files changed, 109 insertions(+), 21 deletions(-) diff --git a/dvc/cli/completion.py b/dvc/cli/completion.py index 36568f13b..7106aacd0 100644 --- a/dvc/cli/completion.py +++ b/dvc/cli/completion.py @@ -31,6 +31,27 @@ _dvc_compgen_remotes() { _dvc_compgen_config_vars() { compgen -W "${_dvc_config_vars[*]}" -- $1 } + +_dvc_compgen_remote_config_vars() { + local cur prev words cword remote_name filtered + _init_completion || return + + # last non-option word before cursor + local nonopts=() + for w in "${words[@]:1:$((cword-1))}"; do + [[ $w == -* ]] || nonopts+=("$w") + done + + remote_name="${nonopts[-1]}" + if [[ -n $remote_name ]]; then + local _dvc_remote_config_vars=($( + dvc config --available-options -q 2> /dev/null \ + | grep "^remote\\.${remote_name}\\." \ + | sed "s/^remote\\.${remote_name}\\.//" + )) + compgen -W "${_dvc_remote_config_vars[*]}" -- $1 + fi +} """ ZSH_PREAMBLE = """ @@ -69,9 +90,27 @@ _dvc_compadd_remotes() { _describe 'remotes' "($(dvc remote list | cut -d' ' -f1))" } +_dvc_config_config_vars_for_completion() { + dvc config --available-options -q 2> /dev/null +} + _dvc_compadd_config_vars() { _describe 'config_vars' _dvc_config_vars } + +_dvc_compadd_remote_config_vars() { + local remote_name filtered + + # last non-option word before cursor + remote_name=${${words[1,CURRENT-1]##-*}[-1]} + if [[ -n $remote_name ]]; then + filtered=$(_dvc_config_config_vars_for_completion \\ + | grep "^remote.${remote_name}\\\\." \\ + | sed "s/^remote\\\\.${remote_name}\\\\.//" + ) + compadd -- $=filtered + fi +} """ FISH_PREAMBLE = """ @@ -100,6 +139,17 @@ end function __fish_complete_dvc_remotes dvc remote list | cut -d' ' -f1 end + +function __fish_complete_dvc_remote_config_vars + # last non-option word before cursor + set -l remote_name (commandline -opc | string match -rv '^-' | tail -n 1) + + if test -n "$remote_name" + dvc config --available-options -q 2>/dev/null | + string match -re "^remote\\.$remote_name\\." | + string replace -r "^remote\\.$remote_name\\." '' + end +end """ PREAMBLE = { @@ -113,32 +163,37 @@ DIR = shtab.DIRECTORY DVC_FILE = { "bash": "_dvc_compgen_DVCFiles", "zsh": "_dvc_compadd_DVCFiles", - "fish": "__fish_complete_dvc_files", + "fish": '-f -a "(__fish_complete_dvc_files)"', } STAGE = { "bash": "_dvc_compgen_stages", "zsh": "_dvc_compadd_stages", - "fish": "__fish_complete_dvc_stages", + "fish": '-f -a "(__fish_complete_dvc_stages)"', } DVCFILES_AND_STAGE = { "bash": "_dvc_compgen_stages_and_files", "zsh": "_dvc_compadd_stages_and_files", - "fish": "__fish_complete_dvc_stages_and_files", + "fish": '-f -a "(__fish_complete_dvc_stages_and_files)"', } EXPERIMENT = { "bash": "_dvc_compgen_exps", "zsh": "_dvc_compadd_exps", - "fish": "__fish_complete_dvc_experiments", + "fish": '-f -a "(__fish_complete_dvc_experiments)"', } REMOTE = { "bash": "_dvc_compgen_remotes", "zsh": "_dvc_compadd_remotes", - "fish": "__fish_complete_dvc_remotes", + "fish": '-f -a "(__fish_complete_dvc_remotes)"', } CONFIG_VARS = { "bash": "_dvc_compgen_config_vars", "zsh": "_dvc_compadd_config_vars", - "fish": "__fish_complete_dvc_config_vars", + "fish": '-f -a "(__fish_complete_dvc_config_vars)"', +} +REMOTE_CONFIG_VARS = { + "bash": "_dvc_compgen_remote_config_vars", + "zsh": "_dvc_compadd_remote_config_vars", + "fish": '-f -a "(__fish_complete_dvc_remote_config_vars)"', } diff --git a/dvc/commands/config.py b/dvc/commands/config.py index 70fdd4207..acddb305f 100644 --- a/dvc/commands/config.py +++ b/dvc/commands/config.py @@ -40,6 +40,15 @@ class CmdConfig(CmdBaseNoRepo): self.config = Config.from_cwd(validate=False) def run(self): + if self.args.available_options: + from dvc.config_schema import contextual_config_vars_for_completion + + ui.write( + "\n".join(list(contextual_config_vars_for_completion(self.config))), + force=True, + ) + return 0 + if self.args.show_origin and (self.args.value or self.args.unset): logger.error( "--show-origin can't be used together with any of these " @@ -212,6 +221,9 @@ def add_parser(subparsers, parent_parser): help=CONFIG_HELP, formatter_class=formatter.RawDescriptionHelpFormatter, ) + config_parser.add_argument( + "--available-options", default=False, action="store_true" + ) config_parser.add_argument( "-u", "--unset", diff --git a/dvc/commands/remote.py b/dvc/commands/remote.py index bf727cadf..689bccac0 100644 --- a/dvc/commands/remote.py +++ b/dvc/commands/remote.py @@ -233,7 +233,9 @@ def add_parser(subparsers, parent_parser): remote_modify_parser.add_argument( "name", help="Name of the remote" ).complete = completion.REMOTE - remote_modify_parser.add_argument("option", help="Name of the option to modify.") + remote_modify_parser.add_argument( + "option", help="Name of the option to modify." + ).complete = completion.REMOTE_CONFIG_VARS remote_modify_parser.add_argument( "value", nargs="?", help="(optional) Value of the option." ) diff --git a/dvc/config_schema.py b/dvc/config_schema.py index a4e00ff8a..59844f04d 100644 --- a/dvc/config_schema.py +++ b/dvc/config_schema.py @@ -60,23 +60,27 @@ def Choices(*choices): # noqa: N802 return Any(*choices, msg="expected one of {}".format(", ".join(choices))) +def _get_schema_from_url(url: str) -> str: + parsed = urlparse(url) + # Windows absolute paths should really have scheme == "" (local) + if os.name == "nt" and len(parsed.scheme) == 1 and not parsed.netloc: + return "" + if not parsed.netloc: + return "" + return parsed.scheme + + def ByUrl(mapping): # noqa: N802 - schemas = walk_values(Schema, mapping) + schemas: dict[str, Schema] = walk_values(Schema, mapping) def validate(data): if "url" not in data: raise Invalid("expected 'url'") - parsed = urlparse(data["url"]) - # Windows absolute paths should really have scheme == "" (local) - if os.name == "nt" and len(parsed.scheme) == 1 and not parsed.netloc: - return schemas[""](data) - if not parsed.netloc: - return schemas[""](data) - if parsed.scheme not in schemas: - raise Invalid(f"Unsupported URL type {parsed.scheme}://") - - return schemas[parsed.scheme](data) + scheme = _get_schema_from_url(data["url"]) + if scheme not in schemas: + raise Invalid(f"Unsupported URL type {scheme}://") + return schemas[scheme](data) return validate @@ -396,9 +400,24 @@ def config_vars_for_completion(d: dict = SCHEMA, path: str = "") -> "Iterator[st k = k.schema if not isinstance(k, str): continue - - keypath = path + k + keypath = f"{path}.{k}" if path else k if isinstance(v, dict): - yield from config_vars_for_completion(v, keypath + ".") + yield from config_vars_for_completion(v, keypath) else: yield keypath + + +def contextual_config_vars_for_completion(config) -> "Iterator[str]": + yield from config_vars_for_completion() + for name, data in config["remote"].items(): + if "url" not in data: + continue + scheme = _get_schema_from_url(data["url"]) + if scheme not in REMOTE_SCHEMAS: + continue + schema = REMOTE_SCHEMAS[scheme] + if schema: + yield from config_vars_for_completion(schema, f"remote.{name}") # type: ignore[arg-type] + + for name in config["db"]: + yield from config_vars_for_completion(SCHEMA["db"], f"db.{name}") # type: ignore[arg-type]