From e7c606f4d8a87207110749dfb04cc73cceab1a62 Mon Sep 17 00:00:00 2001 From: Roland Walker Date: Sat, 4 Apr 2026 19:55:47 -0400 Subject: [PATCH] move prompt-format methods to main_modes/repl.py Move these methods to main_modes/repl.py: * set_all_external_titles() * set_external_terminal_tab_title() * set_external_terminal_window_title() * set_external_multiplex_window_title() * set_external_multiplex_pane_title() * get_custom_toolbar() * get_prompt() and other rearrangements needed to effect that change. After the changes, main.py still has two calls to the new functions, which are marked with todo comments regarding the incomplete separation. --- mycli/clitoolbar.py | 11 +- mycli/main.py | 162 +---------------------- mycli/main_modes/repl.py | 172 ++++++++++++++++++++++++- test/pytests/test_clitoolbar.py | 10 +- test/pytests/test_main.py | 7 +- test/pytests/test_main_modes_repl.py | 186 ++++++++++++++++++++++++++- test/pytests/test_main_regression.py | 92 +++++++------ 7 files changed, 423 insertions(+), 217 deletions(-) diff --git a/mycli/clitoolbar.py b/mycli/clitoolbar.py index cdd22dc0..74df09ea 100644 --- a/mycli/clitoolbar.py +++ b/mycli/clitoolbar.py @@ -2,13 +2,18 @@ from prompt_toolkit.application import get_app from prompt_toolkit.enums import EditingMode -from prompt_toolkit.formatted_text import to_formatted_text +from prompt_toolkit.formatted_text import AnyFormattedText, to_formatted_text from prompt_toolkit.key_binding.vi_state import InputMode from mycli.packages import special -def create_toolbar_tokens_func(mycli, show_initial_toolbar_help: Callable, format_string: str | None) -> Callable: +def create_toolbar_tokens_func( + mycli, + show_initial_toolbar_help: Callable[[], bool], + format_string: str | None, + get_custom_toolbar: Callable[[str], AnyFormattedText], +) -> Callable[[], list[tuple[str, str]]]: """Return a function that generates the toolbar tokens.""" def get_toolbar_tokens() -> list[tuple[str, str]]: @@ -73,7 +78,7 @@ def get_toolbar_tokens() -> list[tuple[str, str]]: else: amended_format = format_string result = [] - formatted = to_formatted_text(mycli.get_custom_toolbar(amended_format), style='class:bottom-toolbar') + formatted = to_formatted_text(get_custom_toolbar(amended_format), style='class:bottom-toolbar') result.extend([*formatted]) # coerce to list for mypy result.extend(dynamic) diff --git a/mycli/main.py b/mycli/main.py index 9fe869c9..bbc2fb55 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -3,13 +3,11 @@ from collections import defaultdict from dataclasses import dataclass from decimal import Decimal -import functools from io import TextIOWrapper import logging import os import re import shutil -import subprocess import sys import threading import traceback @@ -73,17 +71,15 @@ from mycli.main_modes.execute import main_execute_from_cli from mycli.main_modes.list_dsn import main_list_dsn from mycli.main_modes.list_ssh_config import main_list_ssh_config -from mycli.main_modes.repl import main_repl +from mycli.main_modes.repl import get_prompt, main_repl, set_all_external_titles from mycli.packages import special from mycli.packages.cli_utils import filtered_sys_argv, is_valid_connection_scheme from mycli.packages.filepaths import dir_path_exists, guess_socket_location from mycli.packages.prompt_utils import confirm_destructive_query from mycli.packages.special.favoritequeries import FavoriteQueries from mycli.packages.special.main import ArgType -from mycli.packages.special.utils import format_uptime, get_ssl_version, get_uptime, get_warning_count from mycli.packages.sqlresult import SQLResult from mycli.packages.ssh_utils import read_ssh_config -from mycli.packages.string_utils import sanitize_terminal_title from mycli.packages.tabular_output import sql_format from mycli.sqlcompleter import SQLCompleter from mycli.sqlexecute import FIELD_TYPES, SQLExecute @@ -412,7 +408,9 @@ def change_db(self, arg: str, **_) -> Generator[SQLResult, None, None]: self.sqlexecute.change_db(arg) msg = f'You are now connected to database "{self.sqlexecute.dbname}" as user "{self.sqlexecute.user}"' - self.set_all_external_titles() + # todo: this jump back to repl.py is a sign that separation is incomplete. + # also: it should not be needed. Don't titles update on every new prompt? + set_all_external_titles(self) yield SQLResult(status=msg) @@ -908,7 +906,8 @@ def get_output_margin(self, status: str | None = None) -> int: render_counter = self.prompt_session.app.render_counter else: render_counter = 0 - self.prompt_lines = self.get_prompt(self.prompt_format, render_counter).count('\n') + 1 + # todo: this jump back to get_prompt() in repl.py is a sign that separation is incomplete + self.prompt_lines = get_prompt(self, self.prompt_format, render_counter).count('\n') + 1 margin = self.get_reserved_space() + self.prompt_lines if special.is_timing_enabled(): margin += 1 @@ -1045,155 +1044,6 @@ def get_completions(self, text: str, cursor_position: int) -> Iterable[Completio with self._completer_lock: return self.completer.get_completions(Document(text=text, cursor_position=cursor_position), None) - def set_all_external_titles(self) -> None: - self.set_external_terminal_tab_title() - self.set_external_terminal_window_title() - self.set_external_multiplex_window_title() - self.set_external_multiplex_pane_title() - - def set_external_terminal_tab_title(self) -> None: - if not self.terminal_tab_title_format: - return - if not self.prompt_session: - return - if not sys.stderr.isatty(): - return - title = sanitize_terminal_title(self.get_prompt(self.terminal_tab_title_format, self.prompt_session.app.render_counter)) - print(f'\x1b]1;{title}\a', file=sys.stderr, end='') - sys.stderr.flush() - - def set_external_terminal_window_title(self) -> None: - if not self.terminal_window_title_format: - return - if not self.prompt_session: - return - if not sys.stderr.isatty(): - return - title = sanitize_terminal_title(self.get_prompt(self.terminal_window_title_format, self.prompt_session.app.render_counter)) - print(f'\x1b]2;{title}\a', file=sys.stderr, end='') - sys.stderr.flush() - - def set_external_multiplex_window_title(self) -> None: - if not self.multiplex_window_title_format: - return - if not os.getenv('TMUX'): - return - if not self.prompt_session: - return - title = sanitize_terminal_title(self.get_prompt(self.multiplex_window_title_format, self.prompt_session.app.render_counter)) - try: - subprocess.run( - ['tmux', 'rename-window', title], - check=False, - stdin=subprocess.DEVNULL, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - ) - except FileNotFoundError: - pass - - def set_external_multiplex_pane_title(self) -> None: - if not self.multiplex_pane_title_format: - return - if not os.getenv('TMUX'): - return - if not self.prompt_session: - return - if not sys.stderr.isatty(): - return - title = sanitize_terminal_title(self.get_prompt(self.multiplex_pane_title_format, self.prompt_session.app.render_counter)) - print(f'\x1b]2;{title}\x1b\\', file=sys.stderr, end='') - sys.stderr.flush() - - def get_custom_toolbar(self, toolbar_format: str) -> ANSI: - if not self.prompt_session: - return ANSI('') - if not self.prompt_session.app: - return ANSI('') - if self.prompt_session.app.current_buffer.text: - return self.last_custom_toolbar_message - toolbar = self.get_prompt(toolbar_format, self.prompt_session.app.render_counter) - toolbar = toolbar.replace("\\x1b", "\x1b") - self.last_custom_toolbar_message = ANSI(toolbar) - return self.last_custom_toolbar_message - - # Memoizing a method leaks the instance, but we only expect one MyCli instance. - # Before memoizing, get_prompt() was called dozens of times per prompt. - # Even after memoizing, get_prompt's logic gets called twice per prompt, which - # should be addressed, because some format strings take a trip to the server. - @functools.lru_cache(maxsize=256) # noqa: B019 - def get_prompt(self, string: str, _render_counter: int) -> str: - sqlexecute = self.sqlexecute - assert sqlexecute is not None - assert sqlexecute.server_info is not None - assert sqlexecute.server_info.species is not None - if self.login_path and self.login_path_as_host: - prompt_host = self.login_path - elif sqlexecute.host is not None: - prompt_host = sqlexecute.host - else: - prompt_host = DEFAULT_HOST - short_prompt_host, _, _ = prompt_host.partition('.') - if re.match(r'^[\d\.]+$', short_prompt_host): - short_prompt_host = prompt_host - now = datetime.now() - backslash_placeholder = '\ufffc_backslash' - string = string.replace('\\\\', backslash_placeholder) - string = string.replace("\\u", sqlexecute.user or "(none)") - string = string.replace("\\h", prompt_host or "(none)") - string = string.replace("\\H", short_prompt_host or "(none)") - string = string.replace("\\d", sqlexecute.dbname or "(none)") - string = string.replace("\\t", sqlexecute.server_info.species.name) - string = string.replace("\\n", "\n") - string = string.replace("\\D", now.strftime("%a %b %d %H:%M:%S %Y")) - string = string.replace("\\m", now.strftime("%M")) - string = string.replace("\\P", now.strftime("%p")) - string = string.replace("\\R", now.strftime("%H")) - string = string.replace("\\r", now.strftime("%I")) - string = string.replace("\\s", now.strftime("%S")) - string = string.replace("\\p", str(sqlexecute.port)) - string = string.replace("\\j", os.path.basename(sqlexecute.socket or '(none)')) - string = string.replace("\\J", sqlexecute.socket or '(none)') - string = string.replace("\\k", os.path.basename(sqlexecute.socket or str(sqlexecute.port))) - string = string.replace("\\K", sqlexecute.socket or str(sqlexecute.port)) - string = string.replace("\\A", self.dsn_alias or "(none)") - string = string.replace("\\_", " ") - string = string.replace(backslash_placeholder, '\\') - - # jump through hoops for the test environment, and for efficiency - if hasattr(sqlexecute, 'conn') and sqlexecute.conn is not None: - if '\\y' in string: - with sqlexecute.conn.cursor() as cur: - string = string.replace('\\y', str(get_uptime(cur)) or '(none)') - if '\\Y' in string: - with sqlexecute.conn.cursor() as cur: - string = string.replace('\\Y', format_uptime(str(get_uptime(cur))) or '(none)') - else: - string = string.replace('\\y', '(none)') - string = string.replace('\\Y', '(none)') - - if hasattr(sqlexecute, 'conn') and sqlexecute.conn is not None: - if '\\T' in string: - with sqlexecute.conn.cursor() as cur: - string = string.replace('\\T', get_ssl_version(cur) or '(none)') - else: - string = string.replace('\\T', '(none)') - - if hasattr(sqlexecute, 'conn') and sqlexecute.conn is not None: - if '\\w' in string: - with sqlexecute.conn.cursor() as cur: - string = string.replace('\\w', str(get_warning_count(cur) or '(none)')) - else: - string = string.replace('\\w', '(none)') - if hasattr(sqlexecute, 'conn') and sqlexecute.conn is not None: - if '\\W' in string: - with sqlexecute.conn.cursor() as cur: - string = string.replace('\\W', str(get_warning_count(cur) or '')) - else: - string = string.replace('\\W', '') - - return string - def run_query( self, query: str, diff --git a/mycli/main_modes/repl.py b/mycli/main_modes/repl.py index 66eca056..17edcd19 100644 --- a/mycli/main_modes/repl.py +++ b/mycli/main_modes/repl.py @@ -1,11 +1,14 @@ from __future__ import annotations from dataclasses import dataclass +from datetime import datetime +import functools from functools import partial from importlib import resources import os import random import re +import subprocess import sys import time import traceback @@ -34,6 +37,7 @@ from mycli.clistyle import style_factory_ptoolkit from mycli.clitoolbar import create_toolbar_tokens_func from mycli.constants import ( + DEFAULT_HOST, DEFAULT_WIDTH, HOME_URL, ISSUES_URL, @@ -49,6 +53,7 @@ ) from mycli.packages.prompt_utils import confirm, confirm_destructive_query from mycli.packages.ptoolkit.history import FileHistoryWithTimestamp +from mycli.packages.special.utils import format_uptime, get_ssl_version, get_uptime, get_warning_count from mycli.packages.sql_utils import ( is_dropping_database, is_mutating, @@ -57,6 +62,7 @@ need_completion_reset, ) from mycli.packages.sqlresult import SQLResult +from mycli.packages.string_utils import sanitize_terminal_title from mycli.sqlexecute import SQLExecute from mycli.types import Query @@ -68,6 +74,7 @@ SUPPORT_INFO = f"Home: {HOME_URL}\nBug tracker: {ISSUES_URL}" MIN_COMPLETION_TRIGGER = 1 +_PROMPT_TARGETS: dict[int, 'MyCli'] = {} @dataclass(slots=True) @@ -134,6 +141,164 @@ def _show_startup_banner( print('Tip —', _tips_picker()) +def set_all_external_titles(mycli: 'MyCli') -> None: + set_external_terminal_tab_title(mycli) + set_external_terminal_window_title(mycli) + set_external_multiplex_window_title(mycli) + set_external_multiplex_pane_title(mycli) + + +def set_external_terminal_tab_title(mycli: 'MyCli') -> None: + if not mycli.terminal_tab_title_format: + return + if not mycli.prompt_session: + return + if not sys.stderr.isatty(): + return + title = sanitize_terminal_title(get_prompt(mycli, mycli.terminal_tab_title_format, mycli.prompt_session.app.render_counter)) + print(f'\x1b]1;{title}\a', file=sys.stderr, end='') + sys.stderr.flush() + + +def set_external_terminal_window_title(mycli: 'MyCli') -> None: + if not mycli.terminal_window_title_format: + return + if not mycli.prompt_session: + return + if not sys.stderr.isatty(): + return + title = sanitize_terminal_title(get_prompt(mycli, mycli.terminal_window_title_format, mycli.prompt_session.app.render_counter)) + print(f'\x1b]2;{title}\a', file=sys.stderr, end='') + sys.stderr.flush() + + +def set_external_multiplex_window_title(mycli: 'MyCli') -> None: + if not mycli.multiplex_window_title_format: + return + if not os.getenv('TMUX'): + return + if not mycli.prompt_session: + return + title = sanitize_terminal_title(get_prompt(mycli, mycli.multiplex_window_title_format, mycli.prompt_session.app.render_counter)) + try: + subprocess.run( + ['tmux', 'rename-window', title], + check=False, + stdin=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + except FileNotFoundError: + pass + + +def set_external_multiplex_pane_title(mycli: 'MyCli') -> None: + if not mycli.multiplex_pane_title_format: + return + if not os.getenv('TMUX'): + return + if not mycli.prompt_session: + return + if not sys.stderr.isatty(): + return + title = sanitize_terminal_title(get_prompt(mycli, mycli.multiplex_pane_title_format, mycli.prompt_session.app.render_counter)) + print(f'\x1b]2;{title}\x1b\\', file=sys.stderr, end='') + sys.stderr.flush() + + +def get_custom_toolbar( + mycli: 'MyCli', + toolbar_format: str, +) -> ANSI: + if not mycli.prompt_session: + return ANSI('') + if not mycli.prompt_session.app: + return ANSI('') + if mycli.prompt_session.app.current_buffer.text: + return mycli.last_custom_toolbar_message + toolbar = get_prompt(mycli, toolbar_format, mycli.prompt_session.app.render_counter) + toolbar = toolbar.replace('\\x1b', '\x1b') + mycli.last_custom_toolbar_message = ANSI(toolbar) + return mycli.last_custom_toolbar_message + + +@functools.lru_cache(maxsize=256) +def get_prompt( + mycli: 'MyCli', + string: str, + _render_counter: int, +) -> str: + sqlexecute = mycli.sqlexecute + assert sqlexecute is not None + assert sqlexecute.server_info is not None + assert sqlexecute.server_info.species is not None + if mycli.login_path and mycli.login_path_as_host: + prompt_host = mycli.login_path + elif sqlexecute.host is not None: + prompt_host = sqlexecute.host + else: + prompt_host = DEFAULT_HOST + short_prompt_host, _, _ = prompt_host.partition('.') + if re.match(r'^[\d\.]+$', short_prompt_host): + short_prompt_host = prompt_host + now = datetime.now() + backslash_placeholder = '\ufffc_backslash' + string = string.replace('\\\\', backslash_placeholder) + string = string.replace('\\u', sqlexecute.user or '(none)') + string = string.replace('\\h', prompt_host or '(none)') + string = string.replace('\\H', short_prompt_host or '(none)') + string = string.replace('\\d', sqlexecute.dbname or '(none)') + string = string.replace('\\t', sqlexecute.server_info.species.name) + string = string.replace('\\n', '\n') + string = string.replace('\\D', now.strftime('%a %b %d %H:%M:%S %Y')) + string = string.replace('\\m', now.strftime('%M')) + string = string.replace('\\P', now.strftime('%p')) + string = string.replace('\\R', now.strftime('%H')) + string = string.replace('\\r', now.strftime('%I')) + string = string.replace('\\s', now.strftime('%S')) + string = string.replace('\\p', str(sqlexecute.port)) + string = string.replace('\\j', os.path.basename(sqlexecute.socket or '(none)')) + string = string.replace('\\J', sqlexecute.socket or '(none)') + string = string.replace('\\k', os.path.basename(sqlexecute.socket or str(sqlexecute.port))) + string = string.replace('\\K', sqlexecute.socket or str(sqlexecute.port)) + string = string.replace('\\A', mycli.dsn_alias or '(none)') + string = string.replace('\\_', ' ') + string = string.replace(backslash_placeholder, '\\') + + if hasattr(sqlexecute, 'conn') and sqlexecute.conn is not None: + if '\\y' in string: + with sqlexecute.conn.cursor() as cur: + string = string.replace('\\y', str(get_uptime(cur)) or '(none)') + if '\\Y' in string: + with sqlexecute.conn.cursor() as cur: + string = string.replace('\\Y', format_uptime(str(get_uptime(cur))) or '(none)') + else: + string = string.replace('\\y', '(none)') + string = string.replace('\\Y', '(none)') + + if hasattr(sqlexecute, 'conn') and sqlexecute.conn is not None: + if '\\T' in string: + with sqlexecute.conn.cursor() as cur: + string = string.replace('\\T', get_ssl_version(cur) or '(none)') + else: + string = string.replace('\\T', '(none)') + + if hasattr(sqlexecute, 'conn') and sqlexecute.conn is not None: + if '\\w' in string: + with sqlexecute.conn.cursor() as cur: + string = string.replace('\\w', str(get_warning_count(cur) or '(none)')) + else: + string = string.replace('\\w', '(none)') + if hasattr(sqlexecute, 'conn') and sqlexecute.conn is not None: + if '\\W' in string: + with sqlexecute.conn.cursor() as cur: + string = string.replace('\\W', str(get_warning_count(cur) or '')) + else: + string = string.replace('\\W', '') + + return string + + def _get_prompt_message( mycli: 'MyCli', app: prompt_toolkit.application.application.Application, @@ -141,9 +306,9 @@ def _get_prompt_message( if app.current_buffer.text: return mycli.last_prompt_message - prompt = mycli.get_prompt(mycli.prompt_format, app.render_counter) + prompt = get_prompt(mycli, mycli.prompt_format, app.render_counter) if mycli.prompt_format == mycli.default_prompt and len(prompt) > mycli.max_len_prompt: - prompt = mycli.get_prompt(mycli.default_prompt_splitln, app.render_counter) + prompt = get_prompt(mycli, mycli.default_prompt_splitln, app.render_counter) mycli.prompt_lines = prompt.count('\n') + 1 prompt = prompt.replace('\\x1b', '\x1b') if not mycli.prompt_lines: @@ -301,6 +466,7 @@ def _build_prompt_session( mycli, lambda: state.iterations == 0, mycli.toolbar_format, + partial(get_custom_toolbar, mycli), ) if mycli.wider_completion_menu: @@ -585,7 +751,7 @@ def main_repl(mycli: 'MyCli') -> None: key_bindings = mycli_bindings(mycli) _show_startup_banner(mycli, sqlexecute) _build_prompt_session(mycli, state, history, key_bindings) - mycli.set_all_external_titles() + set_all_external_titles(mycli) try: while True: diff --git a/test/pytests/test_clitoolbar.py b/test/pytests/test_clitoolbar.py index cffb5fd9..50d7c097 100644 --- a/test/pytests/test_clitoolbar.py +++ b/test/pytests/test_clitoolbar.py @@ -31,7 +31,7 @@ def make_mycli( def test_create_toolbar_tokens_func_shows_initial_help() -> None: mycli = make_mycli() - toolbar = clitoolbar.create_toolbar_tokens_func(mycli, lambda: True, None) + toolbar = clitoolbar.create_toolbar_tokens_func(mycli, lambda: True, None, mycli.get_custom_toolbar) result = toolbar() assert ("class:bottom-toolbar", "right-arrow accepts full-line suggestion") in result @@ -44,7 +44,7 @@ def test_create_toolbar_tokens_func_shows_initial_help() -> None: def test_create_toolbar_tokens_func_clears_toolbar_error_message() -> None: mycli = make_mycli(toolbar_error_message="boom") - toolbar = clitoolbar.create_toolbar_tokens_func(mycli, lambda: False, None) + toolbar = clitoolbar.create_toolbar_tokens_func(mycli, lambda: False, None, mycli.get_custom_toolbar) first = toolbar() second = toolbar() @@ -64,7 +64,7 @@ def test_create_toolbar_tokens_func_shows_multiline_vi_and_refreshing(monkeypatc monkeypatch.setattr(clitoolbar.special, 'get_current_delimiter', lambda: '$$') monkeypatch.setattr(clitoolbar, '_get_vi_mode', lambda: 'N') - toolbar = clitoolbar.create_toolbar_tokens_func(mycli, lambda: False, None) + toolbar = clitoolbar.create_toolbar_tokens_func(mycli, lambda: False, None, mycli.get_custom_toolbar) result = toolbar() assert ("class:bottom-toolbar.off", "OFF") in result @@ -84,7 +84,7 @@ def test_create_toolbar_tokens_func_applies_custom_format(monkeypatch) -> None: to_formatted_text = MagicMock(return_value=formatted) monkeypatch.setattr(clitoolbar, 'to_formatted_text', to_formatted_text) - toolbar = clitoolbar.create_toolbar_tokens_func(mycli, lambda: True, r'\Bfmt') + toolbar = clitoolbar.create_toolbar_tokens_func(mycli, lambda: True, r'\Bfmt', mycli.get_custom_toolbar) result = toolbar() mycli.get_custom_toolbar.assert_called_once_with('fmt') @@ -103,7 +103,7 @@ def test_create_toolbar_tokens_func_replaces_default_toolbar_for_plain_custom_fo to_formatted_text = MagicMock(return_value=formatted) monkeypatch.setattr(clitoolbar, 'to_formatted_text', to_formatted_text) - toolbar = clitoolbar.create_toolbar_tokens_func(mycli, lambda: True, 'fmt') + toolbar = clitoolbar.create_toolbar_tokens_func(mycli, lambda: True, 'fmt', mycli.get_custom_toolbar) result = toolbar() mycli.get_custom_toolbar.assert_called_once_with('fmt') diff --git a/test/pytests/test_main.py b/test/pytests/test_main.py index 1c4562b7..6f80b0f4 100644 --- a/test/pytests/test_main.py +++ b/test/pytests/test_main.py @@ -21,6 +21,7 @@ TEST_DATABASE, ) from mycli.main import EMPTY_PASSWORD_FLAG_SENTINEL, MyCli, click_entrypoint +import mycli.main_modes.repl as repl_mode import mycli.packages.special from mycli.packages.special.main import COMMANDS as SPECIAL_COMMANDS from mycli.packages.sqlresult import SQLResult @@ -368,7 +369,7 @@ def test_prompt_no_host_only_socket(executor): mycli.sqlexecute.user = DEFAULT_USER mycli.sqlexecute.dbname = DEFAULT_DATABASE mycli.sqlexecute.port = DEFAULT_PORT - prompt = mycli.get_prompt(mycli.prompt_format, 0) + prompt = repl_mode.get_prompt(mycli, mycli.prompt_format, 0) assert prompt == f"MySQL {DEFAULT_USER}@{DEFAULT_HOST}:{DEFAULT_DATABASE}> " @@ -383,7 +384,7 @@ def test_prompt_socket_overrides_port(executor): mycli.sqlexecute.user = DEFAULT_USER mycli.sqlexecute.dbname = DEFAULT_DATABASE mycli.sqlexecute.port = DEFAULT_PORT - prompt = mycli.get_prompt(mycli.prompt_format, 0) + prompt = repl_mode.get_prompt(mycli, mycli.prompt_format, 0) assert prompt == f"MySQL {DEFAULT_USER}@{DEFAULT_HOST}:mysqld.sock {DEFAULT_DATABASE}> " @@ -398,7 +399,7 @@ def test_prompt_socket_short_host(executor): mycli.sqlexecute.user = DEFAULT_USER mycli.sqlexecute.dbname = DEFAULT_DATABASE mycli.sqlexecute.port = DEFAULT_PORT - prompt = mycli.get_prompt(mycli.prompt_format, 0) + prompt = repl_mode.get_prompt(mycli, mycli.prompt_format, 0) assert prompt == f"MySQL {DEFAULT_USER}@{DEFAULT_HOST}:{DEFAULT_PORT} {DEFAULT_DATABASE}> " diff --git a/test/pytests/test_main_modes_repl.py b/test/pytests/test_main_modes_repl.py index 496fa2c9..919aa575 100644 --- a/test/pytests/test_main_modes_repl.py +++ b/test/pytests/test_main_modes_repl.py @@ -28,6 +28,10 @@ def error(self, *args: Any, **kwargs: Any) -> None: self.error_calls.append((args, kwargs)) +class HashableNamespace: + pass + + @dataclass class DummyFormatterWithQuery: query: str = '' @@ -129,7 +133,7 @@ def open(self, mode: str) -> StringIO: def make_repl_cli(sqlexecute: Any | None = None) -> Any: - cli = SimpleNamespace() + cli: Any = HashableNamespace() cli.logger = DummyLogger() cli.query_history = [] cli.last_prompt_message = repl_mode.ANSI('') @@ -157,6 +161,13 @@ def make_repl_cli(sqlexecute: Any | None = None) -> Any: cli.config = {'history_file': '~/.mycli-history-testing'} cli.key_bindings = 'emacs' cli.wider_completion_menu = False + cli.login_path = None + cli.login_path_as_host = False + cli.dsn_alias = None + cli.terminal_tab_title_format = '' + cli.terminal_window_title_format = '' + cli.multiplex_window_title_format = '' + cli.multiplex_pane_title_format = '' cli._completer_lock = ReusableLock() cli.completer = object() cli.syntax_style = 'native' @@ -191,7 +202,6 @@ def refresh_completions(reset: bool = False) -> list[SQLResult]: return [SQLResult(status='refresh')] cli.refresh_completions = refresh_completions - cli.set_all_external_titles = lambda: setattr(cli, 'title_calls', cli.title_calls + 1) def output_timing(timing: str, is_warnings_style: bool = False) -> None: cli.timing_calls.append((timing, is_warnings_style)) @@ -218,7 +228,6 @@ def output(formatted: Any, result: Any, is_warnings_style: bool = False) -> None cli.output_calls.append((list(formatted), result, is_warnings_style)) cli.output = output - cli.get_prompt = lambda string, render_counter: f'{string}:{render_counter}' return cli @@ -320,7 +329,11 @@ def test_repl_show_startup_banner_and_prompt_helpers(monkeypatch: pytest.MonkeyP assert any('Thanks to the contributor' in line for line in printed) assert any('Tip — Tip' in line for line in printed) - cli.get_prompt = lambda string, render_counter: '0123456' if string == cli.default_prompt else 'a\nb' + monkeypatch.setattr( + repl_mode, + 'get_prompt', + lambda mycli, string, render_counter: '0123456' if string == cli.default_prompt else 'a\nb', + ) cli.max_len_prompt = 5 prompt_text = to_plain_text(repl_mode._get_prompt_message(cli, cast(Any, FakeApp(text='', render_counter=2)))) assert prompt_text == 'a\nb' @@ -331,7 +344,7 @@ def test_repl_show_startup_banner_and_prompt_helpers(monkeypatch: pytest.MonkeyP cli.prompt_format = 'custom' cli.prompt_lines = 0 - cli.get_prompt = lambda string, render_counter: 'single' + monkeypatch.setattr(repl_mode, 'get_prompt', lambda mycli, string, render_counter: 'single') assert to_plain_text(repl_mode._get_prompt_message(cli, cast(Any, FakeApp(text='', render_counter=4)))) == 'single' assert cli.prompt_lines == 1 @@ -342,6 +355,164 @@ def test_repl_show_startup_banner_and_prompt_helpers(monkeypatch: pytest.MonkeyP assert repl_mode._get_continuation(cli, 4, 0, 0) == [('class:continuation', ' ')] +def test_prompt_toolbar_and_title_helpers(monkeypatch: pytest.MonkeyPatch) -> None: + class PromptCursor: + def __enter__(self) -> 'PromptCursor': + return self + + def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> Literal[False]: + return False + + class PromptConnection: + def cursor(self) -> PromptCursor: + return PromptCursor() + + sqlexecute = SimpleNamespace( + user='alice', + host='127.0.0.1', + dbname='db', + port=3307, + socket='/tmp/mysql.sock', + server_info=SimpleNamespace(species=SimpleNamespace(name='TiDB')), + conn=None, + ) + cli = make_repl_cli(sqlexecute) + cli.login_path = 'prod' + cli.login_path_as_host = True + cli.dsn_alias = 'dsn' + prompt = repl_mode.get_prompt(cli, r'\h|\H|\A|\y|\Y|\T|\w|\W', 0) + assert prompt == 'prod|prod|dsn|(none)|(none)|(none)|(none)|' + + sqlexecute.conn = PromptConnection() + cli.login_path_as_host = False + monkeypatch.setattr(repl_mode, 'get_uptime', lambda cur: 123) + monkeypatch.setattr(repl_mode, 'format_uptime', lambda uptime: f'uptime:{uptime}') + monkeypatch.setattr(repl_mode, 'get_ssl_version', lambda cur: 'TLSv1.3') + monkeypatch.setattr(repl_mode, 'get_warning_count', lambda cur: 7) + prompt = repl_mode.get_prompt(cli, r'\H|\y|\Y|\T|\w|\W', 1) + assert prompt == '127.0.0.1|123|uptime:123|TLSv1.3|7|7' + + cli.prompt_session = None + assert to_plain_text(repl_mode.get_custom_toolbar(cli, 'fmt')) == '' + cli.prompt_session = cast(Any, SimpleNamespace(app=None)) + assert to_plain_text(repl_mode.get_custom_toolbar(cli, 'fmt')) == '' + + cli.prompt_session = cast(Any, FakePromptSession()) + cli.last_custom_toolbar_message = repl_mode.ANSI('cached') + cli.prompt_session.app.current_buffer.text = 'typing' + assert repl_mode.get_custom_toolbar(cli, 'fmt') == cli.last_custom_toolbar_message + + cli.prompt_session.app.current_buffer.text = '' + monkeypatch.setattr(repl_mode, 'get_prompt', lambda mycli, string, render_counter: f'title:{string}') + assert 'title:fmt' in str(repl_mode.get_custom_toolbar(cli, 'fmt')) + + cli.terminal_tab_title_format = 'tab' + cli.terminal_window_title_format = 'window' + cli.multiplex_window_title_format = 'mux-window' + cli.multiplex_pane_title_format = 'mux-pane' + monkeypatch.setattr(repl_mode, 'sanitize_terminal_title', lambda title: title.upper()) + monkeypatch.setattr(repl_mode.sys.stderr, 'isatty', lambda: True) + printed: list[str] = [] + monkeypatch.setattr(builtins, 'print', lambda *args, **kwargs: printed.append(args[0])) + tmux_calls: list[tuple[Any, ...]] = [] + monkeypatch.setattr(repl_mode.subprocess, 'run', lambda *args, **kwargs: tmux_calls.append(args)) + monkeypatch.setenv('TMUX', '1') + repl_mode.set_all_external_titles(cli) + assert printed[0].startswith('\x1b]1;TITLE:TAB') + assert printed[1].startswith('\x1b]2;TITLE:WINDOW') + assert printed[2].startswith('\x1b]2;TITLE:MUX-PANE') + assert tmux_calls + + monkeypatch.setattr(repl_mode.sys.stderr, 'isatty', lambda: False) + repl_mode.set_external_terminal_tab_title(cli) + repl_mode.set_external_terminal_window_title(cli) + repl_mode.set_external_multiplex_pane_title(cli) + monkeypatch.delenv('TMUX', raising=False) + repl_mode.set_external_multiplex_window_title(cli) + monkeypatch.setenv('TMUX', '1') + monkeypatch.setattr(repl_mode.subprocess, 'run', lambda *args, **kwargs: (_ for _ in ()).throw(FileNotFoundError())) + repl_mode.set_external_multiplex_window_title(cli) + + +def test_prompt_and_title_helper_early_returns_and_remaining_prompt_branches(monkeypatch: pytest.MonkeyPatch) -> None: + class PromptCursor: + def __enter__(self) -> 'PromptCursor': + return self + + def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> Literal[False]: + return False + + class PromptConnection: + def cursor(self) -> PromptCursor: + return PromptCursor() + + cli = make_repl_cli( + SimpleNamespace( + user='alice', + host=None, + dbname='db', + port=3306, + socket=None, + server_info=SimpleNamespace(species=SimpleNamespace(name='MySQL')), + conn=PromptConnection(), + ) + ) + cli.prompt_session = cast(Any, FakePromptSession()) + + monkeypatch.setattr(repl_mode, 'get_uptime', lambda cur: 123) + monkeypatch.setattr(repl_mode, 'format_uptime', lambda uptime: f'uptime:{uptime}') + monkeypatch.setattr(repl_mode, 'get_ssl_version', lambda cur: 'TLSv1.3') + monkeypatch.setattr(repl_mode, 'get_warning_count', lambda cur: 7) + + prompt = repl_mode.get_prompt(cli, r'\h|\H|\y|\Y', 0) + assert prompt == f'{repl_mode.DEFAULT_HOST}|{repl_mode.DEFAULT_HOST}|123|uptime:123' + + prompt = repl_mode.get_prompt(cli, r'\h|\H|\w|\W', 1) + assert prompt == f'{repl_mode.DEFAULT_HOST}|{repl_mode.DEFAULT_HOST}|7|7' + + prompt = repl_mode.get_prompt(cli, r'\h|\H|\T', 2) + assert prompt == f'{repl_mode.DEFAULT_HOST}|{repl_mode.DEFAULT_HOST}|TLSv1.3' + + monkeypatch.setattr(repl_mode.sys.stderr, 'isatty', lambda: True) + monkeypatch.setattr(builtins, 'print', lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError('unexpected print'))) + monkeypatch.setattr( + repl_mode.subprocess, + 'run', + lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError('unexpected tmux call')), + ) + + cli.terminal_tab_title_format = '' + repl_mode.set_external_terminal_tab_title(cli) + cli.terminal_tab_title_format = 'tab' + cli.prompt_session = None + repl_mode.set_external_terminal_tab_title(cli) + + cli.prompt_session = cast(Any, FakePromptSession()) + cli.terminal_window_title_format = '' + repl_mode.set_external_terminal_window_title(cli) + cli.terminal_window_title_format = 'window' + cli.prompt_session = None + repl_mode.set_external_terminal_window_title(cli) + + cli.prompt_session = cast(Any, FakePromptSession()) + cli.multiplex_window_title_format = '' + repl_mode.set_external_multiplex_window_title(cli) + cli.multiplex_window_title_format = 'mux-window' + monkeypatch.setenv('TMUX', '1') + cli.prompt_session = None + repl_mode.set_external_multiplex_window_title(cli) + + cli.prompt_session = cast(Any, FakePromptSession()) + cli.multiplex_pane_title_format = '' + repl_mode.set_external_multiplex_pane_title(cli) + cli.multiplex_pane_title_format = 'mux-pane' + monkeypatch.delenv('TMUX', raising=False) + repl_mode.set_external_multiplex_pane_title(cli) + monkeypatch.setenv('TMUX', '1') + cli.prompt_session = None + repl_mode.set_external_multiplex_pane_title(cli) + + def test_output_results_covers_watch_warning_timing_beep_and_interrupts(monkeypatch: pytest.MonkeyPatch) -> None: class FakeSQLExecute: def run(self, text: str) -> list[SQLResult]: @@ -478,8 +649,9 @@ def fake_prompt_session(**kwargs: Any) -> FakePromptSession: monkeypatch.setattr(repl_mode, 'style_factory_ptoolkit', lambda *args, **kwargs: 'style') monkeypatch.setattr(repl_mode, 'cli_is_multiline', lambda mycli: False) - def fake_toolbar_tokens(mycli: Any, show_help: Any, fmt: str) -> str: + def fake_toolbar_tokens(mycli: Any, show_help: Any, fmt: str, custom_toolbar: Any) -> str: toolbar_help.append(show_help()) + assert callable(custom_toolbar) return 'toolbar' monkeypatch.setattr(repl_mode, 'create_toolbar_tokens_func', fake_toolbar_tokens) @@ -854,6 +1026,7 @@ def fake_one_iteration(mycli: Any, state: repl_mode.ReplState) -> None: closed: list[bool] = [] monkeypatch.setattr(repl_mode, '_one_iteration', fake_one_iteration) monkeypatch.setattr(repl_mode.special, 'close_tee', lambda: closed.append(True)) + monkeypatch.setattr(repl_mode, 'set_all_external_titles', lambda mycli: setattr(mycli, 'title_calls', mycli.title_calls + 1)) repl_mode.main_repl(cli) @@ -879,6 +1052,7 @@ def test_main_repl_covers_no_refresh_and_quiet_exit(monkeypatch: pytest.MonkeyPa ) monkeypatch.setattr(repl_mode, '_one_iteration', lambda mycli, state: (_ for _ in ()).throw(EOFError())) monkeypatch.setattr(repl_mode.special, 'close_tee', lambda: None) + monkeypatch.setattr(repl_mode, 'set_all_external_titles', lambda mycli: setattr(mycli, 'title_calls', mycli.title_calls + 1)) repl_mode.main_repl(cli) diff --git a/test/pytests/test_main_regression.py b/test/pytests/test_main_regression.py index 4452574b..501d5965 100644 --- a/test/pytests/test_main_regression.py +++ b/test/pytests/test_main_regression.py @@ -256,7 +256,6 @@ def make_bare_mycli() -> Any: cli.log_output = lambda *args, **kwargs: None # type: ignore[assignment] cli.configure_pager = lambda: None # type: ignore[assignment] cli.refresh_completions = lambda reset=False: [SQLResult(status='refresh')] # type: ignore[assignment] - cli.set_all_external_titles = lambda: None # type: ignore[assignment] cli.reconnect = lambda database='': False # type: ignore[assignment] return cli @@ -609,7 +608,11 @@ def test_change_db_handles_empty_same_new_and_backticks(monkeypatch: pytest.Monk changed_to: list[str] = [] cli.sqlexecute.change_db = lambda arg: changed_to.append(arg) # type: ignore[assignment] titles_called = {'count': 0} - cli.set_all_external_titles = lambda: titles_called.__setitem__('count', titles_called['count'] + 1) # type: ignore[assignment] + monkeypatch.setattr( + main, + 'set_all_external_titles', + lambda mycli: titles_called.__setitem__('count', titles_called['count'] + 1), + ) assert list(main.MyCli.change_db(cli, '')) == [] assert secho_calls[0][0][0] == 'No database selected' @@ -1145,7 +1148,8 @@ def failing_connect() -> None: prompt_session = FakePromptSession() prompt_session.app.render_counter = 3 cli.prompt_session = cast(Any, prompt_session) - cli.get_prompt = lambda string, render_counter: 'line1\nline2' # type: ignore[assignment] + monkeypatch.setattr(mycli.main_modes.repl, 'get_prompt', lambda mycli, string, render_counter: 'line1\nline2') + monkeypatch.setattr(main, 'get_prompt', lambda mycli, string, render_counter: 'line1\nline2') monkeypatch.setattr(main.special, 'is_timing_enabled', lambda: True) assert main.MyCli.get_output_margin(cli, 'status\nline') == 13 @@ -1163,24 +1167,24 @@ def failing_connect() -> None: assert printed_status cli.prompt_session = None - assert main.to_plain_text(main.MyCli.get_custom_toolbar(cli, 'fmt')) == '' + assert main.to_plain_text(mycli.main_modes.repl.get_custom_toolbar(cli, 'fmt')) == '' cli.prompt_session = cast(Any, SimpleNamespace(app=None)) - assert main.to_plain_text(main.MyCli.get_custom_toolbar(cli, 'fmt')) == '' + assert main.to_plain_text(mycli.main_modes.repl.get_custom_toolbar(cli, 'fmt')) == '' - monkeypatch.setattr(main.sys.stderr, 'isatty', lambda: False) + monkeypatch.setattr(mycli.main_modes.repl.sys.stderr, 'isatty', lambda: False) cli.prompt_session = cast(Any, FakePromptSession()) cli.terminal_tab_title_format = 'tab' cli.terminal_window_title_format = 'window' cli.multiplex_window_title_format = 'mux-window' cli.multiplex_pane_title_format = 'mux-pane' - main.MyCli.set_external_terminal_tab_title(cli) - main.MyCli.set_external_terminal_window_title(cli) + mycli.main_modes.repl.set_external_terminal_tab_title(cli) + mycli.main_modes.repl.set_external_terminal_window_title(cli) monkeypatch.delenv('TMUX', raising=False) - main.MyCli.set_external_multiplex_window_title(cli) - main.MyCli.set_external_multiplex_pane_title(cli) + mycli.main_modes.repl.set_external_multiplex_window_title(cli) + mycli.main_modes.repl.set_external_multiplex_pane_title(cli) monkeypatch.setenv('TMUX', '1') - monkeypatch.setattr(main.subprocess, 'run', lambda *args, **kwargs: (_ for _ in ()).throw(FileNotFoundError())) - main.MyCli.set_external_multiplex_window_title(cli) + monkeypatch.setattr(mycli.main_modes.repl.subprocess, 'run', lambda *args, **kwargs: (_ for _ in ()).throw(FileNotFoundError())) + mycli.main_modes.repl.set_external_multiplex_window_title(cli) def test_reconnect_first_and_second_passes(monkeypatch: pytest.MonkeyPatch) -> None: @@ -1241,7 +1245,7 @@ def test_get_prompt_and_completion_helper_fallbacks(monkeypatch: pytest.MonkeyPa cli.login_path = 'prod' cli.login_path_as_host = True cli.dsn_alias = 'dsn' - prompt = main.MyCli.get_prompt(cli, r'\h|\H|\A|\y|\Y|\T|\w|\W', 0) + prompt = mycli.main_modes.repl.get_prompt(cli, r'\h|\H|\A|\y|\Y|\T|\w|\W', 0) assert prompt == 'prod|prod|dsn|(none)|(none)|(none)|(none)|' class PromptCursor: @@ -1257,11 +1261,11 @@ def cursor(self) -> PromptCursor: sqlexecute.conn = cast(Any, PromptConnection()) cli.login_path_as_host = False - monkeypatch.setattr(main, 'get_uptime', lambda cur: 123) - monkeypatch.setattr(main, 'format_uptime', lambda uptime: f'uptime:{uptime}') - monkeypatch.setattr(main, 'get_ssl_version', lambda cur: 'TLSv1.3') - monkeypatch.setattr(main, 'get_warning_count', lambda cur: 7) - prompt = main.MyCli.get_prompt(cli, r'\H|\y|\Y|\T|\w|\W', 1) + monkeypatch.setattr(mycli.main_modes.repl, 'get_uptime', lambda cur: 123) + monkeypatch.setattr(mycli.main_modes.repl, 'format_uptime', lambda uptime: f'uptime:{uptime}') + monkeypatch.setattr(mycli.main_modes.repl, 'get_ssl_version', lambda cur: 'TLSv1.3') + monkeypatch.setattr(mycli.main_modes.repl, 'get_warning_count', lambda cur: 7) + prompt = mycli.main_modes.repl.get_prompt(cli, r'\H|\y|\Y|\T|\w|\W', 1) assert prompt == '127.0.0.1|123|uptime:123|TLSv1.3|7|7' @@ -1291,11 +1295,11 @@ def format_output(self, rows: Any, header: Any, format_name: str | None = None, cli.multiplex_window_title_format = 'mux-window' cli.multiplex_pane_title_format = 'mux-pane' monkeypatch.setenv('TMUX', '1') - monkeypatch.setattr(main.sys.stderr, 'isatty', lambda: True) - main.MyCli.set_external_terminal_tab_title(cli) - main.MyCli.set_external_terminal_window_title(cli) - main.MyCli.set_external_multiplex_window_title(cli) - main.MyCli.set_external_multiplex_pane_title(cli) + monkeypatch.setattr(mycli.main_modes.repl.sys.stderr, 'isatty', lambda: True) + mycli.main_modes.repl.set_external_terminal_tab_title(cli) + mycli.main_modes.repl.set_external_terminal_window_title(cli) + mycli.main_modes.repl.set_external_multiplex_window_title(cli) + mycli.main_modes.repl.set_external_multiplex_pane_title(cli) def test_output_uses_stdout_and_pager_paths(monkeypatch: pytest.MonkeyPatch) -> None: @@ -1329,12 +1333,13 @@ def test_output_uses_stdout_and_pager_paths(monkeypatch: pytest.MonkeyPatch) -> def test_format_sqlresult_output_and_prompt_helpers_cover_extra_branches(monkeypatch: pytest.MonkeyPatch) -> None: cli = make_bare_mycli() + real_get_prompt = mycli.main_modes.repl.get_prompt cli.main_formatter = DummyFormatter() cli.redirect_formatter = DummyFormatter() cli.get_reserved_space = lambda: 1 # type: ignore[assignment] - cli.get_prompt = lambda string, render_counter: 'a\nb' # type: ignore[assignment] cli.prompt_lines = 0 cli.prompt_session = None + monkeypatch.setattr(main, 'get_prompt', lambda mycli, string, render_counter: 'a\nb') monkeypatch.setattr(main, 'Cursor', FakeCursorBase) monkeypatch.setattr(main.special, 'is_timing_enabled', lambda: False) rows = FakeCursorBase(rows=[], rowcount=0, description=[('id', 3, None, None, None, None, None)]) @@ -1377,10 +1382,10 @@ def test_format_sqlresult_output_and_prompt_helpers_cover_extra_branches(monkeyp cli.terminal_window_title_format = '' cli.multiplex_window_title_format = '' cli.multiplex_pane_title_format = '' - main.MyCli.set_external_terminal_tab_title(cli) - main.MyCli.set_external_terminal_window_title(cli) - main.MyCli.set_external_multiplex_window_title(cli) - main.MyCli.set_external_multiplex_pane_title(cli) + mycli.main_modes.repl.set_external_terminal_tab_title(cli) + mycli.main_modes.repl.set_external_terminal_window_title(cli) + mycli.main_modes.repl.set_external_multiplex_window_title(cli) + mycli.main_modes.repl.set_external_multiplex_pane_title(cli) cli.sqlexecute = SimpleNamespace( server_info=SimpleNamespace(species=SimpleNamespace(name='MySQL')), @@ -1391,7 +1396,8 @@ def test_format_sqlresult_output_and_prompt_helpers_cover_extra_branches(monkeyp socket=None, conn=None, ) - prompt = main.MyCli.get_prompt(cli, '\\h \\H \\y \\Y \\T \\w \\W', 0) + monkeypatch.setattr(main, 'get_prompt', real_get_prompt) + prompt = mycli.main_modes.repl.get_prompt(cli, '\\h \\H \\y \\Y \\T \\w \\W', 0) assert main.DEFAULT_HOST in prompt assert '(none)' in prompt @@ -1426,28 +1432,28 @@ def test_completion_helpers_title_helpers_thanks_tips(monkeypatch: pytest.Monkey prompt_session = FakePromptSession() prompt_session.app.current_buffer.text = '' cli.prompt_session = cast(Any, prompt_session) - cli.get_prompt = lambda string, render_counter: f'title:{string}' # type: ignore[assignment] - monkeypatch.setattr(main, 'sanitize_terminal_title', lambda title: title.upper()) - monkeypatch.setattr(main.sys.stderr, 'isatty', lambda: True) + monkeypatch.setattr(mycli.main_modes.repl, 'get_prompt', lambda mycli, string, render_counter: f'title:{string}') + monkeypatch.setattr(mycli.main_modes.repl, 'sanitize_terminal_title', lambda title: title.upper()) + monkeypatch.setattr(mycli.main_modes.repl.sys.stderr, 'isatty', lambda: True) printed: list[str] = [] monkeypatch.setattr(builtins, 'print', lambda *args, **kwargs: printed.append(args[0])) - monkeypatch.setattr(main.subprocess, 'run', lambda *args, **kwargs: None) + monkeypatch.setattr(mycli.main_modes.repl.subprocess, 'run', lambda *args, **kwargs: None) monkeypatch.setenv('TMUX', '1') cli.terminal_tab_title_format = 'tab' cli.terminal_window_title_format = 'window' cli.multiplex_window_title_format = 'mux-window' cli.multiplex_pane_title_format = 'mux-pane' - main.MyCli.set_all_external_titles(cli) + mycli.main_modes.repl.set_all_external_titles(cli) assert printed[0].startswith('\x1b]1;TITLE:TAB') assert printed[1].startswith('\x1b]2;TITLE:WINDOW') assert printed[2].startswith('\x1b]2;TITLE:MUX-PANE') - monkeypatch.setattr(main.sys.stderr, 'isatty', lambda: False) - main.MyCli.set_external_multiplex_pane_title(cli) + monkeypatch.setattr(mycli.main_modes.repl.sys.stderr, 'isatty', lambda: False) + mycli.main_modes.repl.set_external_multiplex_pane_title(cli) cli.prompt_session.app.current_buffer.text = 'in progress' - assert main.MyCli.get_custom_toolbar(cli, 'x') == cli.last_custom_toolbar_message + assert mycli.main_modes.repl.get_custom_toolbar(cli, 'x') == cli.last_custom_toolbar_message cli.prompt_session.app.current_buffer.text = '' - assert 'title:x' in str(main.MyCli.get_custom_toolbar(cli, 'x')) + assert 'title:x' in str(mycli.main_modes.repl.get_custom_toolbar(cli, 'x')) new_completer = cast(Any, SimpleNamespace(get_completions=lambda document, event: ['done'])) main.MyCli._on_completions_refreshed(cli, new_completer) @@ -2175,8 +2181,12 @@ def test_run_cli_prompt_rendering_startup_modes_and_goodbye(monkeypatch: pytest. cli.multiline_continuation_char = '>' cli.max_len_prompt = 5 cli.config = {'history_file': '~/.mycli-history-testing'} - cli.get_prompt = lambda string, render_counter: '0123456789' if string == cli.default_prompt else 'a\nb' # type: ignore[assignment] - cli.set_all_external_titles = lambda: None # type: ignore[assignment] + monkeypatch.setattr( + mycli.main_modes.repl, + 'get_prompt', + lambda mycli, string, render_counter: '0123456789' if string == cli.default_prompt else 'a\nb', + ) + monkeypatch.setattr(mycli.main_modes.repl, 'set_all_external_titles', lambda mycli: None) toolbar_help: list[bool] = [] prints: list[str] = [] prompt_messages: list[str] = [] @@ -2214,7 +2224,7 @@ def fake_prompt_session(**kwargs: Any) -> InspectPromptSession: monkeypatch.setattr(mycli.main_modes.repl, 'PromptSession', fake_prompt_session) monkeypatch.setattr(mycli.main_modes.repl, 'mycli_bindings', lambda mycli: 'bindings') - def fake_create_toolbar_tokens(mycli: Any, show_help: Any, fmt: str) -> str: + def fake_create_toolbar_tokens(mycli: Any, show_help: Any, fmt: str, custom_toolbar: Any) -> str: toolbar_help.append(show_help()) return 'toolbar'