diff --git a/mock/local_test.py b/mock/local_test.py index 9e20232..12e4c38 100644 --- a/mock/local_test.py +++ b/mock/local_test.py @@ -1,61 +1,102 @@ -import math +__all__ = ["AutoCompleter"] + +from prompt_toolkit import PromptSession, HTML +from prompt_toolkit.completion import Completer, Completion +from prompt_toolkit.document import Document +from prompt_toolkit.history import History, ThreadedHistory, FileHistory, InMemoryHistory +from prompt_toolkit.key_binding import KeyBindings -def estimate_nth_prime_upper_bound(n: int): - if n < 6: - return 15 - - log_n = math.log(n) - log_log_n = math.log(log_n) - - if n < 100: - return int(n * (log_n + log_log_n) * 1.5) - elif n < 1000: - return int(n * (log_n + log_log_n) * 1.3) - elif n >= 8009824: - return int(n * (log_n + log_log_n - 1 + 1.8 * log_log_n / log_n)) - else: - return int(n * (log_n + log_log_n - 1 + 2.0 * log_log_n / log_n)) +class HistoryCompleter(Completer): + def __init__(self, history_container: History, static_commands: set[str]) -> None: + self.history_container: History = history_container + self.static_commands: set[str] = static_commands + + def get_completions(self, document: Document, complete_event): + text: str = document.text_before_cursor + history_items: set[str] = set(self.history_container.load_history_strings()) + all_candidates: set[str] = history_items.union(self.static_commands) + matches: list[str] = sorted(cmd for cmd in all_candidates if cmd.startswith(text)) + + if not matches: + return + + for match in matches: + yield Completion( + match, + start_position=-len(text), + display=match + ) + + @staticmethod + def _find_common_prefix(matches: list[str]) -> str: + if not matches: + return "" + common: str = matches[0] + for match in matches[1:]: + i: int = 0 + while i < len(common) and i < len(match) and common[i] == match[i]: + i += 1 + common = common[:i] + return common -def odd_dig_primes(n: int) -> list[int]: - nums = {k: True for k in range(2, n+1)} - - for num, is_checkable in nums.items(): - if not is_checkable: - continue - - if nums[2]: - nums[2] = False - - for x in range(num * num, n, num): - nums[x] = False - - primes = len([x for x in nums.items() if x[1]]) - max_prime = max([x[0] for x in nums.items() if x[1]]) - - upper_bound = estimate_nth_prime_upper_bound(primes+1) - print(upper_bound) - nums2 = {k: True for k in range(2, upper_bound)} +class AutoCompleter: + def __init__( + self, + history_filename: str | None = None, + autocomplete_button: str = "tab" + ) -> None: + self.history_filename: str | None = history_filename + self.autocomplete_button: str = autocomplete_button + self._session: PromptSession | None = None - for num, is_checkable in nums2.items(): - if not is_checkable: - continue - - if nums2[2]: - nums2[2] = False - - for x in range(num * num, upper_bound, num): - nums2[x] = False - - print([x for x in nums2.items() if x[1]]) + def initial_setup(self, all_commands: set[str]) -> None: + kb = KeyBindings() - next_prime_after_max = [x[0] for x in nums2.items() if x[1]][-1] + def _(event): + buff = event.app.current_buffer - return [ - primes, - max_prime, - next_prime_after_max - ] - -print(odd_dig_primes(13)) \ No newline at end of file + if buff.complete_state: + buff.complete_next() + else: + completions = list(buff.completer.get_completions(buff.document, None)) + if len(completions) == 1: + buff.apply_completion(completions[0]) + else: + buff.start_completion(select_first=False) + + kb.add(self.autocomplete_button)(_) + + if self.history_filename: + history = FileHistory(self.history_filename) + history = ThreadedHistory(history) + else: + history = InMemoryHistory() + + self._session = PromptSession( + history=history, + completer=HistoryCompleter(history, all_commands), + complete_while_typing=False, + key_bindings=kb, + ) + + def prompt(self, prompt_text: str | HTML = ">>> ") -> str: + if self._session is None: + raise RuntimeError("Call initial_setup() before using prompt()") + return self._session.prompt( + HTML(f"{prompt_text}") if isinstance(prompt_text, str) else prompt_text + ) + + +if __name__ == "__main__": + test_commands: set[str] = {"start", "qwertyu", "stop", "exit"} + hist_file: str = "history.txt" + + ac: AutoCompleter = AutoCompleter(autocomplete_button='tab') + ac.initial_setup(test_commands) + + while True: + inp: str = ac.prompt(">>> ").strip() + if inp == "exit": + break diff --git a/pyproject.toml b/pyproject.toml index eeabf62..0c68235 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,6 +11,7 @@ dependencies = [ "art (>=6.4,<7.0)", "pyreadline3>=3.5.4; sys_platform == 'win32'", "dishka>=1.7.2", + "prompt-toolkit>=3.0.52", ] [dependency-groups] diff --git a/src/argenta/app/autocompleter/entity.py b/src/argenta/app/autocompleter/entity.py index a50bad0..db0f6f1 100644 --- a/src/argenta/app/autocompleter/entity.py +++ b/src/argenta/app/autocompleter/entity.py @@ -1,97 +1,89 @@ __all__ = ["AutoCompleter"] -import os -import readline -from typing import Never +from prompt_toolkit import PromptSession, HTML +from prompt_toolkit.completion import Completer, Completion +from prompt_toolkit.document import Document +from prompt_toolkit.history import History, ThreadedHistory, FileHistory, InMemoryHistory +from prompt_toolkit.key_binding import KeyBindings + + +class HistoryCompleter(Completer): + def __init__(self, history_container: History, static_commands: set[str]) -> None: + self.history_container: History = history_container + self.static_commands: set[str] = static_commands + + def get_completions(self, document: Document, complete_event): + text: str = document.text_before_cursor + history_items: set[str] = set(self.history_container.load_history_strings()) + all_candidates: set[str] = history_items.union(self.static_commands) + matches: list[str] = sorted(cmd for cmd in all_candidates if cmd.startswith(text)) + + if not matches: + return + + for match in matches: + yield Completion( + match, + start_position=-len(text), + display=match + ) + + @staticmethod + def _find_common_prefix(matches: list[str]) -> str: + if not matches: + return "" + common: str = matches[0] + for match in matches[1:]: + i: int = 0 + while i < len(common) and i < len(match) and common[i] == match[i]: + i += 1 + common = common[:i] + return common class AutoCompleter: def __init__( - self, history_filename: str | None = None, autocomplete_button: str = "tab" + self, + history_filename: str | None = None, + autocomplete_button: str = "tab" ) -> None: - """ - Public. Configures and implements auto-completion of input command - :param history_filename: the name of the file for saving the history of the autocompleter - :param autocomplete_button: the button for auto-completion - :return: None - """ self.history_filename: str | None = history_filename self.autocomplete_button: str = autocomplete_button - - def _complete(self, text: str, state: int) -> str | None: - """ - Private. Auto-completion function - :param text: part of the command being entered - :param state: the current cursor position is relative to the beginning of the line - :return: the desired candidate as str or None - """ - matches: list[str] = sorted( - cmd for cmd in _get_history_items() if cmd.startswith(text) - ) - if len(matches) > 1: - common_prefix = matches[0] - for match in matches[1:]: - i = 0 - while ( - i < len(common_prefix) - and i < len(match) - and common_prefix[i] == match[i] - ): - i += 1 - common_prefix = common_prefix[:i] - if state == 0: - readline.insert_text(common_prefix[len(text) :]) - readline.redisplay() - return None - elif len(matches) == 1: - return matches[0] if state == 0 else None - else: - return None + self._session: PromptSession | None = None def initial_setup(self, all_commands: set[str]) -> None: - """ - Private. Initial setup function - :param all_commands: Registered commands for adding them to the autocomplete history - :return: None - """ - if self.history_filename: - if os.path.exists(self.history_filename): - readline.read_history_file(self.history_filename) + kb = KeyBindings() + + def _(event): + buff = event.app.current_buffer + + if buff.complete_state: + buff.complete_next() else: - for line in all_commands: - readline.add_history(line) + completions = list(buff.completer.get_completions(buff.document, None)) + if len(completions) == 1: + buff.apply_completion(completions[0]) + else: + buff.start_completion(select_first=False) - if not self.history_filename: - for line in all_commands: - readline.add_history(line) + kb.add(self.autocomplete_button)(_) - readline.set_completer(self._complete) - readline.set_completer_delims(readline.get_completer_delims().replace(" ", "")) - readline.parse_and_bind(f"{self.autocomplete_button}: complete") - - def exit_setup(self, all_commands: set[str]) -> None: - """ - Private. Exit setup function - :return: None - """ if self.history_filename: - readline.write_history_file(self.history_filename) - with open(self.history_filename, "r") as history_file: - raw_history = history_file.read() - pretty_history: list[str] = [] - for line in set(raw_history.strip().split("\n")): - if line.split()[0] in all_commands: - pretty_history.append(line) - with open(self.history_filename, "w") as history_file: - _ = history_file.write("\n".join(pretty_history)) + history = FileHistory(self.history_filename) + history = ThreadedHistory(history) + else: + history = InMemoryHistory() + self._session = PromptSession( + history=history, + completer=HistoryCompleter(history, all_commands), + complete_while_typing=False, + key_bindings=kb, + ) -def _get_history_items() -> list[str] | list[Never]: - """ - Private. Returns a list of all commands entered by the user - :return: all commands entered by the user as list[str] | list[Never] - """ - return [ - readline.get_history_item(i) - for i in range(1, readline.get_current_history_length() + 1) - ] + def prompt(self, prompt_text: str | HTML = ">>> ") -> str: + if self._session is None: + raise RuntimeError("Call initial_setup() before using prompt()") + return self._session.prompt( + HTML(f"{prompt_text}") if isinstance(prompt_text, str) else prompt_text + ) \ No newline at end of file diff --git a/uv.lock b/uv.lock index d5fb31f..fe65840 100644 --- a/uv.lock +++ b/uv.lock @@ -44,6 +44,7 @@ source = { editable = "." } dependencies = [ { name = "art" }, { name = "dishka" }, + { name = "prompt-toolkit" }, { name = "pyreadline3", marker = "sys_platform == 'win32'" }, { name = "rich" }, ] @@ -75,6 +76,7 @@ typecheckers = [ requires-dist = [ { name = "art", specifier = ">=6.4,<7.0" }, { name = "dishka", specifier = ">=1.7.2" }, + { name = "prompt-toolkit", specifier = ">=3.0.52" }, { name = "pyreadline3", marker = "sys_platform == 'win32'", specifier = ">=3.5.4" }, { name = "rich", specifier = ">=14.0.0,<15.0.0" }, ] @@ -589,6 +591,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] +[[package]] +name = "prompt-toolkit" +version = "3.0.52" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, +] + [[package]] name = "pycodestyle" version = "2.14.0" @@ -1023,6 +1037,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, ] +[[package]] +name = "wcwidth" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/38/75/2144b65e4fba12a2d9868e9a3f99db7fa0760670d064603634bef9ff1709/wcwidth-0.3.0.tar.gz", hash = "sha256:af1a2fb0b83ef4a7fc0682a4c95ca2576e14d0280bca2a9e67b7dc9f2733e123", size = 172238, upload-time = "2026-01-21T17:44:09.508Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/0e/a5f0257ab47492b7afb5fb60347d14ba19445e2773fc8352d4be6bd2f6f8/wcwidth-0.3.0-py3-none-any.whl", hash = "sha256:073a1acb250e4add96cfd5ef84e0036605cd6e0d0782c8c15c80e42202348458", size = 85520, upload-time = "2026-01-21T17:44:08.002Z" }, +] + [[package]] name = "websockets" version = "15.0.1"