From 1e2fb9af1214e6abfb3e36767ac83995d7be8502 Mon Sep 17 00:00:00 2001 From: Dick Marinus Date: Sun, 18 Nov 2018 20:56:15 +0100 Subject: [PATCH] prompt-toolkit 1.0 patches --- mycli/clibuffer.py | 24 ++-- mycli/clistyle.py | 120 ++--------------- mycli/clitoolbar.py | 54 ++++---- mycli/filters.py | 12 ++ mycli/key_bindings.py | 83 +++++++----- mycli/main.py | 194 +++++++++++++-------------- mycli/myclirc | 55 ++++---- mycli/packages/filepaths.py | 6 +- mycli/packages/parseutils.py | 8 -- mycli/packages/prompt_utils.py | 1 - setup.py | 2 +- test/features/steps/crud_table.py | 4 +- test/features/steps/iocommands.py | 2 +- test/features/steps/named_queries.py | 12 +- test/test_clistyle.py | 3 - test/test_main.py | 4 +- 16 files changed, 251 insertions(+), 333 deletions(-) create mode 100644 mycli/filters.py diff --git a/mycli/clibuffer.py b/mycli/clibuffer.py index f6cc737a..41a63df1 100644 --- a/mycli/clibuffer.py +++ b/mycli/clibuffer.py @@ -1,21 +1,17 @@ -from __future__ import unicode_literals - -from prompt_toolkit.enums import DEFAULT_BUFFER +from prompt_toolkit.buffer import Buffer from prompt_toolkit.filters import Condition -from prompt_toolkit.application import get_app -from .packages.parseutils import is_open_quote +class CLIBuffer(Buffer): + def __init__(self, always_multiline, *args, **kwargs): + self.always_multiline = always_multiline -def cli_is_multiline(mycli): - @Condition - def cond(): - doc = get_app().layout.get_buffer_by_name(DEFAULT_BUFFER).document + @Condition + def is_multiline(): + doc = self.document + return self.always_multiline and not _multiline_exception(doc.text) - if not mycli.multi_line: - return False - else: - return not _multiline_exception(doc.text) - return cond + super(self.__class__, self).__init__(*args, is_multiline=is_multiline, + tempfile_suffix='.sql', **kwargs) def _multiline_exception(text): orig = text diff --git a/mycli/clistyle.py b/mycli/clistyle.py index 6f8b03af..89caf71a 100644 --- a/mycli/clistyle.py +++ b/mycli/clistyle.py @@ -1,120 +1,28 @@ -from __future__ import unicode_literals - -import logging - +import pygments.style import pygments.styles -from pygments.token import string_to_tokentype, Token -from pygments.style import Style as PygmentsStyle +from pygments.token import string_to_tokentype from pygments.util import ClassNotFound -from prompt_toolkit.styles.pygments import style_from_pygments_cls -from prompt_toolkit.styles import merge_styles, Style - -logger = logging.getLogger(__name__) - -# map Pygments tokens (ptk 1.0) to class names (ptk 2.0). -TOKEN_TO_PROMPT_STYLE = { - Token.Menu.Completions.Completion.Current: 'completion-menu.completion.current', - Token.Menu.Completions.Completion: 'completion-menu.completion', - Token.Menu.Completions.Meta.Current: 'completion-menu.meta.completion.current', - Token.Menu.Completions.Meta: 'completion-menu.meta.completion', - Token.Menu.Completions.MultiColumnMeta: 'completion-menu.multi-column-meta', - Token.Menu.Completions.ProgressButton: 'scrollbar.arrow', # best guess - Token.Menu.Completions.ProgressBar: 'scrollbar', # best guess - Token.SelectedText: 'selected', - Token.SearchMatch: 'search', - Token.SearchMatch.Current: 'search.current', - Token.Toolbar: 'bottom-toolbar', - Token.Toolbar.Off: 'bottom-toolbar.off', - Token.Toolbar.On: 'bottom-toolbar.on', - Token.Toolbar.Search: 'search-toolbar', - Token.Toolbar.Search.Text: 'search-toolbar.text', - Token.Toolbar.System: 'system-toolbar', - Token.Toolbar.Arg: 'arg-toolbar', - Token.Toolbar.Arg.Text: 'arg-toolbar.text', - Token.Toolbar.Transaction.Valid: 'bottom-toolbar.transaction.valid', - Token.Toolbar.Transaction.Failed: 'bottom-toolbar.transaction.failed', - Token.Output.Header: 'output.header', - Token.Output.OddRow: 'output.odd-row', - Token.Output.EvenRow: 'output.even-row', - Token.Prompt: 'prompt', - Token.Continuation: 'continuation', -} -# reverse dict for cli_helpers, because they still expect Pygments tokens. -PROMPT_STYLE_TO_TOKEN = { - v: k for k, v in TOKEN_TO_PROMPT_STYLE.items() -} +def style_factory(name, cli_style): + """Create a Pygments Style class based on the user's preferences. -def parse_pygments_style(token_name, style_object, style_dict): - """Parse token type and style string. - - :param token_name: str name of Pygments token. Example: "Token.String" - :param style_object: pygments.style.Style instance to use as base - :param style_dict: dict of token names and their styles, customized to this cli + :param str name: The name of a built-in Pygments style. + :param dict cli_style: The user's token-type style preferences. """ - token_type = string_to_tokentype(token_name) - try: - other_token_type = string_to_tokentype(style_dict[token_name]) - return token_type, style_object.styles[other_token_type] - except AttributeError as err: - return token_type, style_dict[token_name] - - -def style_factory(name, cli_style): try: style = pygments.styles.get_style_by_name(name) except ClassNotFound: style = pygments.styles.get_style_by_name('native') - prompt_styles = [] - # prompt-toolkit used pygments tokens for styling before, switched to style - # names in 2.0. Convert old token types to new style names, for backwards compatibility. - for token in cli_style: - if token.startswith('Token.'): - # treat as pygments token (1.0) - token_type, style_value = parse_pygments_style( - token, style, cli_style) - if token_type in TOKEN_TO_PROMPT_STYLE: - prompt_style = TOKEN_TO_PROMPT_STYLE[token_type] - prompt_styles.append((prompt_style, style_value)) - else: - # we don't want to support tokens anymore - logger.error('Unhandled style / class name: %s', token) - else: - # treat as prompt style name (2.0). See default style names here: - # https://github.com/jonathanslenders/python-prompt-toolkit/blob/master/prompt_toolkit/styles/defaults.py - prompt_styles.append((token, cli_style[token])) - - override_style = Style([('bottom-toolbar', 'noreverse')]) - return merge_styles([ - style_from_pygments_cls(style), - override_style, - Style(prompt_styles) - ]) - - -def style_factory_output(name, cli_style): - try: - style = pygments.styles.get_style_by_name(name).styles - except ClassNotFound: - style = pygments.styles.get_style_by_name('native').styles - - for token in cli_style: - if token.startswith('Token.'): - token_type, style_value = parse_pygments_style( - token, style, cli_style) - style.update({token_type: style_value}) - elif token in PROMPT_STYLE_TO_TOKEN: - token_type = PROMPT_STYLE_TO_TOKEN[token] - style.update({token_type: cli_style[token]}) - else: - # TODO: cli helpers will have to switch to ptk.Style - logger.error('Unhandled style / class name: %s', token) + style_tokens = {} + style_tokens.update(style.styles) + custom_styles = {string_to_tokentype(x): y for x, y in cli_style.items()} + style_tokens.update(custom_styles) - class OutputStyle(PygmentsStyle): - default_style = "" - styles = style + class MycliStyle(pygments.style.Style): + default_styles = '' + styles = style_tokens - return OutputStyle + return MycliStyle diff --git a/mycli/clitoolbar.py b/mycli/clitoolbar.py index 89e6afa0..bb638d55 100644 --- a/mycli/clitoolbar.py +++ b/mycli/clitoolbar.py @@ -1,48 +1,48 @@ -from __future__ import unicode_literals - +from pygments.token import Token +from prompt_toolkit.enums import DEFAULT_BUFFER, EditingMode from prompt_toolkit.key_binding.vi_state import InputMode -from prompt_toolkit.application import get_app -from prompt_toolkit.enums import EditingMode -def create_toolbar_tokens_func(mycli, show_fish_help): - """Return a function that generates the toolbar tokens.""" - def get_toolbar_tokens(): - result = [] - result.append(('class:bottom-toolbar', ' ')) +def create_toolbar_tokens_func(get_is_refreshing, show_fish_help): + """ + Return a function that generates the toolbar tokens. + """ + token = Token.Toolbar - if mycli.multi_line: - result.append( - ('class:bottom-toolbar', ' (Semi-colon [;] will end the line) ')) + def get_toolbar_tokens(cli): + result = [] + result.append((token, ' ')) - if mycli.multi_line: - result.append(('class:bottom-toolbar.on', '[F3] Multiline: ON ')) + if cli.buffers[DEFAULT_BUFFER].always_multiline: + result.append((token.On, '[F3] Multiline: ON ')) else: - result.append(('class:bottom-toolbar.off', - '[F3] Multiline: OFF ')) - if mycli.prompt_app.editing_mode == EditingMode.VI: + result.append((token.Off, '[F3] Multiline: OFF ')) + + if cli.buffers[DEFAULT_BUFFER].always_multiline: + result.append((token, + ' (Semi-colon [;] will end the line)')) + + if cli.editing_mode == EditingMode.VI: result.append(( - 'class:botton-toolbar.on', - 'Vi-mode ({})'.format(_get_vi_mode()) + token.On, + 'Vi-mode ({})'.format(_get_vi_mode(cli)) )) if show_fish_help(): - result.append( - ('class:bottom-toolbar', ' Right-arrow to complete suggestion')) + result.append((token, ' Right-arrow to complete suggestion')) - if mycli.completion_refresher.is_refreshing(): - result.append( - ('class:bottom-toolbar', ' Refreshing completions...')) + if get_is_refreshing(): + result.append((token, ' Refreshing completions...')) return result return get_toolbar_tokens -def _get_vi_mode(): +def _get_vi_mode(cli): """Get the current vi mode for display.""" return { InputMode.INSERT: 'I', InputMode.NAVIGATION: 'N', InputMode.REPLACE: 'R', - InputMode.INSERT_MULTIPLE: 'M', - }[get_app().vi_state.input_mode] + InputMode.INSERT_MULTIPLE: 'M' + }[cli.vi_state.input_mode] diff --git a/mycli/filters.py b/mycli/filters.py new file mode 100644 index 00000000..6a8075ff --- /dev/null +++ b/mycli/filters.py @@ -0,0 +1,12 @@ +from prompt_toolkit.filters import Filter + +class HasSelectedCompletion(Filter): + """Enable when the current buffer has a selected completion.""" + + def __call__(self, cli): + complete_state = cli.current_buffer.complete_state + return (complete_state is not None and + complete_state.current_completion is not None) + + def __repr__(self): + return "HasSelectedCompletion()" diff --git a/mycli/key_bindings.py b/mycli/key_bindings.py index abc559ff..33bb4f2f 100644 --- a/mycli/key_bindings.py +++ b/mycli/key_bindings.py @@ -1,50 +1,65 @@ -from __future__ import unicode_literals import logging from prompt_toolkit.enums import EditingMode -from prompt_toolkit.filters import completion_is_selected -from prompt_toolkit.key_binding import KeyBindings +from prompt_toolkit.keys import Keys +from prompt_toolkit.key_binding.manager import KeyBindingManager +from .filters import HasSelectedCompletion _logger = logging.getLogger(__name__) -def mycli_bindings(mycli): - """Custom key bindings for mycli.""" - kb = KeyBindings() +def mycli_bindings(): + """ + Custom key bindings for mycli. + """ + key_binding_manager = KeyBindingManager( + enable_open_in_editor=True, + enable_system_bindings=True, + enable_auto_suggest_bindings=True, + enable_search=True, + enable_abort_and_exit_bindings=True) - @kb.add('f2') + @key_binding_manager.registry.add_binding(Keys.F2) def _(event): - """Enable/Disable SmartCompletion Mode.""" + """ + Enable/Disable SmartCompletion Mode. + """ _logger.debug('Detected F2 key.') - mycli.completer.smart_completion = not mycli.completer.smart_completion + buf = event.cli.current_buffer + buf.completer.smart_completion = not buf.completer.smart_completion - @kb.add('f3') + @key_binding_manager.registry.add_binding(Keys.F3) def _(event): - """Enable/Disable Multiline Mode.""" + """ + Enable/Disable Multiline Mode. + """ _logger.debug('Detected F3 key.') - mycli.multi_line = not mycli.multi_line + buf = event.cli.current_buffer + buf.always_multiline = not buf.always_multiline - @kb.add('f4') + @key_binding_manager.registry.add_binding(Keys.F4) def _(event): - """Toggle between Vi and Emacs mode.""" + """ + Toggle between Vi and Emacs mode. + """ _logger.debug('Detected F4 key.') - if mycli.key_bindings == "vi": - event.app.editing_mode = EditingMode.EMACS - mycli.key_bindings = "emacs" + if event.cli.editing_mode == EditingMode.VI: + event.cli.editing_mode = EditingMode.EMACS else: - event.app.editing_mode = EditingMode.VI - mycli.key_bindings = "vi" + event.cli.editing_mode = EditingMode.VI - @kb.add('tab') + @key_binding_manager.registry.add_binding(Keys.Tab) def _(event): - """Force autocompletion at cursor.""" + """ + Force autocompletion at cursor. + """ _logger.debug('Detected key.') - b = event.app.current_buffer + b = event.cli.current_buffer if b.complete_state: b.complete_next() else: - b.start_completion(select_first=True) + event.cli.start_completion(select_first=True) - @kb.add('c-space') + @key_binding_manager.registry.add_binding(Keys.ControlSpace) def _(event): """ Initialize autocompletion at cursor. @@ -56,25 +71,21 @@ def _(event): """ _logger.debug('Detected key.') - b = event.app.current_buffer + b = event.cli.current_buffer if b.complete_state: b.complete_next() else: - b.start_completion(select_first=False) + event.cli.start_completion(select_first=False) - @kb.add('enter', filter=completion_is_selected) + @key_binding_manager.registry.add_binding(Keys.ControlJ, filter=HasSelectedCompletion()) def _(event): - """Makes the enter key work as the tab key only when showing the menu. - - In other words, don't execute query when enter is pressed in - the completion dropdown menu, instead close the dropdown menu - (accept current selection). - """ - _logger.debug('Detected enter key.') + Makes the enter key work as the tab key only when showing the menu. + """ + _logger.debug('Detected key.') event.current_buffer.complete_state = None - b = event.app.current_buffer + b = event.cli.current_buffer b.complete_state = None - return kb + return key_binding_manager diff --git a/mycli/main.py b/mycli/main.py index 2e8b9bf7..203d8d37 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -15,17 +15,18 @@ from cli_helpers.tabular_output import preprocessors import click import sqlparse -from prompt_toolkit.completion import DynamicCompleter +from prompt_toolkit import CommandLineInterface, Application, AbortAction +from prompt_toolkit.interface import AcceptAction from prompt_toolkit.enums import DEFAULT_BUFFER, EditingMode -from prompt_toolkit.shortcuts import PromptSession, CompleteStyle -from prompt_toolkit.styles.pygments import style_from_pygments_cls +from prompt_toolkit.shortcuts import create_prompt_layout, create_eventloop +from prompt_toolkit.styles.from_pygments import style_from_pygments from prompt_toolkit.document import Document -from prompt_toolkit.filters import HasFocus, IsDone +from prompt_toolkit.filters import Always, HasFocus, IsDone from prompt_toolkit.layout.processors import (HighlightMatchingBracketProcessor, ConditionalProcessor) -from prompt_toolkit.lexers import PygmentsLexer from prompt_toolkit.history import FileHistory from prompt_toolkit.auto_suggest import AutoSuggestFromHistory +from pygments.token import Token from .packages.special.main import NO_QUERY from .packages.prompt_utils import confirm, confirm_destructive_query, prompt @@ -33,9 +34,9 @@ import mycli.packages.special as special from .sqlcompleter import SQLCompleter from .clitoolbar import create_toolbar_tokens_func -from .clistyle import style_factory, style_factory_output +from .clistyle import style_factory from .sqlexecute import FIELD_TYPES, SQLExecute -from .clibuffer import cli_is_multiline +from .clibuffer import CLIBuffer from .completion_refresher import CompletionRefresher from .config import (write_default_config, get_mylogin_cnf_path, open_mylogin_cnf, read_config_files, str_to_bool) @@ -124,10 +125,7 @@ def __init__(self, sqlexecute=None, prompt=None, self.syntax_style = c['main']['syntax_style'] self.less_chatty = c['main'].as_bool('less_chatty') self.cli_style = c['colors'] - self.output_style = style_factory_output( - self.syntax_style, - self.cli_style - ) + self.output_style = style_factory(self.syntax_style, self.cli_style) self.wider_completion_menu = c['main'].as_bool('wider_completion_menu') c_dest_warning = c['main'].as_bool('destructive_warning') self.destructive_warning = c_dest_warning if warn is None else warn @@ -185,7 +183,7 @@ def __init__(self, sqlexecute=None, prompt=None, # There was an error reading the login path file. print('Error: Unable to read login path file.') - self.prompt_app = None + self.cli = None def register_special_commands(self): special.register_special_command(self.change_db, 'use', @@ -463,36 +461,37 @@ def _connect(): self.echo(str(e), err=True, fg='red') exit(1) - def handle_editor_command(self, text): - """Editor command is any query that is prefixed or suffixed by a '\e'. - The reason for a while loop is because a user might edit a query - multiple times. For eg: - + def handle_editor_command(self, cli, document): + """ + Editor command is any query that is prefixed or suffixed + by a '\e'. The reason for a while loop is because a user + might edit a query multiple times. + For eg: "select * from \e" to edit it in vim, then come back to the prompt with the edited query "select * from blah where q = 'abc'\e" to edit it again. - :param text: Document + :param cli: CommandLineInterface + :param document: Document :return: Document - """ - - while special.editor_command(text): - filename = special.get_filename(text) - query = (special.get_editor_query(text) or + # FIXME: using application.pre_run_callables like this here is not the best solution. + # It's internal api of prompt_toolkit that may change. This was added to fix + # https://github.com/dbcli/pgcli/issues/668. We may find a better way to do it in the future. + saved_callables = cli.application.pre_run_callables + while special.editor_command(document.text): + filename = special.get_filename(document.text) + query = (special.get_editor_query(document.text) or self.get_last_query()) sql, message = special.open_external_editor(filename, sql=query) if message: # Something went wrong. Raise an exception and bail. raise RuntimeError(message) - while True: - try: - text = self.prompt_app.prompt(default=sql) - break - except KeyboardInterrupt: - sql = "" - + cli.current_buffer.document = Document(sql, cursor_position=len(sql)) + cli.application.pre_run_callables = [] + document = cli.run() continue - return text + cli.application.pre_run_callables = saved_callables + return document def run_cli(self): iterations = 0 @@ -517,7 +516,7 @@ def run_cli(self): 'Your query history will not be saved.'.format(history_file), err=True, fg='red') - key_bindings = mycli_bindings(self) + key_binding_manager = mycli_bindings() if not self.less_chatty: print(' '.join(sqlexecute.server_type())) @@ -527,41 +526,38 @@ def run_cli(self): print('Home: http://mycli.net') print('Thanks to the contributor -', thanks_picker([author_file, sponsor_file])) - def get_message(): + def prompt_tokens(cli): prompt = self.get_prompt(self.prompt_format) if self.prompt_format == self.default_prompt and len(prompt) > self.max_len_prompt: prompt = self.get_prompt('\\d> ') - return [('class:prompt', prompt)] + return [(Token.Prompt, prompt)] - def get_continuation(width, line_number, is_soft_wrap): - continuation = ' ' * (width - 1) + ' ' - return [('class:continuation', continuation)] + def get_continuation_tokens(cli, width): + continuation_prompt = self.get_prompt(self.prompt_continuation_format) + return [(Token.Continuation, ' ' * (width - len(continuation_prompt)) + continuation_prompt)] def show_suggestion_tip(): return iterations < 2 - def one_iteration(text=None): - if text is None: - try: - text = self.prompt_app.prompt() - except KeyboardInterrupt: - return + def one_iteration(document=None): + if document is None: + document = self.cli.run() special.set_expanded_output(False) try: - text = self.handle_editor_command(text) + document = self.handle_editor_command(self.cli, document) except RuntimeError as e: - logger.error("sql: %r, error: %r", text, e) + logger.error("sql: %r, error: %r", document.text, e) logger.error("traceback: %r", traceback.format_exc()) self.echo(str(e), err=True, fg='red') return - if not text.strip(): + if not document.text.strip(): return if self.destructive_warning: - destroy = confirm_destructive_query(text) + destroy = confirm_destructive_query(document.text) if destroy is None: pass # Query was not destructive. Nothing to do here. elif destroy is True: @@ -576,18 +572,18 @@ def one_iteration(text=None): mutating = False try: - logger.debug('sql: %r', text) + logger.debug('sql: %r', document.text) - special.write_tee(self.get_prompt(self.prompt_format) + text) + special.write_tee(self.get_prompt(self.prompt_format) + document.text) if self.logfile: self.logfile.write('\n# %s\n' % datetime.now()) - self.logfile.write(text) + self.logfile.write(document.text) self.logfile.write('\n') successful = False start = time() - res = sqlexecute.run(text) - self.formatter.query = text + res = sqlexecute.run(document.text) + self.formatter.query = document.text successful = True result_count = 0 for title, cur, headers, status in res: @@ -604,7 +600,7 @@ def one_iteration(text=None): break if self.auto_vertical_output: - max_width = self.prompt_app.output.get_size().columns + max_width = self.cli.output.get_size().columns else: max_width = None @@ -642,7 +638,7 @@ def one_iteration(text=None): status_str = str(status).lower() if status_str.find('ok') > -1: logger.debug("cancelled query, connection id: %r, sql: %r", - connection_id_to_kill, text) + connection_id_to_kill, document.text) self.echo("cancelled query", err=True, fg='red') except Exception as e: self.echo('Encountered error while cancelling query: {}'.format(e), @@ -657,7 +653,7 @@ def one_iteration(text=None): try: sqlexecute.connect() logger.debug('Reconnected successfully.') - one_iteration(text) + one_iteration(document) return # OK to just return, cuz the recursion call runs to the end. except OperationalError as e: logger.debug('Reconnect failed. e: %r', e) @@ -665,70 +661,67 @@ def one_iteration(text=None): # If reconnection failed, don't proceed further. return else: - logger.error("sql: %r, error: %r", text, e) + logger.error("sql: %r, error: %r", document.text, e) logger.error("traceback: %r", traceback.format_exc()) self.echo(str(e), err=True, fg='red') except Exception as e: - logger.error("sql: %r, error: %r", text, e) + logger.error("sql: %r, error: %r", document.text, e) logger.error("traceback: %r", traceback.format_exc()) self.echo(str(e), err=True, fg='red') else: - if is_dropping_database(text, self.sqlexecute.dbname): + if is_dropping_database(document.text, self.sqlexecute.dbname): self.sqlexecute.dbname = None self.sqlexecute.connect() # Refresh the table names and column names if necessary. - if need_completion_refresh(text): + if need_completion_refresh(document.text): self.refresh_completions( - reset=need_completion_reset(text)) + reset=need_completion_reset(document.text)) finally: if self.logfile is False: self.echo("Warning: This query was not logged.", err=True, fg='red') - query = Query(text, successful, mutating) + query = Query(document.text, successful, mutating) self.query_history.append(query) get_toolbar_tokens = create_toolbar_tokens_func( - self, show_suggestion_tip) - if self.wider_completion_menu: - complete_style = CompleteStyle.MULTI_COLUMN - else: - complete_style = CompleteStyle.COLUMN - + self.completion_refresher.is_refreshing, + show_suggestion_tip) + + layout = create_prompt_layout( + lexer=MyCliLexer, + multiline=True, + get_prompt_tokens=prompt_tokens, + get_continuation_tokens=get_continuation_tokens, + get_bottom_toolbar_tokens=get_toolbar_tokens, + display_completions_in_columns=self.wider_completion_menu, + extra_input_processors=[ConditionalProcessor( + processor=HighlightMatchingBracketProcessor(chars='[](){}'), + filter=HasFocus(DEFAULT_BUFFER) & ~IsDone() + )], + reserve_space_for_menu=self.get_reserved_space() + ) with self._completer_lock: + buf = CLIBuffer( + always_multiline=self.multi_line, completer=self.completer, + history=history, auto_suggest=AutoSuggestFromHistory(), + complete_while_typing=Always(), + accept_action=AcceptAction.RETURN_DOCUMENT) if self.key_bindings == 'vi': editing_mode = EditingMode.VI else: editing_mode = EditingMode.EMACS - self.prompt_app = PromptSession( - lexer=PygmentsLexer(MyCliLexer), - reserve_space_for_menu=self.get_reserved_space(), - message=get_message, - prompt_continuation=get_continuation, - bottom_toolbar=get_toolbar_tokens, - complete_style=complete_style, - input_processors=[ConditionalProcessor( - processor=HighlightMatchingBracketProcessor( - chars='[](){}'), - filter=HasFocus(DEFAULT_BUFFER) & ~IsDone() - )], - tempfile_suffix='.sql', - completer=DynamicCompleter(lambda: self.completer), - history=history, - auto_suggest=AutoSuggestFromHistory(), - complete_while_typing=True, - multiline=cli_is_multiline(self), - style=style_factory(self.syntax_style, self.cli_style), - include_default_pygments_style=False, - key_bindings=key_bindings, - enable_open_in_editor=True, - enable_system_prompt=True, - enable_suspend=True, - editing_mode=editing_mode, - search_ignore_case=True - ) + application = Application( + style=style_from_pygments(style_cls=self.output_style), + layout=layout, buffer=buf, + key_bindings_registry=key_binding_manager.registry, + on_exit=AbortAction.RAISE_EXCEPTION, + on_abort=AbortAction.RETRY, editing_mode=editing_mode, + ignore_case=True) + self.cli = CommandLineInterface(application=application, + eventloop=create_eventloop()) try: while True: @@ -778,7 +771,7 @@ def output(self, output, status=None): """ if output: - size = self.prompt_app.output.get_size() + size = self.cli.output.get_size() margin = self.get_output_margin(status) @@ -852,11 +845,16 @@ def _on_completions_refreshed(self, new_completer): """ with self._completer_lock: self.completer = new_completer + # When mycli is first launched we call refresh_completions before + # instantiating the cli object. So it is necessary to check if cli + # exists before trying the replace the completer object in cli. + if self.cli: + self.cli.current_buffer.completer = new_completer - if self.prompt_app: + if self.cli: # After refreshing, redraw the CLI to clear the statusbar # "Refreshing completions..." indicator - self.prompt_app.app.invalidate() + self.cli.request_redraw() def get_completions(self, text, cursor_positition): with self._completer_lock: @@ -1167,6 +1165,8 @@ def cli(database, user, host, port, socket, password, dbname, sys.stdin = open('/dev/tty') except (IOError, OSError): mycli.logger.warning('Unable to open TTY as stdin.') + except OSError: + mycli.logger.warning('Unable to open TTY as stdin.') if (mycli.destructive_warning and confirm_destructive_query(stdin_text) is False): diff --git a/mycli/myclirc b/mycli/myclirc index 8ecbddc1..7e564dbe 100644 --- a/mycli/myclirc +++ b/mycli/myclirc @@ -85,31 +85,36 @@ enable_pager = True # Custom colors for the completion menu, toolbar, etc. [colors] -completion-menu.completion.current = 'bg:#ffffff #000000' -completion-menu.completion = 'bg:#008888 #ffffff' -completion-menu.meta.completion.current = 'bg:#44aaaa #000000' -completion-menu.meta.completion = 'bg:#448888 #ffffff' -completion-menu.multi-column-meta = 'bg:#aaffff #000000' -scrollbar.arrow = 'bg:#003333' -scrollbar = 'bg:#00aaaa' -selected = '#ffffff bg:#6666aa' -search = '#ffffff bg:#4444aa' -search.current = '#ffffff bg:#44aa44' -bottom-toolbar = 'bg:#222222 #aaaaaa' -bottom-toolbar.off = 'bg:#222222 #888888' -bottom-toolbar.on = 'bg:#222222 #ffffff' -search-toolbar = 'noinherit bold' -search-toolbar.text = 'nobold' -system-toolbar = 'noinherit bold' -arg-toolbar = 'noinherit bold' -arg-toolbar.text = 'nobold' -bottom-toolbar.transaction.valid = 'bg:#222222 #00ff5f bold' -bottom-toolbar.transaction.failed = 'bg:#222222 #ff005f bold' - -# style classes for colored table output -output.header = "#00ff5f bold" -output.odd-row = "" -output.even-row = "" +# Completion menus. +Token.Menu.Completions.Completion.Current = 'bg:#00aaaa #000000' +Token.Menu.Completions.Completion = 'bg:#008888 #ffffff' +Token.Menu.Completions.MultiColumnMeta = 'bg:#aaffff #000000' +Token.Menu.Completions.ProgressButton = 'bg:#003333' +Token.Menu.Completions.ProgressBar = 'bg:#00aaaa' + +# Query results +Token.Output.Header = 'bold' +Token.Output.OddRow = '' +Token.Output.EvenRow = '' + +# Selected text. +Token.SelectedText = '#ffffff bg:#6666aa' + +# Search matches. (reverse-i-search) +Token.SearchMatch = '#ffffff bg:#4444aa' +Token.SearchMatch.Current = '#ffffff bg:#44aa44' + +# The bottom toolbar. +Token.Toolbar = 'bg:#222222 #aaaaaa' +Token.Toolbar.Off = 'bg:#222222 #888888' +Token.Toolbar.On = 'bg:#222222 #ffffff' + +# Search/arg/system toolbars. +Token.Toolbar.Search = 'noinherit bold' +Token.Toolbar.Search.Text = 'nobold' +Token.Toolbar.System = 'noinherit bold' +Token.Toolbar.Arg = 'noinherit bold' +Token.Toolbar.Arg.Text = 'nobold' # Favorite queries. [favorite_queries] diff --git a/mycli/packages/filepaths.py b/mycli/packages/filepaths.py index 5ebdcd97..87e5e740 100644 --- a/mycli/packages/filepaths.py +++ b/mycli/packages/filepaths.py @@ -1,6 +1,4 @@ # -*- coding: utf-8 -from __future__ import unicode_literals -from mycli.encodingutils import text_type import os @@ -62,10 +60,10 @@ def suggest_path(root_dir): """ if not root_dir: - return [text_type(os.path.abspath(os.sep)), text_type('~'), text_type(os.curdir), text_type(os.pardir)] + return [os.path.abspath(os.sep), '~', os.curdir, os.pardir] if '~' in root_dir: - root_dir = text_type(os.path.expanduser(root_dir)) + root_dir = os.path.expanduser(root_dir) if not os.path.exists(root_dir): root_dir, _ = os.path.split(root_dir) diff --git a/mycli/packages/parseutils.py b/mycli/packages/parseutils.py index 3e0f2e70..672d70c6 100644 --- a/mycli/packages/parseutils.py +++ b/mycli/packages/parseutils.py @@ -209,14 +209,6 @@ def is_destructive(queries): return queries_start_with(queries, keywords) -def is_open_quote(sql): - """Returns true if the query contains an unclosed quote.""" - - # parsed can contain one or more semi-colon separated commands - parsed = sqlparse.parse(sql) - return any(_parsed_is_open_quote(p) for p in parsed) - - if __name__ == '__main__': sql = 'select * from (select t. from tabl t' print (extract_tables(sql)) diff --git a/mycli/packages/prompt_utils.py b/mycli/packages/prompt_utils.py index 138cef38..420ea2a6 100644 --- a/mycli/packages/prompt_utils.py +++ b/mycli/packages/prompt_utils.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals import sys diff --git a/setup.py b/setup.py index 88e930dc..f9b2a074 100755 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ install_requirements = [ 'click >= 4.1', 'Pygments >= 1.6', - 'prompt_toolkit>=2.0.6', + 'prompt_toolkit>=1.0.10,<1.1.0', 'PyMySQL >= 0.9.2', 'sqlparse>=0.2.2,<0.3.0', 'configobj >= 5.0.5', diff --git a/test/features/steps/crud_table.py b/test/features/steps/crud_table.py index dc058c47..8a4cd73a 100644 --- a/test/features/steps/crud_table.py +++ b/test/features/steps/crud_table.py @@ -76,7 +76,7 @@ def step_see_data_selected(context): +-----+\r | yyy |\r +-----+\r - """), timeout=2) + """), timeout=1) wrappers.expect_exact(context, '1 row in set', timeout=2) @@ -108,5 +108,5 @@ def step_see_null_selected(context): +--------+\r | |\r +--------+\r - """), timeout=2) + """), timeout=1) wrappers.expect_exact(context, '1 row in set', timeout=2) diff --git a/test/features/steps/iocommands.py b/test/features/steps/iocommands.py index 1d3ba657..27b55298 100644 --- a/test/features/steps/iocommands.py +++ b/test/features/steps/iocommands.py @@ -38,7 +38,7 @@ def step_edit_quit(context): @then('we see the sql in prompt') def step_edit_done_sql(context): for match in 'select * from abc'.split(' '): - wrappers.expect_exact(context, match, timeout=5) + wrappers.expect_exact(context, match, timeout=1) # Cleanup the command line. context.cli.sendcontrol('c') # Cleanup the edited file. diff --git a/test/features/steps/named_queries.py b/test/features/steps/named_queries.py index b82d5f4c..651af3bd 100644 --- a/test/features/steps/named_queries.py +++ b/test/features/steps/named_queries.py @@ -32,19 +32,19 @@ def step_delete_named_query(context): @then('we see the named query saved') def step_see_named_query_saved(context): """Wait to see query saved.""" - wrappers.expect_exact(context, 'Saved.', timeout=2) + wrappers.expect_exact(context, 'Saved.', timeout=1) @then('we see the named query executed') def step_see_named_query_executed(context): """Wait to see select output.""" - wrappers.expect_exact(context, 'SELECT 12345', timeout=2) + wrappers.expect_exact(context, 'SELECT 12345', timeout=1) @then('we see the named query deleted') def step_see_named_query_deleted(context): """Wait to see query deleted.""" - wrappers.expect_exact(context, 'foo: Deleted', timeout=2) + wrappers.expect_exact(context, 'foo: Deleted', timeout=1) @when('we save a named query with parameters') @@ -63,7 +63,7 @@ def step_use_named_query_with_parameters(context): def step_see_named_query_with_parameters_executed(context): """Wait to see select output.""" wrappers.expect_exact( - context, 'SELECT 101, "second", "third value"', timeout=2) + context, 'SELECT 101, "second", "third value"', timeout=1) @when('we use named query with too few parameters') @@ -76,7 +76,7 @@ def step_use_named_query_with_too_few_parameters(context): def step_see_named_query_with_parameters_fail_with_missing_parameters(context): """Wait to see select output.""" wrappers.expect_exact( - context, 'missing substitution for $2 in query:', timeout=2) + context, 'missing substitution for $2 in query:', timeout=1) @when('we use named query with too many parameters') @@ -89,4 +89,4 @@ def step_use_named_query_with_too_many_parameters(context): def step_see_named_query_with_parameters_fail_with_extra_parameters(context): """Wait to see select output.""" wrappers.expect_exact( - context, 'query does not have substitution parameter $4:', timeout=2) + context, 'query does not have substitution parameter $4:', timeout=1) diff --git a/test/test_clistyle.py b/test/test_clistyle.py index e18a5303..aa1f221d 100644 --- a/test/test_clistyle.py +++ b/test/test_clistyle.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- """Test the mycli.clistyle module.""" -import pytest from pygments.style import Style from pygments.token import Token @@ -8,7 +7,6 @@ from mycli.clistyle import style_factory -@pytest.mark.skip(reason="incompatible with new prompt toolkit") def test_style_factory(): """Test that a Pygments Style class is created.""" header = 'bold underline #ansired' @@ -20,7 +18,6 @@ def test_style_factory(): assert header == style.styles[Token.Output.Header] -@pytest.mark.skip(reason="incompatible with new prompt toolkit") def test_style_factory_unknown_name(): """Test that an unrecognized name will not throw an error.""" style = style_factory('foobar', {}) diff --git a/test/test_main.py b/test/test_main.py index 75f7a24b..95cdf67c 100644 --- a/test/test_main.py +++ b/test/test_main.py @@ -186,10 +186,10 @@ class TestExecute(): def server_type(self): return ['test'] - class PromptBuffer(): + class CommandLineInterface(): output = TestOutput() - m.prompt_app = PromptBuffer() + m.cli = CommandLineInterface() m.sqlexecute = TestExecute() m.explicit_pager = explicit_pager