env-full / lib /python3.12 /site-packages /hydra /plugins /completion_plugin.py
ZhengyangZhang's picture
Add files using upload-large-folder tool
c3ae1e2 verified
Raw History Blame
11.3 kB
# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
# TODO: Test with /miniconda3/envs/hydra36/bin/python , seems to be running python for some reason.
# TODO: Add tests for completion with +prefix (should suggest config groups that are not listed)
# TODO: Test completion when defaults has a missing mandatory item
import os
import re
import sys
from abc import abstractmethod
from hydra.errors import ConfigCompositionException
from omegaconf import (
Container,
DictConfig,
MissingMandatoryValue,
OmegaConf,
ListConfig,
)
from typing import Any, List, Optional, Tuple
from hydra.core.config_loader import ConfigLoader
from hydra.core.object_type import ObjectType
from hydra.plugins.plugin import Plugin
from hydra.types import RunMode
class CompletionPlugin(Plugin):
def __init__(self, config_loader: ConfigLoader) -> None:
self.config_loader = config_loader
@abstractmethod
def install(self) -> None:
...
@abstractmethod
def uninstall(self) -> None:
...
@staticmethod
@abstractmethod
def provides() -> str:
"""
:return: the name of the shell this plugin provides completion for
"""
...
@abstractmethod
def query(self, config_name: Optional[str]) -> None:
...
@staticmethod
@abstractmethod
def help(command: str) -> str:
"""
:param command: "install" or "uninstall"
:return: command the user can run to install or uninstall this shell completion on the appropriate shell
"""
...
@staticmethod
def _get_filename(filename: str) -> Tuple[Optional[str], Optional[str]]:
last = filename.rfind("=")
if last != -1:
key_eq = filename[0 : last + 1]
filename = filename[last + 1 :]
prefixes = [".", "/", "\\", "./", ".\\"]
if sys.platform.startswith("win"):
for drive in range(ord("a"), ord("z")):
prefixes.append(f"{chr(drive)}:")
if not filename:
return None, None
for prefix in prefixes:
if filename.lower().startswith(prefix):
return key_eq, filename
return None, None
@staticmethod
def complete_files(word: str) -> List[str]:
if os.path.isdir(word):
dirname = word
files = os.listdir(word)
file_prefix = ""
else:
dirname = os.path.dirname(word)
if os.path.isdir(dirname):
files = os.listdir(dirname)
else:
files = []
file_prefix = os.path.basename(word)
ret = []
for file in files:
if file.startswith(file_prefix):
ret.append(os.path.join(dirname, file))
return ret
@staticmethod
def _get_matches(config: Container, word: str) -> List[str]:
def str_rep(in_key: Any, in_value: Any) -> str:
if OmegaConf.is_config(in_value):
return f"{in_key}."
else:
return f"{in_key}="
if config is None:
return []
elif OmegaConf.is_config(config):
matches = []
if word.endswith(".") or word.endswith("="):
exact_key = word[0:-1]
try:
conf_node = OmegaConf.select(
config, exact_key, throw_on_missing=True
)
except MissingMandatoryValue:
conf_node = ""
if conf_node is not None:
if OmegaConf.is_config(conf_node):
key_matches = CompletionPlugin._get_matches(conf_node, "")
else:
# primitive
if isinstance(conf_node, bool):
conf_node = str(conf_node).lower()
key_matches = [conf_node]
else:
key_matches = []
matches.extend([f"{word}{match}" for match in key_matches])
else:
last_dot = word.rfind(".")
if last_dot != -1:
base_key = word[0:last_dot]
partial_key = word[last_dot + 1 :]
conf_node = OmegaConf.select(config, base_key)
key_matches = CompletionPlugin._get_matches(conf_node, partial_key)
matches.extend([f"{base_key}.{match}" for match in key_matches])
else:
if isinstance(config, DictConfig):
for key, value in config.items_ex(resolve=False):
str_key = str(key)
if str_key.startswith(word):
matches.append(str_rep(key, value))
elif OmegaConf.is_list(config):
assert isinstance(config, ListConfig)
for idx in range(len(config)):
try:
value = config[idx]
if str(idx).startswith(word):
matches.append(str_rep(idx, value))
except MissingMandatoryValue:
matches.append(str_rep(idx, ""))
else:
assert False, f"Object is not an instance of config : {type(config)}"
return matches
def _query_config_groups(
self, word: str, config_name: Optional[str], words: List[str]
) -> Tuple[List[str], bool]:
is_addition = word.startswith("+")
is_deletion = word.startswith("~")
if is_addition or is_deletion:
prefix, word = word[0], word[1:]
else:
prefix = ""
last_eq_index = word.rfind("=")
last_slash_index = word.rfind("/")
exact_match: bool = False
if last_eq_index != -1:
parent_group = word[0:last_eq_index]
results_filter = ObjectType.CONFIG
else:
results_filter = ObjectType.GROUP
if last_slash_index == -1:
parent_group = ""
else:
parent_group = word[0:last_slash_index]
all_matched_groups = self.config_loader.get_group_options(
group_name=parent_group,
results_filter=results_filter,
config_name=config_name,
overrides=words,
)
matched_groups: List[str] = []
if results_filter == ObjectType.CONFIG:
for match in all_matched_groups:
name = f"{parent_group}={match}" if parent_group != "" else match
if name.startswith(word):
matched_groups.append(name)
exact_match = True
elif results_filter == ObjectType.GROUP:
for match in all_matched_groups:
name = f"{parent_group}/{match}" if parent_group != "" else match
if name.startswith(word):
files = self.config_loader.get_group_options(
group_name=name,
results_filter=ObjectType.CONFIG,
config_name=config_name,
overrides=words,
)
dirs = self.config_loader.get_group_options(
group_name=name,
results_filter=ObjectType.GROUP,
config_name=config_name,
overrides=words,
)
if len(dirs) == 0 and len(files) > 0 and not is_deletion:
name = name + "="
elif len(dirs) > 0 and len(files) == 0:
name = name + "/"
matched_groups.append(name)
matched_groups = [f"{prefix}{group}" for group in matched_groups]
return matched_groups, exact_match
def _query(self, config_name: Optional[str], line: str) -> List[str]:
from .._internal.utils import get_args
new_word = len(line) == 0 or line[-1] == " "
parsed_args = get_args(line.split())
words = parsed_args.overrides
if new_word or len(words) == 0:
word = ""
else:
word = words[-1]
words = words[0:-1]
fname_prefix, filename = CompletionPlugin._get_filename(word)
if filename is not None:
assert fname_prefix is not None
result = CompletionPlugin.complete_files(filename)
result = [fname_prefix + file for file in result]
else:
matched_groups, exact_match = self._query_config_groups(
word, config_name=config_name, words=words
)
config_matches: List[str] = []
if not exact_match:
run_mode = RunMode.MULTIRUN if parsed_args.multirun else RunMode.RUN
config_matches = []
try:
config = self.config_loader.load_configuration(
config_name=config_name, overrides=words, run_mode=run_mode
)
config_matches = CompletionPlugin._get_matches(config, word)
except ConfigCompositionException:
# if config fails to load for whatever reason, do not provide config matches.
# possible reasons:
# - missing entry in defaults list (- group: ???) and not populated in command line
# - a config file is not found
# etc.
pass
result = list(set(matched_groups + config_matches))
return sorted(result)
@staticmethod
def strip_python_or_app_name(line: str) -> str:
"""
Take the command line received from shell completion, and strip the app name from it
which could be at the form of python script.py or some_app.
it also corrects the key (COMP_INDEX) to reflect the same location in the striped command line.
:param line: input line, may contain python file.py followed=by_args..
:return: tuple(args line, key of cursor in args line)
"""
python_args = r"^\s*[\w\/]*python[3]?\s*[\w/\.]*\s*(.*)"
app_args = r"^\s*[\w_\-=\./]+\s*(.*)"
match = re.match(python_args, line)
if match:
return match.group(1)
else:
match = re.match(app_args, line)
if match:
return match.group(1)
else:
raise RuntimeError(f"Error parsing line '{line}'")
class DefaultCompletionPlugin(CompletionPlugin):
"""
A concrete instance of CompletionPlugin that is used for testing.
"""
def install(self) -> None:
raise NotImplementedError
def uninstall(self) -> None:
raise NotImplementedError
@staticmethod
def provides() -> str:
raise NotImplementedError
def query(self, config_name: Optional[str]) -> None:
raise NotImplementedError
@staticmethod
def help(command: str) -> str:
raise NotImplementedError