def test_word_completer_ignore_case(): completer = WordCompleter(['abc', 'def', 'aaa'], ignore_case=True) completions = completer.get_completions(Document('a'), CompleteEvent()) assert [c.text for c in completions] == ['abc', 'aaa'] completions = completer.get_completions(Document('A'), CompleteEvent()) assert [c.text for c in completions] == ['abc', 'aaa']
class CommandCompleter(Completer): """ Completer for command names. """ def __init__(self): # Completer for full command names. self._command_completer = WordCompleter( sorted(COMMANDS_TO_HANDLERS.keys()), ignore_case=True, WORD=True, match_middle=True) # Completer for aliases. self._aliases_completer = WordCompleter( sorted(ALIASES.keys()), ignore_case=True, WORD=True, match_middle=True) def get_completions(self, document, complete_event): # First, complete on full command names. found = False for c in self._command_completer.get_completions(document, complete_event): found = True yield c # When no matches are found, complete aliases instead. # The completion however, inserts the full name. if not found: for c in self._aliases_completer.get_completions(document, complete_event): full_name = ALIASES.get(c.display) yield Completion(full_name, start_position=c.start_position, display='%s (%s)' % (c.display, full_name))
def test_word_completer_sentence(): # With sentence=True completer = WordCompleter(['hello world', 'www', 'hello www', 'hello there'], sentence=True) completions = completer.get_completions(Document('hello w'), CompleteEvent()) assert [c.text for c in completions] == ['hello world', 'hello www'] # With sentence=False completer = WordCompleter(['hello world', 'www', 'hello www', 'hello there'], sentence=False) completions = completer.get_completions(Document('hello w'), CompleteEvent()) assert [c.text for c in completions] == ['www']
def test_word_completer_pattern(): # With a pattern which support '.' completer = WordCompleter(['abc', 'a.b.c', 'a.b', 'xyz'], pattern=re.compile(r'^([a-zA-Z0-9_.]+|[^a-zA-Z0-9_.\s]+)')) completions = completer.get_completions(Document('a.'), CompleteEvent()) assert [c.text for c in completions] == ['a.b.c', 'a.b'] # Without pattern completer = WordCompleter(['abc', 'a.b.c', 'a.b', 'xyz']) completions = completer.get_completions(Document('a.'), CompleteEvent()) assert [c.text for c in completions] == []
def test_word_completer_static_word_list(): completer = WordCompleter(['abc', 'def', 'aaa']) # Static list on empty input. completions = completer.get_completions(Document(''), CompleteEvent()) assert [c.text for c in completions] == ['abc', 'def', 'aaa'] # Static list on non-empty input. completions = completer.get_completions(Document('a'), CompleteEvent()) assert [c.text for c in completions] == ['abc', 'aaa'] completions = completer.get_completions(Document('A'), CompleteEvent()) assert [c.text for c in completions] == [] # Multiple words. (Check last only.) completions = completer.get_completions(Document('test a'), CompleteEvent()) assert [c.text for c in completions] == ['abc', 'aaa']
def test_word_completer_dynamic_word_list(): called = [0] def get_words(): called[0] += 1 return ['abc', 'def', 'aaa'] completer = WordCompleter(get_words) # Dynamic list on empty input. completions = completer.get_completions(Document(''), CompleteEvent()) assert [c.text for c in completions] == ['abc', 'def', 'aaa'] assert called[0] == 1 # Static list on non-empty input. completions = completer.get_completions(Document('a'), CompleteEvent()) assert [c.text for c in completions] == ['abc', 'aaa'] assert called[0] == 2
def get_completions_for_parts(parts, last_part, complete_event, pymux): completer = None # Resolve aliases. if len(parts) > 0: parts = [ALIASES.get(parts[0], parts[0])] + parts[1:] if len(parts) == 0: # New command. completer = _command_completer elif len(parts) >= 1 and last_part.startswith('-'): flags = get_option_flags_for_command(parts[0]) completer = WordCompleter(sorted(flags), WORD=True) elif len(parts) == 1 and parts[0] in ('set-option', 'set-window-option'): options = pymux.options if parts[0] == 'set-option' else pymux.window_options completer = WordCompleter(sorted(options.keys()), sentence=True) elif len(parts) == 2 and parts[0] in ('set-option', 'set-window-option'): options = pymux.options if parts[0] == 'set-option' else pymux.window_options option = options.get(parts[1]) if option: completer = WordCompleter(sorted(option.get_all_values(pymux)), sentence=True) elif len(parts) == 1 and parts[0] == 'select-layout': completer = _layout_type_completer elif len(parts) == 1 and parts[0] == 'send-keys': completer = _keys_completer elif parts[0] == 'bind-key': if len(parts) == 1: completer = _keys_completer elif len(parts) == 2: completer = _command_completer # Recursive, for bind-key options. if parts and parts[0] == 'bind-key' and len(parts) > 2: for c in get_completions_for_parts(parts[2:], last_part, complete_event, pymux): yield c if completer: for c in completer.get_completions(Document(last_part), complete_event): yield c
def test_word_completer_match_middle(): completer = WordCompleter(['abc', 'def', 'abca'], match_middle=True) completions = completer.get_completions(Document('bc'), CompleteEvent()) assert [c.text for c in completions] == ['abc', 'abca']
class CommandsRegistry(object): """ A registry that holds all commands implementations and creates a quick access point for resolving a command string into the corresponding handling object """ def __init__(self, parser, listeners): self._completer = WordCompleter([], ignore_case=True, sentence=True) # maps a command to Command Instance self._cmd_instance_map = {} # objects interested in receiving messages self._listeners = [] # argparser so each command can add its options self._parser = parser for lst in listeners: self.register_listener(lst(self)) def _make_human_suggestion(self, suggestions): if len(suggestions) == 1: return suggestions[0] human_string = ", ".join(suggestions[:-1]) return human_string + " or {}".format(suggestions[-1]) def register_command(self, cmd_instance, override=False): if not isinstance(cmd_instance, Command): raise TypeError("Invalid command instance, must be an instance of " "subclass of Command") cmd_instance.set_command_registry(self) cmd_keys = cmd_instance.get_command_names() for cmd in cmd_keys: if not cmd_instance.get_help(cmd): cprint( ("[WARNING] The command {} will not be loaded. " "Please provide a help message by either defining a " "docstring or filling the help argument in the " "@command annotation").format(cmd_keys[0]), "red", ) return None cmd_instance.add_arguments(self._parser) if not override: conflicts = [ cmd for cmd in cmd_keys if cmd in self._cmd_instance_map ] if conflicts: raise ValueError("Some other command instance has registered " "the name(s) {}".format(conflicts)) if isinstance(cmd_instance, Listener): self._listeners.append(cmd_instance) for cmd in cmd_keys: self._cmd_instance_map[cmd.lower()] = cmd_instance if cmd not in self._completer.words: self._completer.words.append(cmd) self._completer.meta_dict[cmd] = cmd_instance.get_help(cmd) aliases = cmd_instance.get_cli_aliases() for alias in aliases: self._cmd_instance_map[alias.lower()] = cmd_instance def register_priority_listener(self, instance): """ Registers a listener that get the top priority in callbacks """ if not isinstance(instance, Listener): raise TypeError("Only Listeners can be registered") self._listeners.insert(0, instance) def register_listener(self, instance): if not isinstance(instance, Listener): raise TypeError("Only Listeners can be registered") self._listeners.append(instance) def __contains__(self, cmd): return cmd.lower() in self._cmd_instance_map def get_completer(self): return self._completer def get_all_commands(self): return set(self._cmd_instance_map.values()) def find_command(self, cmd): return self._cmd_instance_map.get(cmd.lower()) def find_approx(self, cmd): """Finds the closest command to the passed cmd, this is used in case we cannot find an exact match for the cmd """ suggestions = [] for c in self._cmd_instance_map.keys(): dist = Levenshtein.distance(str(cmd), str(c)) if dist <= 2: suggestions.append(c) if not suggestions: return "" return " Did you mean `{}`?".format( self._make_human_suggestion(suggestions)) def get_completions(self, document, complete_event): return self._completer.get_completions(document, complete_event) def dispatch_message(self, msg, *args, **kwargs): for mod in self._listeners: mod.react(msg, *args, **kwargs) def set_cli_args(self, args): self._args = args def get_cli_arg(self, arg): return getattr(self._args, arg, None)
class HummingbotCompleter(Completer): def __init__(self, hummingbot_application): super(HummingbotCompleter, self).__init__() self.hummingbot_application = hummingbot_application self._path_completer = WordCompleter( file_name_list(CONF_FILE_PATH, "yml")) self._command_completer = WordCompleter(self.parser.commands, ignore_case=True) self._exchange_completer = WordCompleter(sorted( CONNECTOR_SETTINGS.keys()), ignore_case=True) self._spot_completer = WordCompleter(sorted( EXCHANGES.union(SPOT_PROTOCOL_CONNECTOR)), ignore_case=True) self._spot_exchange_completer = WordCompleter(sorted(EXCHANGES), ignore_case=True) self._derivative_completer = WordCompleter(DERIVATIVES, ignore_case=True) self._derivative_exchange_completer = WordCompleter( DERIVATIVES.difference(DERIVATIVE_PROTOCOL_CONNECTOR), ignore_case=True) self._connect_option_completer = WordCompleter(CONNECT_OPTIONS, ignore_case=True) self._export_completer = WordCompleter(["keys", "trades"], ignore_case=True) self._balance_completer = WordCompleter(["limit", "paper"], ignore_case=True) self._history_completer = WordCompleter( ["--days", "--verbose", "--precision"], ignore_case=True) self._strategy_completer = WordCompleter(STRATEGIES, ignore_case=True) self._py_file_completer = WordCompleter( file_name_list(SCRIPTS_PATH, "py")) self._rate_oracle_completer = WordCompleter( [r.name for r in RateOracleSource], ignore_case=True) @property def prompt_text(self) -> str: return self.hummingbot_application.app.prompt_text @property def parser(self) -> ThrowingArgumentParser: return self.hummingbot_application.parser def get_subcommand_completer(self, first_word: str) -> Completer: subcommands: List[str] = self.parser.subcommands_from(first_word) return WordCompleter(subcommands, ignore_case=True) @property def _trading_pair_completer(self) -> Completer: trading_pair_fetcher = TradingPairFetcher.get_instance() market = "" for exchange in sorted(list(CONNECTOR_SETTINGS.keys()), key=len, reverse=True): if exchange in self.prompt_text: market = exchange break trading_pairs = trading_pair_fetcher.trading_pairs.get( market, []) if trading_pair_fetcher.ready and market else [] return WordCompleter(trading_pairs, ignore_case=True, sentence=True) @property def _wallet_address_completer(self): return WordCompleter(list_wallets(), ignore_case=True) @property def _option_completer(self): outer = re.compile(r"\((.+)\)") inner_str = outer.search(self.prompt_text).group(1) options = inner_str.split("/") if "/" in inner_str else [] return WordCompleter(options, ignore_case=True) @property def _config_completer(self): config_keys = self.hummingbot_application.config_able_keys() return WordCompleter(config_keys, ignore_case=True) def _complete_strategies(self, document: Document) -> bool: return "strategy" in self.prompt_text and "strategy file" not in self.prompt_text def _complete_script_files(self, document: Document) -> bool: return "script file" in self.prompt_text def _complete_configs(self, document: Document) -> bool: text_before_cursor: str = document.text_before_cursor return "config" in text_before_cursor def _complete_options(self, document: Document) -> bool: return "(" in self.prompt_text and ")" in self.prompt_text and "/" in self.prompt_text def _complete_exchanges(self, document: Document) -> bool: return any(x for x in ("exchange name", "name of exchange", "name of the exchange") if x in self.prompt_text.lower()) def _complete_derivatives(self, document: Document) -> bool: text_before_cursor: str = document.text_before_cursor return "perpetual" in text_before_cursor or \ any(x for x in ("derivative connector", "derivative name", "name of derivative", "name of the derivative") if x in self.prompt_text.lower()) def _complete_connect_options(self, document: Document) -> bool: text_before_cursor: str = document.text_before_cursor return text_before_cursor.startswith("connect ") def _complete_spot_connectors(self, document: Document) -> bool: return "spot" in self.prompt_text def _complete_export_options(self, document: Document) -> bool: text_before_cursor: str = document.text_before_cursor return "export" in text_before_cursor def _complete_balance_options(self, document: Document) -> bool: text_before_cursor: str = document.text_before_cursor return text_before_cursor.startswith("balance ") def _complete_history_arguments(self, document: Document) -> bool: text_before_cursor: str = document.text_before_cursor return text_before_cursor.startswith("history ") def _complete_trading_pairs(self, document: Document) -> bool: return "trading pair" in self.prompt_text def _complete_paths(self, document: Document) -> bool: text_before_cursor: str = document.text_before_cursor return (("path" in self.prompt_text and "file" in self.prompt_text) or "import" in text_before_cursor) def _complete_wallet_addresses(self, document: Document) -> bool: return "Which wallet" in self.prompt_text def _complete_command(self, document: Document) -> bool: text_before_cursor: str = document.text_before_cursor return " " not in text_before_cursor and len( self.prompt_text.replace(">>> ", "")) == 0 def _complete_subcommand(self, document: Document) -> bool: text_before_cursor: str = document.text_before_cursor index: int = text_before_cursor.index(' ') return text_before_cursor[0:index] in self.parser.commands def _complete_balance_limit_exchanges(self, document: Document): text_before_cursor: str = document.text_before_cursor command_args = text_before_cursor.split(" ") return len(command_args) == 3 and command_args[ 0] == "balance" and command_args[1] == "limit" def _complete_rate_oracle_source(self, document: Document): return all(x in self.prompt_text for x in ("source", "rate oracle")) def get_completions(self, document: Document, complete_event: CompleteEvent): """ Get completions for the current scope. This is the defining function for the completer :param document: :param complete_event: """ if self._complete_script_files(document): for c in self._py_file_completer.get_completions( document, complete_event): yield c elif self._complete_paths(document): for c in self._path_completer.get_completions( document, complete_event): yield c elif self._complete_strategies(document): for c in self._strategy_completer.get_completions( document, complete_event): yield c elif self._complete_wallet_addresses(document): for c in self._wallet_address_completer.get_completions( document, complete_event): yield c elif self._complete_spot_connectors(document): if "(Exchange/AMM)" in self.prompt_text: for c in self._spot_completer.get_completions( document, complete_event): yield c else: for c in self._spot_exchange_completer.get_completions( document, complete_event): yield c elif self._complete_connect_options(document): for c in self._connect_option_completer.get_completions( document, complete_event): yield c elif self._complete_export_options(document): for c in self._export_completer.get_completions( document, complete_event): yield c elif self._complete_balance_limit_exchanges(document): for c in self._connect_option_completer.get_completions( document, complete_event): yield c elif self._complete_balance_options(document): for c in self._balance_completer.get_completions( document, complete_event): yield c elif self._complete_history_arguments(document): for c in self._history_completer.get_completions( document, complete_event): yield c elif self._complete_derivatives(document): if "(Exchange/AMM)" in self.prompt_text: for c in self._derivative_completer.get_completions( document, complete_event): yield c else: for c in self._derivative_exchange_completer.get_completions( document, complete_event): yield c elif self._complete_exchanges(document): for c in self._exchange_completer.get_completions( document, complete_event): yield c elif self._complete_trading_pairs(document): for c in self._trading_pair_completer.get_completions( document, complete_event): yield c elif self._complete_command(document): for c in self._command_completer.get_completions( document, complete_event): yield c elif self._complete_configs(document): for c in self._config_completer.get_completions( document, complete_event): yield c elif self._complete_options(document): for c in self._option_completer.get_completions( document, complete_event): yield c elif self._complete_rate_oracle_source(document): for c in self._rate_oracle_completer.get_completions( document, complete_event): yield c else: text_before_cursor: str = document.text_before_cursor try: first_word: str = text_before_cursor[0:text_before_cursor. index(' ')] except ValueError: return subcommand_completer: Completer = self.get_subcommand_completer( first_word) if complete_event.completion_requested or self._complete_subcommand( document): for c in subcommand_completer.get_completions( document, complete_event): yield c
class HummingbotCompleter(Completer): def __init__(self, hummingbot_application): super(HummingbotCompleter, self).__init__() self.hummingbot_application = hummingbot_application # static completers self._path_completer = PathCompleter(get_paths=lambda: [f"./{CONF_FILE_PATH}"], file_filter=lambda fname: fname.endswith(".yml")) self._command_completer = WordCompleter(self.parser.commands, ignore_case=True) self._exchange_completer = WordCompleter(EXCHANGES, ignore_case=True) self._strategy_completer = WordCompleter(STRATEGIES, ignore_case=True) @property def prompt_text(self) -> str: return self.hummingbot_application.app.prompt_text @property def parser(self) -> ThrowingArgumentParser: return self.hummingbot_application.parser def get_subcommand_completer(self, first_word: str) -> Completer: subcommands: List[str] = self.parser.subcommands_from(first_word) return WordCompleter(subcommands, ignore_case=True) @property def _trading_pair_completer(self) -> Completer: trading_pair_fetcher = TradingPairFetcher.get_instance() market = None for exchange in EXCHANGES: if exchange in self.prompt_text: market = exchange break trading_pairs = trading_pair_fetcher.trading_pairs.get(market, []) if trading_pair_fetcher.ready else [] return WordCompleter(trading_pairs, ignore_case=True) @property def _wallet_address_completer(self): return WordCompleter(list_wallets(), ignore_case=True) @property def _option_completer(self): outer = re.compile(r"\((.+)\)") inner_str = outer.search(self.prompt_text).group(1) options = inner_str.split("/") if "/" in inner_str else [] return WordCompleter(options, ignore_case=True) @property def _config_completer(self): return WordCompleter(self.hummingbot_application.get_all_available_config_keys(), ignore_case=True) def _complete_strategies(self, document: Document) -> bool: return "strategy" in self.prompt_text def _complete_configs(self, document: Document) -> bool: text_before_cursor: str = document.text_before_cursor return "config" in text_before_cursor def _complete_options(self, document: Document) -> bool: return "(" in self.prompt_text and ")" in self.prompt_text and "/" in self.prompt_text def _complete_exchanges(self, document: Document) -> bool: text_before_cursor: str = document.text_before_cursor return "-e" in text_before_cursor or \ "--exchange" in text_before_cursor or \ "exchange" in self.prompt_text def _complete_trading_pairs(self, document: Document) -> bool: return "trading_pair" in self.prompt_text def _complete_paths(self, document: Document) -> bool: return "path" in self.prompt_text and "file" in self.prompt_text def _complete_wallet_addresses(self, document: Document) -> bool: return "Which wallet" in self.prompt_text def _complete_command(self, document: Document) -> bool: text_before_cursor: str = document.text_before_cursor return " " not in text_before_cursor and len(self.prompt_text.replace(">>> ", "")) == 0 def _complete_subcommand(self, document: Document) -> bool: text_before_cursor: str = document.text_before_cursor index: int = text_before_cursor.index(' ') return text_before_cursor[0:index] in self.parser.commands def get_completions(self, document: Document, complete_event: CompleteEvent): """ Get completions for the current scope. This is the defining function for the completer :param document: :param complete_event: """ if self._complete_paths(document): for c in self._path_completer.get_completions(document, complete_event): yield c return if self._complete_strategies(document): for c in self._strategy_completer.get_completions(document, complete_event): yield c if self._complete_wallet_addresses(document): for c in self._wallet_address_completer.get_completions(document, complete_event): yield c elif self._complete_exchanges(document): for c in self._exchange_completer.get_completions(document, complete_event): yield c elif self._complete_trading_pairs(document): for c in self._trading_pair_completer.get_completions(document, complete_event): yield c elif self._complete_command(document): for c in self._command_completer.get_completions(document, complete_event): yield c elif self._complete_configs(document): for c in self._config_completer.get_completions(document, complete_event): yield c elif self._complete_options(document): for c in self._option_completer.get_completions(document, complete_event): yield c else: text_before_cursor: str = document.text_before_cursor first_word: str = text_before_cursor[0:text_before_cursor.index(' ')] subcommand_completer: Completer = self.get_subcommand_completer(first_word) if complete_event.completion_requested or self._complete_subcommand(document): for c in subcommand_completer.get_completions(document, complete_event): yield c
def test_word_completer_match_middle(): completer = WordCompleter(["abc", "def", "abca"], match_middle=True) completions = completer.get_completions(Document("bc"), CompleteEvent()) assert [c.text for c in completions] == ["abc", "abca"]
class HummingbotCompleter(Completer): def __init__(self, hummingbot_application): super(HummingbotCompleter, self).__init__() self.hummingbot_application = hummingbot_application self._symbols: Dict[str, List[str]] = {} # static completers self._path_completer = PathCompleter() self._command_completer = WordCompleter(self.parser.commands, ignore_case=True) self._exchange_completer = WordCompleter(EXCHANGES, ignore_case=True) self._strategy_completer = WordCompleter(STRATEGIES, ignore_case=True) asyncio.ensure_future(self._fetch_symbols()) async def _fetch_symbols(self): self._symbols: Dict[str, List[str]] = await fetch_all() @property def prompt_text(self) -> str: return self.hummingbot_application.app.prompt_text @property def parser(self) -> ThrowingArgumentParser: return self.hummingbot_application.parser def get_subcommand_completer(self, first_word: str) -> Completer: subcommands: List[str] = self.parser.subcommands_from(first_word) return WordCompleter(subcommands, ignore_case=True) @property def _symbol_completer(self) -> Completer: market = None for exchange in EXCHANGES: if exchange in self.prompt_text: market = exchange break return WordCompleter(self._symbols.get(market) or [], ignore_case=True) def _complete_strategies(self, document: Document) -> bool: return "strategy" in self.prompt_text def _complete_exchanges(self, document: Document) -> bool: text_before_cursor: str = document.text_before_cursor return "-e" in text_before_cursor or \ "--exchange" in text_before_cursor or \ "exchange" in self.prompt_text def _complete_symbols(self, document: Document) -> bool: return "symbol" in self.prompt_text def _complete_paths(self, document: Document) -> bool: return "path" in self.prompt_text and "file" in self.prompt_text def _complete_command(self, document: Document) -> bool: text_before_cursor: str = document.text_before_cursor return " " not in text_before_cursor and len( self.prompt_text.replace(">>> ", "")) == 0 def _complete_subcommand(self, document: Document) -> bool: text_before_cursor: str = document.text_before_cursor index: int = text_before_cursor.index(' ') return text_before_cursor[0:index] in self.parser.commands def get_completions(self, document: Document, complete_event: CompleteEvent): """ Get completions for the current scope. This is the defining function for the completer :param document: :param complete_event: """ if self._complete_paths(document): for c in self._path_completer.get_completions( document, complete_event): yield c if self._complete_strategies(document): for c in self._strategy_completer.get_completions( document, complete_event): yield c elif self._complete_exchanges(document): for c in self._exchange_completer.get_completions( document, complete_event): yield c elif self._complete_symbols(document): for c in self._symbol_completer.get_completions( document, complete_event): yield c elif self._complete_command(document): for c in self._command_completer.get_completions( document, complete_event): yield c else: text_before_cursor: str = document.text_before_cursor first_word: str = text_before_cursor[0:text_before_cursor.index(' ' )] subcommand_completer: Completer = self.get_subcommand_completer( first_word) if complete_event.completion_requested or self._complete_subcommand( document): for c in subcommand_completer.get_completions( document, complete_event): yield c
def get_completions(self, document: Document, complete_event: CompleteEvent) -> Iterable[Completion]: """ Get a list of completions for the given document """ text = document.text_before_cursor.lstrip() try: args = shlex.split(text) except ValueError: try: args = shlex.split(text + '"') except ValueError: args = shlex.split(text + "'") # We haven't finished typing the command. Use our word completer for # commands if text == "" or (len(args) == 1 and not text.endswith(" ")): yield from self.completer.get_completions(document, complete_event) return # Not in a known command, can't autocomplete if args[0] not in self.layers: return command = self.layers[args[0]] args = args[1:] next_completer = command[0] this_completer = command[0] positional = 0 # state = "options", completing options next # state = "arguments", completing arguments to options next state = "options" for arg in args: if state == "options": # Flag options if arg.startswith("-"): # Exact match, with a sub-completer if arg in command[2] and command[2][arg] is not None: # Completer for next argument next_completer = command[2][arg] state = "arguments" # Exact match, with no arguments elif arg in command[2]: # Command has no argument, next completer is options # completer next_completer = command[0] state = "options" this_completer = command[0] # Non-exact match else: next_completer = command[0] this_completer = command[0] state = "options" # Appears to be a positional argument, grab next positional # completer and increment positional count else: if positional < len(command[1]): this_completer = command[1][positional] next_completer = command[0] state = "options" positional += 1 else: this_completer = command[0] next_completer = command[0] state = "options" else: # Completing an argument to a option/switch. We can't verify # it's legitimacy, so we assume it's right, and reset to a # default state. state = "options" this_completer = next_completer next_completer = command[0] if isinstance(this_completer, tuple) and this_completer[0] == "choices": this_completer = WordCompleter(this_completer[1]) if isinstance(next_completer, tuple) and next_completer[0] == "choices": next_completer = WordCompleter(next_completer[1]) if text.endswith(" "): yield from next_completer.get_completions(document, complete_event) else: yield from this_completer.get_completions(document, complete_event)
class CommandCompleter(Completer): """ Complete commands from a given list of commands """ def __init__(self, commands: List["CommandDefinition"]): """ Construct a new command completer """ self.layers = {} local_file_completer = LocalPathCompleter() remote_file_completer = RemotePathCompleter() for command in commands: self.layers[command.PROG] = [None, [], {}] option_names = [] if command.ARGS is not None: for name_list, param in command.ARGS.items(): name_list = name_list.split(",") if param.complete == Complete.CHOICES: completer = ("choices", param.kwargs["choices"]) elif param.complete == Complete.LOCAL_FILE: completer = local_file_completer elif param.complete == Complete.REMOTE_FILE: completer = remote_file_completer elif param.complete == Complete.NONE: completer = None if len(name_list ) == 1 and not name_list[0].startswith("-"): self.layers[command.PROG][1].append(completer) else: for name in name_list: self.layers[command.PROG][2][name] = completer option_names.append(name) self.layers[command.PROG][0] = WordCompleter(option_names + ["--help", "-h"]) self.completer = WordCompleter(list(self.layers)) def get_completions(self, document: Document, complete_event: CompleteEvent) -> Iterable[Completion]: """ Get a list of completions for the given document """ text = document.text_before_cursor.lstrip() try: args = shlex.split(text) except ValueError: try: args = shlex.split(text + '"') except ValueError: args = shlex.split(text + "'") # We haven't finished typing the command. Use our word completer for # commands if text == "" or (len(args) == 1 and not text.endswith(" ")): yield from self.completer.get_completions(document, complete_event) return # Not in a known command, can't autocomplete if args[0] not in self.layers: return command = self.layers[args[0]] args = args[1:] next_completer = command[0] this_completer = command[0] positional = 0 # state = "options", completing options next # state = "arguments", completing arguments to options next state = "options" for arg in args: if state == "options": # Flag options if arg.startswith("-"): # Exact match, with a sub-completer if arg in command[2] and command[2][arg] is not None: # Completer for next argument next_completer = command[2][arg] state = "arguments" # Exact match, with no arguments elif arg in command[2]: # Command has no argument, next completer is options # completer next_completer = command[0] state = "options" this_completer = command[0] # Non-exact match else: next_completer = command[0] this_completer = command[0] state = "options" # Appears to be a positional argument, grab next positional # completer and increment positional count else: if positional < len(command[1]): this_completer = command[1][positional] next_completer = command[0] state = "options" positional += 1 else: this_completer = command[0] next_completer = command[0] state = "options" else: # Completing an argument to a option/switch. We can't verify # it's legitimacy, so we assume it's right, and reset to a # default state. state = "options" this_completer = next_completer next_completer = command[0] if isinstance(this_completer, tuple) and this_completer[0] == "choices": this_completer = WordCompleter(this_completer[1]) if isinstance(next_completer, tuple) and next_completer[0] == "choices": next_completer = WordCompleter(next_completer[1]) if text.endswith(" "): yield from next_completer.get_completions(document, complete_event) else: yield from this_completer.get_completions(document, complete_event)
class HummingbotCompleter(Completer): def __init__(self, hummingbot_application): super(HummingbotCompleter, self).__init__() self.hummingbot_application = hummingbot_application self._path_completer = WordCompleter(file_name_list(str(STRATEGIES_CONF_DIR_PATH), "yml")) self._command_completer = WordCompleter(self.parser.commands, ignore_case=True) self._exchange_completer = WordCompleter(sorted(AllConnectorSettings.get_connector_settings().keys()), ignore_case=True) self._spot_exchange_completer = WordCompleter(sorted(AllConnectorSettings.get_exchange_names()), ignore_case=True) self._exchange_amm_completer = WordCompleter(sorted(AllConnectorSettings.get_exchange_names().union(AllConnectorSettings.get_gateway_evm_amm_connector_names())), ignore_case=True) self._evm_amm_lp_completer = WordCompleter(sorted(AllConnectorSettings.get_gateway_evm_amm_lp_connector_names()), ignore_case=True) self._trading_timeframe_completer = WordCompleter(["infinite", "from_date_to_date", "daily_between_times"], ignore_case=True) self._derivative_completer = WordCompleter(AllConnectorSettings.get_derivative_names(), ignore_case=True) self._derivative_exchange_completer = WordCompleter(AllConnectorSettings.get_derivative_names().difference(AllConnectorSettings.get_derivative_dex_names()), ignore_case=True) self._connect_option_completer = WordCompleter(CONNECT_OPTIONS, ignore_case=True) self._export_completer = WordCompleter(["keys", "trades"], ignore_case=True) self._balance_completer = WordCompleter(["limit", "paper"], ignore_case=True) self._history_completer = WordCompleter(["--days", "--verbose", "--precision"], ignore_case=True) self._gateway_completer = WordCompleter(["create", "config", "connect", "connector-tokens", "generate-certs", "status", "test-connection", "start", "stop"], ignore_case=True) self._gateway_connect_completer = WordCompleter(GATEWAY_CONNECTORS, ignore_case=True) self._gateway_connector_tokens_completer = WordCompleter(sorted(AllConnectorSettings.get_gateway_evm_amm_connector_names()), ignore_case=True) self._gateway_config_completer = WordCompleter(hummingbot_application.gateway_config_keys, ignore_case=True) self._strategy_completer = WordCompleter(STRATEGIES, ignore_case=True) self._py_file_completer = WordCompleter(file_name_list(str(PMM_SCRIPTS_PATH), "py")) self._script_strategy_completer = WordCompleter(file_name_list(str(SCRIPT_STRATEGIES_PATH), "py")) self._rate_oracle_completer = WordCompleter([r.name for r in RateOracleSource], ignore_case=True) self._gateway_networks = [] self._list_gateway_wallets_parameters = {"wallets": [], "chain": ""} def set_gateway_networks(self, gateway_networks): self._gateway_networks = gateway_networks def set_list_gateway_wallets_parameters(self, wallets, chain): self._list_gateway_wallets_parameters = {"wallets": wallets, "chain": chain} @property def prompt_text(self) -> str: return self.hummingbot_application.app.prompt_text @property def parser(self) -> ThrowingArgumentParser: return self.hummingbot_application.parser def get_subcommand_completer(self, first_word: str) -> Completer: subcommands: List[str] = self.parser.subcommands_from(first_word) return WordCompleter(subcommands, ignore_case=True) @property def _trading_pair_completer(self) -> Completer: trading_pair_fetcher = TradingPairFetcher.get_instance() market = "" for exchange in sorted(list(AllConnectorSettings.get_connector_settings().keys()), key=len, reverse=True): if exchange in self.prompt_text: market = exchange break trading_pairs = trading_pair_fetcher.trading_pairs.get(market, []) if trading_pair_fetcher.ready and market else [] return WordCompleter(trading_pairs, ignore_case=True, sentence=True) @property def _gateway_network_completer(self): return WordCompleter(self._gateway_networks, ignore_case=True) @property def _gateway_wallet_address_completer(self): return WordCompleter(list_gateway_wallets(self._list_gateway_wallets_parameters["wallets"], self._list_gateway_wallets_parameters["chain"]), ignore_case=True) @property def _option_completer(self): outer = re.compile(r"\((.+)\)") inner_str = outer.search(self.prompt_text).group(1) options = inner_str.split("/") if "/" in inner_str else [] return WordCompleter(options, ignore_case=True) @property def _config_completer(self): config_keys = self.hummingbot_application.configurable_keys() return WordCompleter(config_keys, ignore_case=True) def _complete_strategies(self, document: Document) -> bool: return "strategy" in self.prompt_text and "strategy file" not in self.prompt_text def _complete_pmm_script_files(self, document: Document) -> bool: return "PMM script file" in self.prompt_text def _complete_configs(self, document: Document) -> bool: text_before_cursor: str = document.text_before_cursor return "config" in text_before_cursor def _complete_options(self, document: Document) -> bool: return "(" in self.prompt_text and ")" in self.prompt_text and "/" in self.prompt_text def _complete_exchanges(self, document: Document) -> bool: return any(x for x in ("exchange name", "name of exchange", "name of the exchange") if x in self.prompt_text.lower()) def _complete_derivatives(self, document: Document) -> bool: text_before_cursor: str = document.text_before_cursor return "perpetual" in text_before_cursor or \ any(x for x in ("derivative connector", "derivative name", "name of derivative", "name of the derivative") if x in self.prompt_text.lower()) def _complete_connect_options(self, document: Document) -> bool: text_before_cursor: str = document.text_before_cursor return text_before_cursor.startswith("connect ") def _complete_exchange_amm_connectors(self, document: Document) -> bool: return "(Exchange/AMM)" in self.prompt_text def _complete_spot_exchanges(self, document: Document) -> bool: return "spot" in self.prompt_text def _complete_lp_connector(self, document: Document) -> bool: return "LP" in self.prompt_text def _complete_trading_timeframe(self, document: Document) -> bool: return any(x for x in ("trading timeframe", "execution timeframe") if x in self.prompt_text.lower()) def _complete_export_options(self, document: Document) -> bool: text_before_cursor: str = document.text_before_cursor return "export" in text_before_cursor def _complete_balance_options(self, document: Document) -> bool: text_before_cursor: str = document.text_before_cursor return text_before_cursor.startswith("balance ") def _complete_history_arguments(self, document: Document) -> bool: text_before_cursor: str = document.text_before_cursor return text_before_cursor.startswith("history ") def _complete_gateway_connect_arguments(self, document: Document) -> bool: text_before_cursor: str = document.text_before_cursor return text_before_cursor.startswith("gateway connect ") def _complete_gateway_connector_tokens_arguments(self, document: Document) -> bool: text_before_cursor: str = document.text_before_cursor return text_before_cursor.startswith("gateway connector-tokens ") def _complete_gateway_arguments(self, document: Document) -> bool: text_before_cursor: str = document.text_before_cursor return text_before_cursor.startswith("gateway ") and not text_before_cursor.startswith("gateway config ") def _complete_gateway_config_arguments(self, document: Document) -> bool: text_before_cursor: str = document.text_before_cursor return text_before_cursor.startswith("gateway config ") def _complete_script_strategy_files(self, document: Document) -> bool: text_before_cursor: str = document.text_before_cursor return text_before_cursor.startswith("start --script ") def _complete_trading_pairs(self, document: Document) -> bool: return "trading pair" in self.prompt_text def _complete_paths(self, document: Document) -> bool: text_before_cursor: str = document.text_before_cursor return (("path" in self.prompt_text and "file" in self.prompt_text) or "import" in text_before_cursor) def _complete_gateway_network(self, document: Document) -> bool: return "Which network do you want" in self.prompt_text def _complete_gateway_wallet_addresses(self, document: Document) -> bool: return "Select a gateway wallet" in self.prompt_text def _complete_command(self, document: Document) -> bool: text_before_cursor: str = document.text_before_cursor return " " not in text_before_cursor and len(self.prompt_text.replace(">>> ", "")) == 0 def _complete_subcommand(self, document: Document) -> bool: text_before_cursor: str = document.text_before_cursor index: int = text_before_cursor.index(' ') return text_before_cursor[0:index] in self.parser.commands def _complete_balance_limit_exchanges(self, document: Document): text_before_cursor: str = document.text_before_cursor command_args = text_before_cursor.split(" ") return len(command_args) == 3 and command_args[0] == "balance" and command_args[1] == "limit" def _complete_rate_oracle_source(self, document: Document): return all(x in self.prompt_text for x in ("source", "rate oracle")) def get_completions(self, document: Document, complete_event: CompleteEvent): """ Get completions for the current scope. This is the defining function for the completer :param document: :param complete_event: """ if self._complete_pmm_script_files(document): for c in self._py_file_completer.get_completions(document, complete_event): yield c elif self._complete_script_strategy_files(document): for c in self._script_strategy_completer.get_completions(document, complete_event): yield c elif self._complete_paths(document): for c in self._path_completer.get_completions(document, complete_event): yield c elif self._complete_strategies(document): for c in self._strategy_completer.get_completions(document, complete_event): yield c elif self._complete_gateway_network(document): for c in self._gateway_network_completer.get_completions(document, complete_event): yield c elif self._complete_gateway_wallet_addresses(document): for c in self._gateway_wallet_address_completer.get_completions(document, complete_event): yield c if self._complete_lp_connector(document): for c in self._evm_amm_lp_completer.get_completions(document, complete_event): yield c elif self._complete_exchange_amm_connectors(document): if self._complete_spot_exchanges(document): for c in self._spot_exchange_completer.get_completions(document, complete_event): yield c elif self._complete_derivatives(document): for c in self._derivative_exchange_completer.get_completions(document, complete_event): yield c else: for c in self._exchange_amm_completer.get_completions(document, complete_event): yield c elif self._complete_spot_exchanges(document): for c in self._spot_exchange_completer.get_completions(document, complete_event): yield c elif self._complete_derivatives(document): for c in self._derivative_exchange_completer.get_completions(document, complete_event): yield c elif self._complete_trading_timeframe(document): for c in self._trading_timeframe_completer.get_completions(document, complete_event): yield c elif self._complete_connect_options(document): for c in self._connect_option_completer.get_completions(document, complete_event): yield c elif self._complete_export_options(document): for c in self._export_completer.get_completions(document, complete_event): yield c elif self._complete_balance_limit_exchanges(document): for c in self._connect_option_completer.get_completions(document, complete_event): yield c elif self._complete_balance_options(document): for c in self._balance_completer.get_completions(document, complete_event): yield c elif self._complete_history_arguments(document): for c in self._history_completer.get_completions(document, complete_event): yield c elif self._complete_gateway_connect_arguments(document): for c in self._gateway_connect_completer.get_completions(document, complete_event): yield c elif self._complete_gateway_connector_tokens_arguments(document): for c in self._gateway_connector_tokens_completer.get_completions(document, complete_event): yield c elif self._complete_gateway_arguments(document): for c in self._gateway_completer.get_completions(document, complete_event): yield c elif self._complete_gateway_config_arguments(document): for c in self._gateway_config_completer.get_completions(document, complete_event): yield c elif self._complete_derivatives(document): if "(Exchange/AMM)" in self.prompt_text: for c in self._derivative_completer.get_completions(document, complete_event): yield c else: for c in self._derivative_exchange_completer.get_completions(document, complete_event): yield c elif self._complete_exchanges(document): for c in self._exchange_completer.get_completions(document, complete_event): yield c elif self._complete_trading_pairs(document): for c in self._trading_pair_completer.get_completions(document, complete_event): yield c elif self._complete_command(document): for c in self._command_completer.get_completions(document, complete_event): yield c elif self._complete_configs(document): for c in self._config_completer.get_completions(document, complete_event): yield c elif self._complete_options(document): for c in self._option_completer.get_completions(document, complete_event): yield c elif self._complete_rate_oracle_source(document): for c in self._rate_oracle_completer.get_completions(document, complete_event): yield c else: text_before_cursor: str = document.text_before_cursor try: first_word: str = text_before_cursor[0:text_before_cursor.index(' ')] except ValueError: return subcommand_completer: Completer = self.get_subcommand_completer(first_word) if complete_event.completion_requested or self._complete_subcommand(document): for c in subcommand_completer.get_completions(document, complete_event): yield c
class AutoCommand(Command): def __init__(self, fn): self._built_in = False self._fn = fn if not callable(fn): raise ValueError("fn argument must be a callable") self._obj_metadata = inspect_object(fn) self._is_super_command = len(self.metadata.subcommands) > 0 self._subcommand_names = [] # We never expect a function to be passed here that has a self argument # In that case, we should get a bound method if "self" in self.metadata.arguments and not inspect.ismethod(self._fn): raise ValueError( "Expecting either a function (eg. bar) or " "a bound method (eg. Foo().bar). " "You passed what appears to be an unbound method " "(eg. Foo.bar) it has a 'self' argument: %s" % function_to_str(fn) ) if not self.metadata.command: raise ValueError( "function or class {} needs to be annotated with " "@command".format(function_to_str(fn)) ) # If this is a super command, we need a completer for sub-commands if self.super_command: self._commands_completer = WordCompleter( [], ignore_case=True, sentence=True ) for _, inspection in self.metadata.subcommands: _sub_name = inspection.command.name self._commands_completer.words.append(_sub_name) self._commands_completer.meta_dict[_sub_name] = dedent( inspection.command.help ).strip() self._subcommand_names.append(_sub_name) @property def metadata(self) -> FunctionInspection: """ The Inspection object of this command. This object contains all the information required by AutoCommand to understand the command arguments type information, help messages, aliases, and attributes. """ return self._obj_metadata def _create_subcommand_obj(self, key_values): """ Instantiates an object of the super command class, passes the right arguments and returns a dict with the remaining unused arguments """ kwargs = { k: v for k, v in get_arguments_for_inspection(self.metadata, key_values).items() if v is not None } remaining = {k: v for k, v in key_values.items() if k not in kwargs.keys()} return self._fn(**kwargs), remaining def run_interactive(self, cmd, args, raw): try: args_metadata = self.metadata.arguments parsed = parser.parse(args, expect_subcommand=self.super_command) # prepare args dict parsed_dict = parsed.asDict() args_dict = parsed.kv.asDict() key_values = parsed.kv.asDict() command_name = cmd # if this is a super command, we need first to create an instance of # the class (fn) and pass the right arguments if self.super_command: subcommand = parsed_dict.get("__subcommand__") if not subcommand: cprint( "A sub-command must be supplied, valid values: " "{}".format(", ".join(self._get_subcommands())), "red", ) return 2 sub_inspection = self.subcommand_metadata(subcommand) if not sub_inspection: cprint( "Invalid sub-command '{}', valid values: " "{}".format(subcommand, ", ".join(self._get_subcommands())), "red", ) return 2 instance, remaining_args = self._create_subcommand_obj(args_dict) assert instance args_dict = remaining_args key_values = copy.copy(args_dict) args_metadata = sub_inspection.arguments attrname = self._find_subcommand_attr(subcommand) command_name = subcommand assert attrname is not None fn = getattr(instance, attrname) else: # not a super-command, use use the function instead fn = self._fn positionals = parsed_dict["positionals"] if parsed.positionals != "" else [] # We only allow positionals for arguments that have positional=True # ِ We filter out the OrderedDict this way to ensure we don't lose the # order of the arguments. We also filter out arguments that have # been passed by name already. The order of the positional arguments # follows the order of the function definition. can_be_positional = self._positional_arguments( args_metadata, args_dict.keys() ) if len(positionals) > len(can_be_positional): if len(can_be_positional) == 0: err = "This command does not support positional arguments" else: # We have more positionals than we should err = ( "This command only supports ({}) positional arguments, " "namely arguments ({}). You have passed {} arguments ({})" " instead!" ).format( len(can_be_positional), ", ".join(can_be_positional.keys()), len(positionals), ", ".join(str(x) for x in positionals), ) cprint(err, "red") return 2 # constuct key_value dict from positional arguments. args_from_positionals = { key: value for value, key in zip(positionals, can_be_positional) } # update the total arguments dict with the positionals args_dict.update(args_from_positionals) # Run some validations on number of arguments provided # do we have keys that are supplied in both positionals and # key_value style? duplicate_keys = set(args_from_positionals.keys()).intersection( set(key_values.keys()) ) if duplicate_keys: cprint( "Arguments '{}' have been passed already, cannot have" " duplicate keys".format(list(duplicate_keys)), "red", ) return 2 # check for verbosity override in kwargs ctx = context.get_context() old_verbose = ctx.args.verbose if "verbose" in args_dict: ctx.set_verbose(args_dict["verbose"]) del args_dict["verbose"] del key_values["verbose"] # do we have keys that we know nothing about? extra_keys = set(args_dict.keys()) - set(args_metadata) if extra_keys: cprint( "Unknown argument(s) {} were" " passed".format(list(extra_keys)), "magenta", ) return 2 # is there any required keys that were not resolved from positionals # nor key_values? missing_keys = set(args_metadata) - set(args_dict.keys()) if missing_keys: required_missing = [] for key in missing_keys: if not args_metadata[key].default_value_set: required_missing.append(key) if required_missing: cprint( "Missing required argument(s) {} for command" " {}".format(required_missing, command_name), "yellow", ) return 3 # convert expected types for arguments for key, value in args_dict.items(): target_type = args_metadata[key].type if target_type is None: target_type = str try: new_value = apply_typing(value, target_type) except ValueError as e: fn_name = function_to_str(target_type, False, False) cprint( 'Cannot convert value "{}" to {} on argument {}'.format( value, fn_name, key ), "yellow", ) return 4 else: args_dict[key] = new_value # Validate that arguments with `choices` are supplied with the # acceptable values. for arg, value in args_dict.items(): choices = args_metadata[arg].choices if choices: # Validate the choices in the case of values and list of # values. if issubclass_(args_metadata[arg].type, typing.List): bad_inputs = [v for v in value if v not in choices] if bad_inputs: cprint( f"Argument '{arg}' got an unexpected " f"value(s) '{bad_inputs}'. Expected one " f"or more of {choices}.", "red", ) return 4 elif value not in choices: cprint( f"Argument '{arg}' got an unexpected value " f"'{value}'. Expected one of " f"{choices}.", "red", ) return 4 # arguments appear to be fine, time to run the function try: # convert argument names back to match the function signature args_dict = {args_metadata[k].arg: v for k, v in args_dict.items()} if inspect.iscoroutinefunction(fn): loop = asyncio.get_event_loop() ret = loop.run_until_complete(fn(**args_dict)) else: ret = fn(**args_dict) ctx.set_verbose(old_verbose) except Exception as e: cprint("Error running command: {}".format(str(e)), "red") cprint("-" * 60, "yellow") traceback.print_exc(file=sys.stderr) cprint("-" * 60, "yellow") return 1 return ret except CommandParseError as e: cprint("Error parsing command", "red") cprint(cmd + " " + args, "white", attrs=["bold"]) cprint((" " * (e.col + len(cmd))) + "^", "white", attrs=["bold"]) cprint(str(e), "yellow") return 1 def _positional_arguments(self, args_metadata, filter_out): positionals = OrderedDict() for k, v in args_metadata.items(): if v.positional and k not in filter_out: positionals[k] = v return positionals def subcommand_metadata(self, name: str) -> FunctionInspection: assert self.super_command subcommands = self.metadata.subcommands for _, inspection in subcommands: if inspection.command.name == name: return inspection def _find_subcommand_attr(self, name): assert self.super_command subcommands = self.metadata.subcommands for attr, inspection in subcommands: if inspection.command.name == name: return attr # be explicit about returning None for readability return None def _get_subcommands(self) -> Iterable[str]: assert self.super_command return [inspection.command.name for _, inspection in self.metadata.subcommands] def _kwargs_for_fn(self, fn, args): return { k: v for k, v in get_arguments_for_command(fn, args).items() if v is not None } def run_cli(self, args): # if this is a super-command, we need to dispatch the call to the # correct function kwargs = self._kwargs_for_fn(self._fn, args) try: if self._is_super_command: # let's instantiate an instance of the klass instance = self._fn(**kwargs) # we need to find the actual method we want to call, in addition to # this we need to extract the correct kwargs for this method # find which function it is in the sub commands attrname = self._find_subcommand_attr(args._subcmd) assert attrname is not None fn = getattr(instance, attrname) kwargs = self._kwargs_for_fn(fn, args) else: fn = self._fn if inspect.iscoroutinefunction(fn): # execute in an event loop loop = asyncio.get_event_loop() return loop.run_until_complete(fn(**kwargs)) else: return fn(**kwargs) except Exception as e: cprint("Error running command: {}".format(str(e)), "red") cprint("-" * 60, "yellow") traceback.print_exc(file=sys.stderr) cprint("-" * 60, "yellow") return 1 @property def super_command(self): return self._is_super_command def has_subcommand(self, subcommand): assert self.super_command return subcommand.lower() in self._subcommand_names def add_arguments(self, parser): register_command(parser, self.metadata) def get_command_names(self): command = self.metadata.command return [command.name] + command.aliases def get_completions( self, _: str, document: Document, complete_event: CompleteEvent ) -> Iterable[Completion]: if self._is_super_command: exploded = document.text.lstrip().split(" ", 1) # Are we at the first word? we expect a sub-command here if len(exploded) <= 1: return self._commands_completer.get_completions( document, complete_event ) state_machine = AutoCommandCompletion(self, document, complete_event) return state_machine.get_completions() def get_help(self, cmd, *args): help = self.metadata.command.help return dedent(help).strip() if help else None
def test_word_completer_match_middle(): completer = WordCompleter(['abc', 'def', 'abca'], match_middle=True) completions = completer.get_completions(Document('bc'), CompleteEvent()) assert [c.text for c in completions] == ['abc', 'abca']