From 59382a12f8e0ffc20fe21f37bd2e8a958650385d Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 9 Jun 2015 18:40:34 +0200 Subject: [PATCH 001/470] Determine compiler flags dynamically. --- ptpython/_eval.py | 16 ---------------- ptpython/python_input.py | 16 +++++++++++++++- ptpython/repl.py | 14 +++++++++++--- ptpython/validator.py | 16 +++++++++++++++- 4 files changed, 41 insertions(+), 21 deletions(-) delete mode 100644 ptpython/_eval.py diff --git a/ptpython/_eval.py b/ptpython/_eval.py deleted file mode 100644 index ffb62edf..00000000 --- a/ptpython/_eval.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -Eval without `unicode_literals` and `print_function`. -""" - -__all__ = ( - 'eval_', -) - - -def eval_(source, globals=None, locals=None): - """ - A wrapper around eval, executing in a new file - without `unicode_literals`. (For a REPL, we don't want `unicode_literals` - to propagate through eval.) - """ - return eval(source, globals, locals) diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 0603b61a..f2167bec 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -30,6 +30,7 @@ from functools import partial import six +import __future__ __all__ = ( @@ -108,7 +109,7 @@ def __init__(self, self.get_locals = get_locals or self.get_globals self._completer = _completer or PythonCompleter(self.get_globals, self.get_locals) - self._validator = _validator or PythonValidator() + self._validator = _validator or PythonValidator(self.get_compiler_flags) self._history = FileHistory(history_filename) if history_filename else History() self._lexer = _lexer or PythonLexer self._extra_buffers = _extra_buffers @@ -239,6 +240,19 @@ def get_completion_menu_value(): # (Never run more than one at the same time.) self._get_signatures_thread_running = False + def get_compiler_flags(self): + """ + Give the current compiler flags by looking for _Feature instances + in the globals. + """ + flags = 0 + + for value in self.get_globals().values(): + if isinstance(value, __future__._Feature): + flags |= value.compiler_flag + + return flags + @property def key_bindings_registry(self): return self.key_bindings_manager.registry diff --git a/ptpython/repl.py b/ptpython/repl.py index 407635fa..6e71d50a 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -18,7 +18,6 @@ from prompt_toolkit.interface import AcceptAction, CommandLineInterface from .python_input import PythonInput -from ._eval import eval_ # eval() without `unicode_literals` and `print_function`. import os import six @@ -87,13 +86,21 @@ def _execute(self, cli, line): """ output = cli.output + def compile_with_flags(code, mode): + " Compile code with the right compiler flags. " + return compile(code, '', mode, + flags=self.get_compiler_flags(), + dont_inherit=True) + if line[0:1] == '!': # Run as shell command os.system(line[1:]) else: # Try eval first try: - result = eval_(line, self.get_globals(), self.get_locals()) + code = compile_with_flags(line, 'eval') + result = eval(code, self.get_globals(), self.get_locals()) + locals = self.get_locals() locals['_'] = locals['_%i' % self.current_statement_index] = result @@ -116,7 +123,8 @@ def _execute(self, cli, line): output.write(out_string) # If not a valid `eval` expression, run using `exec` instead. except SyntaxError: - six.exec_(line, self.get_globals(), self.get_locals()) + code = compile_with_flags(line, 'exec') + six.exec_(code, self.get_globals(), self.get_locals()) output.write('\n\n') output.flush() diff --git a/ptpython/validator.py b/ptpython/validator.py index 83fb0c76..f3f7ab06 100644 --- a/ptpython/validator.py +++ b/ptpython/validator.py @@ -2,14 +2,28 @@ from prompt_toolkit.validation import Validator, ValidationError +__all__ = ( + 'PythonValidator', +) + class PythonValidator(Validator): + """ + Validation of Python input. + + :param get_compiler_flags: Callable that returns the currently + active compiler flags. + """ + def __init__(self, get_compiler_flags): + self.get_compiler_flags = get_compiler_flags + def validate(self, document): """ Check input for Python syntax errors. """ try: - compile(document.text, '', 'exec') + compile(document.text, '', 'exec', + flags=self.get_compiler_flags(), dont_inherit=True) except SyntaxError as e: # Note, the 'or 1' for offset is required because Python 2.7 # gives `None` as offset in case of '4=4' as input. (Looks like From 1184dd4889f08e7cea3abe78eafc0e0d48be552a Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 9 Jun 2015 18:45:36 +0200 Subject: [PATCH 002/470] Better spacing in output. --- ptpython/repl.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ptpython/repl.py b/ptpython/repl.py index 6e71d50a..d1b3126e 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -121,12 +121,13 @@ def compile_with_flags(code, mode): out_string = out_mark + line_sep.join(result_str.splitlines()) output.write(out_string) + output.write('\n') # If not a valid `eval` expression, run using `exec` instead. except SyntaxError: code = compile_with_flags(line, 'exec') six.exec_(code, self.get_globals(), self.get_locals()) - output.write('\n\n') + output.write('\n') output.flush() @classmethod From f96d2ceef6b15e397e82e310a3f369f61879a6d0 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Wed, 10 Jun 2015 18:08:33 +0200 Subject: [PATCH 003/470] Make get_compiler_flags optional for PythonValidator. (Fixes ptipython.) --- ptpython/validator.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/ptpython/validator.py b/ptpython/validator.py index f3f7ab06..0257a017 100644 --- a/ptpython/validator.py +++ b/ptpython/validator.py @@ -14,7 +14,7 @@ class PythonValidator(Validator): :param get_compiler_flags: Callable that returns the currently active compiler flags. """ - def __init__(self, get_compiler_flags): + def __init__(self, get_compiler_flags=None): self.get_compiler_flags = get_compiler_flags def validate(self, document): @@ -22,8 +22,12 @@ def validate(self, document): Check input for Python syntax errors. """ try: - compile(document.text, '', 'exec', - flags=self.get_compiler_flags(), dont_inherit=True) + if self.get_compiler_flags: + flags = self.get_compiler_flags() + else: + flags = 0 + + compile(document.text, '', 'exec', flags=flags, dont_inherit=True) except SyntaxError as e: # Note, the 'or 1' for offset is required because Python 2.7 # gives `None` as offset in case of '4=4' as input. (Looks like From cc51ff533786e307882501912c78e09e738003e8 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 11 Jun 2015 18:47:57 +0200 Subject: [PATCH 004/470] Made an option of enable_input_validation. --- examples/ptpython_config/config.py | 4 ++++ ptpython/python_input.py | 7 ++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/examples/ptpython_config/config.py b/examples/ptpython_config/config.py index 20b1bdbb..4c18e130 100644 --- a/examples/ptpython_config/config.py +++ b/examples/ptpython_config/config.py @@ -63,6 +63,10 @@ def configure(repl): # Ask for confirmation on exit. repl.confirm_exit = True + # Enable input validation. (Don't try to execute when the input contains + # syntax errors.) + repl.enable_input_validation = True + # Install custom colorscheme named 'my-colorscheme' and use it. repl.install_ui_colorscheme('my-colorscheme', _custom_ui_colorscheme) repl.use_ui_colorscheme('my-colorscheme') diff --git a/ptpython/python_input.py b/ptpython/python_input.py index f2167bec..fbc40954 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -18,6 +18,7 @@ from prompt_toolkit.interface import CommandLineInterface, Application, AcceptAction from prompt_toolkit.key_binding.manager import KeyBindingManager from prompt_toolkit.utils import Callback +from prompt_toolkit.validation import SwitchableValidator from ptpython.completer import PythonCompleter from ptpython.key_bindings import load_python_bindings, load_sidebar_bindings, load_confirm_exit_bindings @@ -135,6 +136,7 @@ def __init__(self, self.confirm_exit = True # Ask for confirmation when Control-D is pressed. self.enable_open_in_editor = True self.enable_system_bindings = True + self.enable_input_validation = True self.enable_history_search = False # When True, like readline, going # back in history will filter the # history on the records starting @@ -213,6 +215,7 @@ def get_completion_menu_value(): name: partial(self.use_ui_colorscheme, name) for name in self.ui_styles }, lambda: self._current_ui_style_name), simple_option('Confirm on exit', 'confirm_exit'), + simple_option('Input validation', 'enable_input_validation'), ] self.selected_option = 0 @@ -367,7 +370,9 @@ def is_buffer_multiline(): tempfile_suffix='.py', history=self._history, completer=self._completer, - validator=self._validator, + validator=SwitchableValidator( + self._validator, + Condition(lambda: self.enable_input_validation)), accept_action=self._accept_action) return python_buffer From c24c0f9484867c9290a853aca6ea39195552bff0 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Mon, 15 Jun 2015 21:07:14 +0200 Subject: [PATCH 005/470] Options sidebar + colorscheme + coloring of the output. --- ptpython/key_bindings.py | 16 ++-- ptpython/layout.py | 56 ++++++++---- ptpython/python_input.py | 187 +++++++++++++++++++++++++-------------- ptpython/repl.py | 35 ++++++-- ptpython/style.py | 16 ++-- 5 files changed, 202 insertions(+), 108 deletions(-) diff --git a/ptpython/key_bindings.py b/ptpython/key_bindings.py index 603cc1d7..35390e91 100644 --- a/ptpython/key_bindings.py +++ b/ptpython/key_bindings.py @@ -86,6 +86,7 @@ def _(event): (When not in Vi navigaton mode, and when multiline is enabled.) """ b = event.current_buffer + empty_lines_required = python_input.accept_input_on_enter or 10000 def at_the_end(b): """ we consider the cursor at the end when there is no text after @@ -97,7 +98,8 @@ def at_the_end(b): # In paste mode, always insert text. b.insert_text('\n') - elif at_the_end(b) and b.document.text.replace(' ', '').endswith('\n'): + elif at_the_end(b) and b.document.text.replace(' ', '').endswith( + '\n' * (empty_lines_required - 1)): if b.validate(): # When the cursor is at the end, and we have an empty line: # drop the empty lines, but return the value. @@ -134,30 +136,30 @@ def load_sidebar_bindings(key_bindings_manager, python_input): @handle('k', filter=sidebar_visible) def _(event): " Go to previous option. " - python_input.selected_option = ( - (python_input.selected_option - 1) % len(python_input.options)) + python_input.selected_option_index = ( + (python_input.selected_option_index - 1) % python_input.option_count) @handle(Keys.Down, filter=sidebar_visible) @handle(Keys.ControlN, filter=sidebar_visible) @handle('j', filter=sidebar_visible) def _(event): " Go to next option. " - python_input.selected_option = ( - (python_input.selected_option + 1) % len(python_input.options)) + python_input.selected_option_index = ( + (python_input.selected_option_index + 1) % python_input.option_count) @handle(Keys.Right, filter=sidebar_visible) @handle('l', filter=sidebar_visible) @handle(' ', filter=sidebar_visible) def _(event): " Select next value for current option. " - option = python_input.options[python_input.selected_option] + option = python_input.selected_option option.activate_next() @handle(Keys.Left, filter=sidebar_visible) @handle('h', filter=sidebar_visible) def _(event): " Select previous value for current option. " - option = python_input.options[python_input.selected_option] + option = python_input.selected_option option.activate_previous() @handle(Keys.ControlC, filter=sidebar_visible) diff --git a/ptpython/layout.py b/ptpython/layout.py index 5e130603..095be115 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -35,32 +35,47 @@ def get_tokens(cli): tokens = [] T = Token.Sidebar - tokens.extend([ - (T, ' '), - (T.Title, 'Options'), - (T, '\n'), - ]) + def append_category(category): + tokens.extend([ + (T, ' '), + (T.Title, ' %-36s' % category.title), + (T, '\n'), + ]) def append(selected, label, status): token = T.Selected if selected else T tokens.append((T, ' >' if selected else ' ')) tokens.append((token.Label, '%-24s' % label)) - tokens.append((token.Status, ' %-14s' % status)) + tokens.append((token.Status, ' ')) + tokens.append((token.Status, '%s' % status)) + + if selected: + tokens.append((Token.SetCursorPosition, '')) + + tokens.append((token.Status, ' ' * (14 - len(status)))) tokens.append((T, '<' if selected else '')) tokens.append((T, '\n')) - for i, option in enumerate(python_input.options): - append(i == python_input.selected_option, - option.description, '%s' % option.get_current_value()) + i = 0 + for category in python_input.options: + append_category(category) + + for option in category.options: + append(i == python_input.selected_option_index, + option.description, '%s' % option.get_current_value()) + i += 1 tokens.pop() # Remove last newline. return tokens super(PythonSidebar, self).__init__( - TokenListControl(get_tokens, Char(token=Token.Sidebar)), + TokenListControl(get_tokens, Char(token=Token.Sidebar), + has_focus=ShowSidebar(python_input) & ~IsDone()), width=LayoutDimension.exact(43), + height=LayoutDimension(min=3), + scroll_offset=1, filter=ShowSidebar(python_input) & ~IsDone()) @@ -149,7 +164,11 @@ class PythonPrompt(TokenListControl): """ def __init__(self, python_input): def get_tokens(cli): - return [(Token.Layout.Prompt, 'In [%s]: ' % python_input.current_statement_index)] + return [ + (Token.In, 'In ['), + (Token.In.Number, '%s' % python_input.current_statement_index), + (Token.In, ']: '), + ] super(PythonPrompt, self).__init__(get_tokens) @@ -267,17 +286,20 @@ class ExitConfirmation(Window): """ Display exit message. """ - def __init__(self, python_input): + def __init__(self, python_input, token=Token.ExitConfirmation): def get_tokens(cli): return [ - (Token.ExitConfirmation, ' \n'), - (Token.ExitConfirmation, ' Do you really want to exit ([y]/n)? \n'), - (Token.ExitConfirmation, ' '), + (token, '\n Do you really want to exit? ([y]/n)'), + (Token.SetCursorPosition, ''), + (token, ' \n'), ] + visible = ~IsDone() & Condition(lambda cli: python_input.show_exit_confirmation) + super(ExitConfirmation, self).__init__( - TokenListControl(get_tokens), - filter=~IsDone() & Condition(lambda cli: python_input.show_exit_confirmation)) + TokenListControl(get_tokens, default_char=Char(token=token), + has_focus=visible), + filter=visible) def create_layout(python_input, key_bindings_manager, diff --git a/ptpython/python_input.py b/ptpython/python_input.py index fbc40954..802f8fa2 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -39,7 +39,16 @@ 'PythonCommandLineInterface', ) -class _Option(object): +class OptionCategory(object): + def __init__(self, title, options): + assert isinstance(title, six.text_type) + assert isinstance(options, list) + + self.title = title + self.options = options + + +class Option(object): """ Ptpython configuration option that can be shown and modified from the sidebar. @@ -69,11 +78,20 @@ def activate_next(self, _previous=False): current = self.get_current_value() options = sorted(self.values.keys()) + # Get current index. + try: + index = options.index(current) + except ValueError: + index = 0 + + # Go to previous/next index. if _previous: - next_option = options[(options.index(current) - 1) % len(options)] + index -= 1 else: - next_option = options[(options.index(current) + 1) % len(options)] + index += 1 + # Call handler for this option. + next_option = options[index % len(options)] self.values[next_option]() def activate_previous(self): @@ -134,6 +152,8 @@ def __init__(self, self.vi_mode = vi_mode self.paste_mode = False # When True, don't insert whitespace after newline. self.confirm_exit = True # Ask for confirmation when Control-D is pressed. + self.accept_input_on_enter = 2 # Accept when pressing Enter 'n' times. + # 'None' means that meta-enter is always required. self.enable_open_in_editor = True self.enable_system_bindings = True self.enable_input_validation = True @@ -153,71 +173,8 @@ def __init__(self, self._current_style = self._generate_style() # Options to be configurable from the sidebar. - def handler(enable=None, disable=None): - " Handler for an '_Option'. " - def handler(): - if enable: - for o in enable: - setattr(self, o, True) - - if disable: - for o in disable: - setattr(self, o, False) - return handler - - def simple_option(description, field_name, values=None): - " Create Simple on/of option. " - values = values or ['off', 'on'] - - def current_value(): - return values[bool(getattr(self, field_name))] - - return _Option(description, lambda: { - values[1]: handler(enable=[field_name]), - values[0]: handler(disable=[field_name]), - }, current_value) - - def get_completion_menu_value(): - " Return active value for the 'completion menu' option. " - if self.show_completions_menu: - return 'pop-up' - elif self.show_completions_toolbar: - return 'toolbar' - else: - return 'off' - - self.options = [ - simple_option('Input mode', 'vi_mode', values=['emacs', 'vi']), - simple_option('Paste mode', 'paste_mode'), - _Option('Completion menu', lambda: { - 'off': handler(disable=['show_completions_menu', 'show_completions_toolbar']), - 'pop-up': handler(enable=['show_completions_menu'], - disable=['show_completions_toolbar']), - 'toolbar': handler(enable=['show_completions_toolbar'], - disable=['show_completions_menu']) - }, get_completion_menu_value), - _Option('Complete while typing', lambda: { - 'on': handler(enable=['complete_while_typing'], disable=['enable_history_search']), - 'off': handler(disable=['complete_while_typing']), - }, lambda: ['off', 'on'][self.complete_while_typing]), - simple_option('Show signature', 'show_signature'), - simple_option('Show docstring', 'show_docstring'), - simple_option('Show line numbers', 'show_line_numbers'), - simple_option('Show status bar', 'show_status_bar'), - _Option('History search', lambda: { - 'on': handler(enable=['enable_history_search'], disable=['complete_while_typing']), - 'off': handler(disable=['enable_history_search']), - }, lambda: ['off', 'on'][self.enable_history_search]), - _Option('Color scheme (code)', lambda: { - name: partial(self.use_code_colorscheme, name) for name in self.code_styles - }, lambda: self._current_code_style_name), - _Option('Color scheme (UI)', lambda: { - name: partial(self.use_ui_colorscheme, name) for name in self.ui_styles - }, lambda: self._current_ui_style_name), - simple_option('Confirm on exit', 'confirm_exit'), - simple_option('Input validation', 'enable_input_validation'), - ] - self.selected_option = 0 + self.options = self._create_options() + self.selected_option_index = 0 #: Incremeting integer counting the current statement. self.current_statement_index = 1 @@ -243,6 +200,22 @@ def get_completion_menu_value(): # (Never run more than one at the same time.) self._get_signatures_thread_running = False + @property + def option_count(self): + " Return the total amount of options. (In all categories together.) " + return sum(len(category.options) for category in self.options) + + @property + def selected_option(self): + " Return the currently selected option. " + i = 0 + for category in self.options: + for o in category.options: + if i == self.selected_option_index: + return o + else: + i += 1 + def get_compiler_flags(self): """ Give the current compiler flags by looking for _Feature instances @@ -329,6 +302,84 @@ def _generate_style(self): return generate_style(self.code_styles[self._current_code_style_name], self.ui_styles[self._current_ui_style_name]) + def _create_options(self): + """ + Create a list of `Option` instances for the options sidebar. + """ + def enable(attribute, value=True): + setattr(self, attribute, value) + + # Return `True`, to be able to chain this in the lambdas below. + return True + + def disable(attribute): + setattr(self, attribute, False) + return True + + def simple_option(description, field_name, values=None): + " Create Simple on/of option. " + values = values or ['off', 'on'] + + def current_value(): + return values[bool(getattr(self, field_name))] + + return Option(description, lambda: { + values[1]: lambda: enable(field_name), + values[0]: lambda: disable(field_name), + }, current_value) + + def get_completion_menu_value(): + " Return active value for the 'completion menu' option. " + if self.show_completions_menu: + return 'pop-up' + elif self.show_completions_toolbar: + return 'toolbar' + else: + return 'off' + + return [ + OptionCategory('Input', [ + simple_option('Input mode', 'vi_mode', values=['emacs', 'vi']), + simple_option('Paste mode', 'paste_mode'), + Option('Completion menu', lambda: { + 'off': lambda: disable('show_completions_menu') and disable('show_completions_toolbar'), + 'pop-up': lambda: enable('show_completions_menu') and disable('show_completions_toolbar'), + 'toolbar': lambda: enable('show_completions_toolbar') and disable('show_completions_menu'), + }, get_completion_menu_value), + Option('Complete while typing', lambda: { + 'on': lambda: enable('complete_while_typing') and disable('enable_history_search'), + 'off': lambda: disable('complete_while_typing'), + }, lambda: ['off', 'on'][self.complete_while_typing]), + Option('History search', lambda: { + 'on': lambda: enable('enable_history_search') and disable('complete_while_typing'), + 'off': lambda: disable('enable_history_search'), + }, lambda: ['off', 'on'][self.enable_history_search]), + simple_option('Confirm on exit', 'confirm_exit'), + simple_option('Input validation', 'enable_input_validation'), + Option('Accept input on enter', lambda: { + '1': lambda: enable('accept_input_on_enter', 1), + '2': lambda: enable('accept_input_on_enter', 2), + '3': lambda: enable('accept_input_on_enter', 3), + '4': lambda: enable('accept_input_on_enter', 4), + 'meta-enter': lambda: enable('accept_input_on_enter', None), + }, lambda: str(self.accept_input_on_enter or 'meta-enter')), + ]), + OptionCategory('Display', [ + simple_option('Show signature', 'show_signature'), + simple_option('Show docstring', 'show_docstring'), + simple_option('Show line numbers', 'show_line_numbers'), + simple_option('Show status bar', 'show_status_bar'), + ]), + OptionCategory('Colors', [ + Option('Code', lambda: { + name: partial(self.use_code_colorscheme, name) for name in self.code_styles + }, lambda: self._current_code_style_name), + Option('User interface', lambda: { + name: partial(self.use_ui_colorscheme, name) for name in self.ui_styles + }, lambda: self._current_ui_style_name), + ]), + ] + def create_application(self): """ Create an `Application` instance for use in a `CommandLineInterface`. diff --git a/ptpython/repl.py b/ptpython/repl.py index d1b3126e..d8282651 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -9,13 +9,15 @@ """ from __future__ import unicode_literals -from pygments.lexers import PythonTracebackLexer +from pygments.lexers import PythonTracebackLexer, PythonLexer from pygments.styles.default import DefaultStyle +from pygments.token import Token from prompt_toolkit.application import AbortAction -from prompt_toolkit.utils import DummyContext, Callback -from prompt_toolkit.shortcuts import create_eventloop, create_asyncio_eventloop from prompt_toolkit.interface import AcceptAction, CommandLineInterface +from prompt_toolkit.layout.utils import token_list_width +from prompt_toolkit.shortcuts import create_eventloop, create_asyncio_eventloop +from prompt_toolkit.utils import DummyContext, Callback from .python_input import PythonInput @@ -105,7 +107,12 @@ def compile_with_flags(code, mode): locals['_'] = locals['_%i' % self.current_statement_index] = result if result is not None: - out_mark = 'Out[%i]: ' % self.current_statement_index + out_tokens = [ + (Token.Out, 'Out['), + (Token.Out.Number, '%s' % self.current_statement_index), + (Token.Out, ']:'), + (Token, ' '), + ] try: result_str = '%r\n' % (result, ) @@ -116,12 +123,13 @@ def compile_with_flags(code, mode): # characters. Decode as utf-8 in that case. result_str = '%s\n' % repr(result).decode('utf-8') - # align every line to the first one - line_sep = '\n' + ' ' * len(out_mark) - out_string = out_mark + line_sep.join(result_str.splitlines()) + # Align every line to the first one. + line_sep = '\n' + ' ' * token_list_width(out_tokens) + result_str = line_sep.join(result_str.splitlines()) + '\n' - output.write(out_string) - output.write('\n') + # Write output tokens. + out_tokens.extend(_lex_python_result(result_str)) + cli.print_tokens(out_tokens) # If not a valid `eval` expression, run using `exec` instead. except SyntaxError: code = compile_with_flags(line, 'exec') @@ -160,10 +168,19 @@ def _handle_keyboard_interrupt(cls, cli, e): output.write('\rKeyboardInterrupt\n\n') output.flush() + def _lex_python_traceback(tb): + " Return token list for traceback string. " lexer = PythonTracebackLexer() return lexer.get_tokens(tb) + +def _lex_python_result(tb): + " Return token list for Python string. " + lexer = PythonLexer() + return lexer.get_tokens(tb) + + def enable_deprecation_warnings(): """ Show deprecation warnings, when they are triggered directly by actions in diff --git a/ptpython/style.py b/ptpython/style.py index 44e8889d..023be993 100644 --- a/ptpython/style.py +++ b/ptpython/style.py @@ -48,8 +48,14 @@ class PythonStyle(Style): default_ui_style = { # (Python) Prompt: "In [1]:" - Token.Layout.Prompt: 'bold #008800', + Token.In: 'bold #008800', + Token.In.Number: '', + # Return value. + Token.Out: '#ff0000', + Token.Out.Number: '#ff0000', + + # Separator between windows. (Used above docstring.) Token.Separator: '#bbbbbb', # Search toolbar. @@ -84,18 +90,14 @@ class PythonStyle(Style): # When Control-C has been pressed. Grayed. Token.Aborted: '#888888', + # The options sidebar. Token.Sidebar: 'bg:#bbbbbb #000000', - Token.Sidebar.Title: 'bg:#bbbbbb #000000 underline', + Token.Sidebar.Title: 'bg:#888888 #ffffff underline', Token.Sidebar.Label: 'bg:#bbbbbb #222222', Token.Sidebar.Status: 'bg:#dddddd #000011', - - Token.Sidebar.Selected: 'bg:#222222 #000000', Token.Sidebar.Selected.Label: 'bg:#222222 #eeeeee', Token.Sidebar.Selected.Status: 'bg:#444444 #ffffff bold', - Token.Sidebar.Keys: 'bg:#ffffff #222222', - Token.Sidebar.Keys.Description: 'bg:#cccccc #222222', - # Exit confirmation. Token.ExitConfirmation: 'bg:#aa6666 #ffffff', } From f349358b788fd16f173db75e7f917d02cd1b167a Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Mon, 15 Jun 2015 21:23:30 +0200 Subject: [PATCH 006/470] __main__.py added. --- ptpython/__main__.py | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 ptpython/__main__.py diff --git a/ptpython/__main__.py b/ptpython/__main__.py new file mode 100644 index 00000000..7e4cbabe --- /dev/null +++ b/ptpython/__main__.py @@ -0,0 +1,7 @@ +""" +Make `python -m ptpython` an alias for running `./ptpython`. +""" +from __future__ import unicode_literals +from .entry_points.run_ptpython import run + +run() From 14c0113ea6c5649be473b4d28817d600df542238 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Mon, 15 Jun 2015 21:57:12 +0200 Subject: [PATCH 007/470] Added contrib.asyncio_ssh --- examples/asyncio-ssh-python-embed.py | 115 +++---------------------- ptpython/contrib/__init__.py | 0 ptpython/contrib/asyncssh_repl.py | 123 +++++++++++++++++++++++++++ 3 files changed, 134 insertions(+), 104 deletions(-) create mode 100644 ptpython/contrib/__init__.py create mode 100644 ptpython/contrib/asyncssh_repl.py diff --git a/examples/asyncio-ssh-python-embed.py b/examples/asyncio-ssh-python-embed.py index 8a30df01..bbb73e0a 100755 --- a/examples/asyncio-ssh-python-embed.py +++ b/examples/asyncio-ssh-python-embed.py @@ -8,131 +8,38 @@ import asyncssh import logging -from prompt_toolkit.interface import CommandLineInterface -from prompt_toolkit.layout.screen import Size -from prompt_toolkit.shortcuts import create_asyncio_eventloop -from prompt_toolkit.terminal.vt100_output import Vt100_Output -from prompt_toolkit.input import PipeInput - -from ptpython.repl import PythonRepl +from ptpython.contrib.asyncssh_repl import ReplSSHServerSession logging.basicConfig() logging.getLogger().setLevel(logging.INFO) -class MySSHServerSession(asyncssh.SSHServerSession): - def __init__(self, get_globals, get_locals): - self._chan = None - - def _globals(): - data = {'print': self._print} - data.update(get_globals()) - return data - - repl = PythonRepl(get_globals=_globals, get_locals=get_locals) - - # Disable open-in-editor and system prompt. Because it would run and - # display these commands on the server side, rather than in the SSH - # client. - repl.enable_open_in_editor = False - repl.enable_system_bindings = False - - # PipInput object, for sending input in the CLI. - # (This is something that we can use in the prompt_toolkit event loop, - # but still write date in manually.) - self._input_pipe = PipeInput() - - # Output object. Don't render to the real stdout, but write everything - # in the SSH channel. - class Stdout(object): - def write(s, data): - if self._chan is not None: - self._chan.write(data.replace('\n', '\r\n')) - - def flush(s): - pass - - # Create command line interface. - self.cli = CommandLineInterface( - application=repl.create_application(), - eventloop=create_asyncio_eventloop(), - input=self._input_pipe, - output=Vt100_Output(Stdout(), self._get_size)) - - self._callbacks = self.cli.create_eventloop_callbacks() - - def _get_size(self): - """ - Callable that returns the current `Size`, required by Vt100_Output. - """ - if self._chan is None: - return Size(rows=20, columns=79) - else: - width, height, pixwidth, pixheight = self._chan.get_terminal_size() - return Size(rows=height, columns=width) - - def connection_made(self, chan): - """ - Client connected, run repl in coroutine. - """ - self._chan = chan - - def corot(): - # Run REPL interface. - yield from self.cli.run_async() - - # Close channel when done. - chan.close() - self._chan = None - asyncio.async(corot()) - - def shell_requested(self): - return True - - def terminal_size_changed(self, width, height, pixwidth, pixheight): - """ - When the terminal size changes, report back to CLI. - """ - self._callbacks.terminal_size_changed() - - def data_received(self, data, datatype): - """ - When data is received, send to inputstream of the CLI and repaint. - """ - self._input_pipe.send(data) - - def _print(self, *data, sep=' ', end='\n', file=None): - """ - Alternative 'print' function that prints back into the SSH channel. - """ - data = sep.join(map(str, data)) - self._chan.write(data + end) - - class MySSHServer(asyncssh.SSHServer): - def __init__(self, get_globals=None, get_locals=None): - self.get_globals = get_globals - self.get_locals = get_locals + """ + Server without authentication, running `ReplSSHServerSession`. + """ + def __init__(self, get_namespace): + self.get_namespace = get_namespace def begin_auth(self, username): return False def session_requested(self): - return MySSHServerSession(self.get_globals, self.get_locals) + return ReplSSHServerSession(self.get_namespace) def main(port=8222): """ - Main, example that starts an SSH server. + Example that starts the REPL through an SSH server. """ loop = asyncio.get_event_loop() - # Namespace that expose in the REPL. - environ = {} + # Namespace exposed in the REPL. + environ = {'hello': 'world'} # Start SSH server. def create_server(): - return MySSHServer(lambda: environ, lambda: environ) + return MySSHServer(lambda: environ) print('Listening on :%i' % port) print('To connect, do "ssh localhost -p %i"' % port) diff --git a/ptpython/contrib/__init__.py b/ptpython/contrib/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ptpython/contrib/asyncssh_repl.py b/ptpython/contrib/asyncssh_repl.py new file mode 100644 index 00000000..cddd5d3a --- /dev/null +++ b/ptpython/contrib/asyncssh_repl.py @@ -0,0 +1,123 @@ +""" +Tool for embedding a REPL inside a Python 3 asyncio process. +See ./examples/asyncio-ssh-python-embed.py for a demo. + +Note that the code in this file is Python 3 only. However, we +should make sure not to use Python 3-only syntax, because this +package should be installable in Python 2 as well! +""" +from __future__ import unicode_literals + +import asyncio +import asyncssh + +from prompt_toolkit.input import PipeInput +from prompt_toolkit.interface import CommandLineInterface +from prompt_toolkit.layout.screen import Size +from prompt_toolkit.shortcuts import create_asyncio_eventloop +from prompt_toolkit.terminal.vt100_output import Vt100_Output + +from ptpython.repl import PythonRepl + +__all__ = ( + 'ReplSSHServerSession', +) + + +class ReplSSHServerSession(asyncssh.SSHServerSession): + """ + SSH server session that runs a Python REPL. + + :param get_globals: callable that returns the current globals. + :param get_locals: (optional) callable that returns the current locals. + """ + def __init__(self, get_globals, get_locals=None): + assert callable(get_globals) + assert get_locals is None or callable(get_locals) + + self._chan = None + + def _globals(): + data = get_globals() + data.setdefault('print', self._print) + return data + + repl = PythonRepl(get_globals=_globals, + get_locals=get_locals or _globals) + + # Disable open-in-editor and system prompt. Because it would run and + # display these commands on the server side, rather than in the SSH + # client. + repl.enable_open_in_editor = False + repl.enable_system_bindings = False + + # PipInput object, for sending input in the CLI. + # (This is something that we can use in the prompt_toolkit event loop, + # but still write date in manually.) + self._input_pipe = PipeInput() + + # Output object. Don't render to the real stdout, but write everything + # in the SSH channel. + class Stdout(object): + def write(s, data): + if self._chan is not None: + self._chan.write(data.replace('\n', '\r\n')) + + def flush(s): + pass + + # Create command line interface. + self.cli = CommandLineInterface( + application=repl.create_application(), + eventloop=create_asyncio_eventloop(), + input=self._input_pipe, + output=Vt100_Output(Stdout(), self._get_size)) + + self._callbacks = self.cli.create_eventloop_callbacks() + + def _get_size(self): + """ + Callable that returns the current `Size`, required by Vt100_Output. + """ + if self._chan is None: + return Size(rows=20, columns=79) + else: + width, height, pixwidth, pixheight = self._chan.get_terminal_size() + return Size(rows=height, columns=width) + + def connection_made(self, chan): + """ + Client connected, run repl in coroutine. + """ + self._chan = chan + + # Run REPL interface. + f = asyncio.async(self.cli.run_async()) + + # Close channel when done. + def done(_): + chan.close() + self._chan = None + f.add_done_callback(done) + + def shell_requested(self): + return True + + def terminal_size_changed(self, width, height, pixwidth, pixheight): + """ + When the terminal size changes, report back to CLI. + """ + self._callbacks.terminal_size_changed() + + def data_received(self, data, datatype): + """ + When data is received, send to inputstream of the CLI and repaint. + """ + self._input_pipe.send(data) + + def _print(self, *data, sep=' ', end='\n', file=None): + """ + Alternative 'print' function that prints back into the SSH channel. + """ + data = sep.join(map(str, data)) + self._chan.write(data + end) From c948fd201d2fd631bac3ccf700b370f52a26014c Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Mon, 15 Jun 2015 22:12:56 +0200 Subject: [PATCH 008/470] Pypi release 0.13 --- CHANGELOG | 14 ++++++++++++++ setup.py | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 26170442..9325f5c5 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,20 @@ CHANGELOG ========= +0.13: 2015-06-15 +--------------- + +New features: +- Upgrade to prompt_toolkit 0.40 +- Options sidebar. +- Custom color schemes. +- Syntax highlighting of the output. +- Input validation can now be turned off. +- Determine compiler flags dynamically. (Fixes importing unicode_literals). +- Exit confirmation and dialog. +- Autocompletion of IPython %cat command. +- Correctly render tracebacks on Windows. + 0.12: 2015-06-04 --------------- diff --git a/setup.py b/setup.py index ef6cacb5..68efe46d 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ setup( name='ptpython', author='Jonathan Slenders', - version='0.12', + version='0.13', url='https://github.com/jonathanslenders/ptpython', description='Python REPL build on top of prompt_toolkit', long_description=long_description, From 7aaa889be95ce84336b9e6a05f6154eb98b1f862 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Mon, 15 Jun 2015 22:43:43 +0200 Subject: [PATCH 009/470] More documentation for asyncio-ssh-python-embed.py. --- examples/asyncio-ssh-python-embed.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/asyncio-ssh-python-embed.py b/examples/asyncio-ssh-python-embed.py index bbb73e0a..cbd07003 100755 --- a/examples/asyncio-ssh-python-embed.py +++ b/examples/asyncio-ssh-python-embed.py @@ -1,8 +1,9 @@ #!/usr/bin/env python """ Example of running the Python REPL through an SSH connection in an asyncio process. +This requires Python 3, asyncio and asyncssh. -This requires Python 3. +Run this example and then SSH to localhost, port 8222. """ import asyncio import asyncssh @@ -22,6 +23,7 @@ def __init__(self, get_namespace): self.get_namespace = get_namespace def begin_auth(self, username): + # No authentication. return False def session_requested(self): From 71cb33319f899d36cfa61fbb97b1c380fed837ba Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 18 Jun 2015 20:12:16 +0200 Subject: [PATCH 010/470] Pypi release 0.14 --- CHANGELOG | 6 ++++++ setup.py | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 9325f5c5..ab789bf2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,12 @@ CHANGELOG ========= +0.14: 2015-06-16 +--------------- + +Fix: +- Correct dependency for prompt_toolkit. + 0.13: 2015-06-15 --------------- diff --git a/setup.py b/setup.py index 68efe46d..7523b0c7 100644 --- a/setup.py +++ b/setup.py @@ -13,13 +13,13 @@ setup( name='ptpython', author='Jonathan Slenders', - version='0.13', + version='0.14', url='https://github.com/jonathanslenders/ptpython', description='Python REPL build on top of prompt_toolkit', long_description=long_description, packages=find_packages('.'), install_requires = [ - 'prompt_toolkit==0.39', + 'prompt_toolkit==0.40', 'jedi>=0.9.0', 'docopt', ], From 6fe4655832ee89ba91534e3958f79c365bb1cdd2 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 18 Jun 2015 20:13:00 +0200 Subject: [PATCH 011/470] Use correct tokens for IPython prompt. --- ptpython/ipython.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ptpython/ipython.py b/ptpython/ipython.py index 2a0fa91b..8a9c9c0f 100644 --- a/ptpython/ipython.py +++ b/ptpython/ipython.py @@ -42,7 +42,7 @@ class IPythonPrompt(TokenListControl): def __init__(self, prompt_manager): def get_tokens(cli): text = prompt_manager.render('in', color=False, just=False) - return [(Token.Layout.Prompt, text)] + return [(Token.In, text)] super(IPythonPrompt, self).__init__(get_tokens) From 4812ded2d6df3c8ed84f461c3e3b3c2d17ba5bec Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 18 Jun 2015 20:13:31 +0200 Subject: [PATCH 012/470] Make asyncssh_repl syntax Python 2 compatible. --- ptpython/contrib/asyncssh_repl.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/ptpython/contrib/asyncssh_repl.py b/ptpython/contrib/asyncssh_repl.py index cddd5d3a..d2d903e3 100644 --- a/ptpython/contrib/asyncssh_repl.py +++ b/ptpython/contrib/asyncssh_repl.py @@ -115,9 +115,19 @@ def data_received(self, data, datatype): """ self._input_pipe.send(data) - def _print(self, *data, sep=' ', end='\n', file=None): + def _print(self, *data, **kw): """ + _print(self, *data, sep=' ', end='\n', file=None) + Alternative 'print' function that prints back into the SSH channel. """ + # Pop keyword-only arguments. (We cannot use the syntax from the + # signature. Otherwise, Python2 will give a syntax error message when + # installing.) + sep = kw.pop('sep', ' ') + end = kw.pop('end', '\n') + file = kw.pop('end', None) + assert not kw, 'Too many keyword-only arguments' + data = sep.join(map(str, data)) self._chan.write(data + end) From 946c4956ff784127f141add140cd83616c50b576 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 18 Jun 2015 20:13:59 +0200 Subject: [PATCH 013/470] Added example for using color scheme to config template. --- examples/ptpython_config/config.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/examples/ptpython_config/config.py b/examples/ptpython_config/config.py index 4c18e130..827c4fa1 100644 --- a/examples/ptpython_config/config.py +++ b/examples/ptpython_config/config.py @@ -67,6 +67,9 @@ def configure(repl): # syntax errors.) repl.enable_input_validation = True + # Use this colorscheme for the code. + repl.use_code_colorscheme('pastie') + # Install custom colorscheme named 'my-colorscheme' and use it. repl.install_ui_colorscheme('my-colorscheme', _custom_ui_colorscheme) repl.use_ui_colorscheme('my-colorscheme') From 3f9874f08f41d07a272edcce6f1a355a028412bd Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sat, 20 Jun 2015 18:30:09 +0200 Subject: [PATCH 014/470] Removed '1' as an option for accept_input_on_enter, because it doesn't make any sense. --- ptpython/python_input.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 802f8fa2..2365b551 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -357,7 +357,6 @@ def get_completion_menu_value(): simple_option('Confirm on exit', 'confirm_exit'), simple_option('Input validation', 'enable_input_validation'), Option('Accept input on enter', lambda: { - '1': lambda: enable('accept_input_on_enter', 1), '2': lambda: enable('accept_input_on_enter', 2), '3': lambda: enable('accept_input_on_enter', 3), '4': lambda: enable('accept_input_on_enter', 4), From 68ad1120068560fc5e0d0508f272e0de2285e328 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sat, 20 Jun 2015 18:35:31 +0200 Subject: [PATCH 015/470] Pypi release 0.15 --- CHANGELOG | 10 ++++++++++ setup.py | 4 ++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index ab789bf2..16d47ef2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,16 @@ CHANGELOG ========= +0.15: 2015-06-20 +--------------- + +Upgrade to prompt_toolkit 0.41 + +Fixes: +- Correct tokens for IPython prompt. +- Syntax fix in asyncssh_repl. + + 0.14: 2015-06-16 --------------- diff --git a/setup.py b/setup.py index 7523b0c7..8d3e4d75 100644 --- a/setup.py +++ b/setup.py @@ -13,13 +13,13 @@ setup( name='ptpython', author='Jonathan Slenders', - version='0.14', + version='0.15', url='https://github.com/jonathanslenders/ptpython', description='Python REPL build on top of prompt_toolkit', long_description=long_description, packages=find_packages('.'), install_requires = [ - 'prompt_toolkit==0.40', + 'prompt_toolkit==0.41', 'jedi>=0.9.0', 'docopt', ], From eb72407946f2facafb3c57bed6e82fbe113a061b Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Mon, 22 Jun 2015 19:33:45 +0200 Subject: [PATCH 016/470] Correctly show traceback on Python 3 --- ptpython/repl.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/ptpython/repl.py b/ptpython/repl.py index d8282651..ca1ae3ea 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -145,7 +145,13 @@ def _handle_exception(cls, cli, e): # Instead of just calling ``traceback.format_exc``, we take the # traceback and skip the bottom calls of this framework. t, v, tb = sys.exc_info() - tblist = traceback.extract_tb(tb)[3:] + tblist = traceback.extract_tb(tb) + + for line_nr, tb_tuple in enumerate(tblist): + if tb_tuple[0] == '': + tblist = tblist[line_nr:] + break + l = traceback.format_list(tblist) if l: l.insert(0, "Traceback (most recent call last):\n") From 0705859421c8885cac25bfbb2f0ad6446adcc879 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Wed, 24 Jun 2015 19:46:18 +0200 Subject: [PATCH 017/470] Better styling of sidebar. --- ptpython/layout.py | 15 +++++++++------ ptpython/style.py | 8 ++++++-- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/ptpython/layout.py b/ptpython/layout.py index 095be115..d682375d 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -90,12 +90,15 @@ def get_tokens(cli): # Show navigation info. tokens.extend([ - (T, ' '), - (T.Keys, '[Arrows]'), - (T.Keys.Description, 'Navigate'), + (T.Separator , ' ' * 43 + '\n'), + (T, ' '), + (T.Key, '[Arrows]'), (T, ' '), - (T.Keys, '[Enter]'), - (T.Keys.Description, 'Hide menu'), + (T.Key.Description, 'Navigate'), + (T, ' '), + (T.Key, '[Enter]'), + (T, ' '), + (T.Key.Description, 'Hide menu'), ]) return tokens @@ -103,7 +106,7 @@ def get_tokens(cli): super(PythonSidebarNavigation, self).__init__( TokenListControl(get_tokens, Char(token=Token.Sidebar)), width=LayoutDimension.exact(43), - height=LayoutDimension.exact(1), + height=LayoutDimension.exact(2), filter=ShowSidebar(python_input) & ~IsDone()) diff --git a/ptpython/style.py b/ptpython/style.py index 023be993..4f77f295 100644 --- a/ptpython/style.py +++ b/ptpython/style.py @@ -92,14 +92,18 @@ class PythonStyle(Style): # The options sidebar. Token.Sidebar: 'bg:#bbbbbb #000000', - Token.Sidebar.Title: 'bg:#888888 #ffffff underline', + Token.Sidebar.Title: 'bg:#668866 #ffffff underline', Token.Sidebar.Label: 'bg:#bbbbbb #222222', Token.Sidebar.Status: 'bg:#dddddd #000011', Token.Sidebar.Selected.Label: 'bg:#222222 #eeeeee', Token.Sidebar.Selected.Status: 'bg:#444444 #ffffff bold', + Token.Sidebar.Separator: 'bg:#bbbbbb #ffffff underline', + Token.Sidebar.Key: 'bg:#bbddbb #000000 bold', + Token.Sidebar.Key.Description: 'bg:#bbbbbb #000000', + # Exit confirmation. - Token.ExitConfirmation: 'bg:#aa6666 #ffffff', + Token.ExitConfirmation: 'bg:#884444 #ffffff', } From 62e08d3d190ff10e952a459ae12d102223d0481c Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Wed, 24 Jun 2015 20:07:17 +0200 Subject: [PATCH 018/470] Exit REPL when the input starts with Ctrl-Z. --- ptpython/repl.py | 6 +++++- ptpython/validator.py | 5 +++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/ptpython/repl.py b/ptpython/repl.py index ca1ae3ea..10ca231c 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -94,7 +94,11 @@ def compile_with_flags(code, mode): flags=self.get_compiler_flags(), dont_inherit=True) - if line[0:1] == '!': + if line.lstrip().startswith('\x1a'): + # When the input starts with Ctrl-Z, quit the REPL. + cli.set_return_value(None) + + elif line.lstrip().startswith('!'): # Run as shell command os.system(line[1:]) else: diff --git a/ptpython/validator.py b/ptpython/validator.py index 0257a017..dd0ae73d 100644 --- a/ptpython/validator.py +++ b/ptpython/validator.py @@ -21,6 +21,11 @@ def validate(self, document): """ Check input for Python syntax errors. """ + # When the input starts with Ctrl-Z, always accept. This means EOF in a + # Python REPL. + if document.text.startswith('\x1a'): + return + try: if self.get_compiler_flags: flags = self.get_compiler_flags() From dff60bd0953b4a9e4024c3d06f1f6ac254e2c364 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Wed, 24 Jun 2015 21:05:47 +0200 Subject: [PATCH 019/470] Set terminal title. --- ptpython/entry_points/run_ptipython.py | 5 +++-- ptpython/entry_points/run_ptpython.py | 3 ++- ptpython/ipython.py | 4 ++++ ptpython/python_input.py | 2 ++ ptpython/repl.py | 6 +++++- 5 files changed, 16 insertions(+), 4 deletions(-) diff --git a/ptpython/entry_points/run_ptipython.py b/ptpython/entry_points/run_ptipython.py index 86d2eb73..c56344e7 100644 --- a/ptpython/entry_points/run_ptipython.py +++ b/ptpython/entry_points/run_ptipython.py @@ -58,10 +58,11 @@ def configure(repl): run_config(repl, path) # Run interactive shell. - embed(vi_mode=vi_mode, + embed(vi_mode=vi_mode, history_filename=os.path.join(config_dir, 'history'), configure=configure, - user_ns=user_ns) + user_ns=user_ns, + title='IPython REPL (ptipython)') if __name__ == '__main__': diff --git a/ptpython/entry_points/run_ptpython.py b/ptpython/entry_points/run_ptpython.py index 730afb32..0ec1d2de 100644 --- a/ptpython/entry_points/run_ptpython.py +++ b/ptpython/entry_points/run_ptpython.py @@ -65,7 +65,8 @@ def configure(repl): embed(vi_mode=vi_mode, history_filename=os.path.join(config_dir, 'history'), configure=configure, - startup_paths=startup_paths) + startup_paths=startup_paths, + title='Python REPL (ptpython)') if __name__ == '__main__': run() diff --git a/ptpython/ipython.py b/ptpython/ipython.py index 8a9c9c0f..5fd73881 100644 --- a/ptpython/ipython.py +++ b/ptpython/ipython.py @@ -169,6 +169,7 @@ def __init__(self, *a, **kw): vi_mode = kw.pop('vi_mode', False) history_filename = kw.pop('history_filename', None) configure = kw.pop('configure', None) + title = kw.pop('title', None) super(InteractiveShellEmbed, self).__init__(*a, **kw) @@ -181,6 +182,9 @@ def get_globals(): get_globals=get_globals, vi_mode=vi_mode, history_filename=history_filename) + if title: + ipython_input.terminal_title = title + if configure: configure(ipython_input) diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 2365b551..b545dd5f 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -164,6 +164,7 @@ def __init__(self, self.show_sidebar = False # Currently show the sidebar. self.show_exit_confirmation = False # Currently show 'Do you really want to exit?' + self.terminal_title = None # The title to be displayed in the terminal. (None or string.) #: Load styles. self.code_styles = get_all_code_styles() @@ -402,6 +403,7 @@ def create_application(self): on_abort=AbortAction.RETRY, on_exit=self._on_exit, get_style=lambda: self._current_style, + get_title=lambda: self.terminal_title, on_start=self._on_start, on_input_timeout=Callback(self._on_input_timeout)) diff --git a/ptpython/repl.py b/ptpython/repl.py index 10ca231c..46ad5c30 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -244,7 +244,7 @@ def enter_to_continue(): def embed(globals=None, locals=None, configure=None, - vi_mode=False, history_filename=None, + vi_mode=False, history_filename=None, title=None, startup_paths=None, patch_stdout=False, return_asyncio_coroutine=False): """ Call this to embed Python shell at the current point in your program. @@ -256,6 +256,7 @@ def embed(globals=None, locals=None, configure=None, :param vi_mode: Boolean. Use Vi instead of Emacs key bindings. :param configure: Callable that will be called with the `PythonRepl` as a first argument, to trigger configuration. + :param title: Title to be displayed in the terminal titlebar. (None or string.) """ assert configure is None or callable(configure) @@ -287,6 +288,9 @@ def get_locals(): history_filename=history_filename, startup_paths=startup_paths) + if title: + repl.terminal_title = title + if configure: configure(repl) From 726dd70d162c7cc7d9e7da0692dd932269d830ca Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Wed, 24 Jun 2015 22:08:23 +0200 Subject: [PATCH 020/470] Display helptext for the sidebar options. --- examples/ptpython_config/config.py | 3 + ptpython/layout.py | 32 ++++++- ptpython/python_input.py | 135 ++++++++++++++++++++--------- ptpython/style.py | 1 + 4 files changed, 128 insertions(+), 43 deletions(-) diff --git a/examples/ptpython_config/config.py b/examples/ptpython_config/config.py index 827c4fa1..8c97ff59 100644 --- a/examples/ptpython_config/config.py +++ b/examples/ptpython_config/config.py @@ -34,6 +34,9 @@ def configure(repl): # Show status bar. repl.show_status_bar = True + # When the sidebar is visible, also show the help text. + repl.show_sidebar_help = True + # Complete while typing. (Don't require tab before the # completion menu is shown.) repl.complete_while_typing = True diff --git a/ptpython/layout.py b/ptpython/layout.py index d682375d..f6927c7c 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -63,7 +63,7 @@ def append(selected, label, status): for option in category.options: append(i == python_input.selected_option_index, - option.description, '%s' % option.get_current_value()) + option.title, '%s' % option.get_current_value()) i += 1 tokens.pop() # Remove last newline. @@ -110,6 +110,35 @@ def get_tokens(cli): filter=ShowSidebar(python_input) & ~IsDone()) +class PythonSidebarHelp(Window): + """ + Help text for the current item in the sidebar. + """ + def __init__(self, python_input): + token = Token.Sidebar.HelpText + + def get_current_description(): + """ + Return the description of the selected option. + """ + i = 0 + for category in python_input.options: + for option in category.options: + if i == python_input.selected_option_index: + return option.description + i += 1 + return '' + + def get_tokens(cli): + return [(token, get_current_description())] + + super(PythonSidebarHelp, self).__init__( + TokenListControl(get_tokens, Char(token=token)), + height=LayoutDimension(min=3), + filter=ShowSidebar(python_input) & + Condition(lambda cli: python_input.show_sidebar_help) & ~IsDone()) + + class SignatureToolbar(Window): def __init__(self, python_input): def get_tokens(cli): @@ -371,6 +400,7 @@ def menu_position(cli): Float(left=2, bottom=1, content=ExitConfirmation(python_input)), + Float(bottom=1, left=1, right=0, content=PythonSidebarHelp(python_input)), ]), ArgToolbar(), SearchToolbar(), diff --git a/ptpython/python_input.py b/ptpython/python_input.py index b545dd5f..3b4a5f80 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -53,19 +53,22 @@ class Option(object): Ptpython configuration option that can be shown and modified from the sidebar. + :param title: Text. :param description: Text. :param get_values: Callable that returns a dictionary mapping the possible values to callbacks that activate these value. :param get_current_value: Callable that returns the current, active value. """ - def __init__(self, description, get_values, get_current_value): + def __init__(self, title, description, get_current_value, get_values): + assert isinstance(title, six.text_type) assert isinstance(description, six.text_type) - assert callable(get_values) assert callable(get_current_value) + assert callable(get_values) + self.title = title self.description = description - self.get_values = get_values self.get_current_value = get_current_value + self.get_values = get_values @property def values(self): @@ -163,6 +166,7 @@ def __init__(self, # with the current input. self.show_sidebar = False # Currently show the sidebar. + self.show_sidebar_help = True # When the sidebar is visible, also show the help text. self.show_exit_confirmation = False # Currently show 'Do you really want to exit?' self.terminal_title = None # The title to be displayed in the terminal. (None or string.) @@ -317,17 +321,22 @@ def disable(attribute): setattr(self, attribute, False) return True - def simple_option(description, field_name, values=None): + def simple_option(title, description, field_name, values=None): " Create Simple on/of option. " values = values or ['off', 'on'] - def current_value(): + def get_current_value(): return values[bool(getattr(self, field_name))] - return Option(description, lambda: { - values[1]: lambda: enable(field_name), - values[0]: lambda: disable(field_name), - }, current_value) + def get_values(): + return { + values[1]: lambda: enable(field_name), + values[0]: lambda: disable(field_name), + } + + return Option(title=title, description=description, + get_values=get_values, + get_current_value=get_current_value) def get_completion_menu_value(): " Return active value for the 'completion menu' option. " @@ -340,43 +349,85 @@ def get_completion_menu_value(): return [ OptionCategory('Input', [ - simple_option('Input mode', 'vi_mode', values=['emacs', 'vi']), - simple_option('Paste mode', 'paste_mode'), - Option('Completion menu', lambda: { - 'off': lambda: disable('show_completions_menu') and disable('show_completions_toolbar'), - 'pop-up': lambda: enable('show_completions_menu') and disable('show_completions_toolbar'), - 'toolbar': lambda: enable('show_completions_toolbar') and disable('show_completions_menu'), - }, get_completion_menu_value), - Option('Complete while typing', lambda: { - 'on': lambda: enable('complete_while_typing') and disable('enable_history_search'), - 'off': lambda: disable('complete_while_typing'), - }, lambda: ['off', 'on'][self.complete_while_typing]), - Option('History search', lambda: { - 'on': lambda: enable('enable_history_search') and disable('complete_while_typing'), - 'off': lambda: disable('enable_history_search'), - }, lambda: ['off', 'on'][self.enable_history_search]), - simple_option('Confirm on exit', 'confirm_exit'), - simple_option('Input validation', 'enable_input_validation'), - Option('Accept input on enter', lambda: { - '2': lambda: enable('accept_input_on_enter', 2), - '3': lambda: enable('accept_input_on_enter', 3), - '4': lambda: enable('accept_input_on_enter', 4), - 'meta-enter': lambda: enable('accept_input_on_enter', None), - }, lambda: str(self.accept_input_on_enter or 'meta-enter')), + simple_option(title='Input mode', + description='Vi or emacs key bindings.', + field_name='vi_mode', + values=['emacs', 'vi']), + simple_option(title='Paste mode', + description="When enabled, don't indent automatically.", + field_name='paste_mode'), + Option(title='Complete while typing', + description="Generate autocompletions automatically while typing. " + 'Don\'t require pressing TAB. (Not compatible with "History search".)', + get_current_value=lambda: ['off', 'on'][self.complete_while_typing], + get_values=lambda: { + 'on': lambda: enable('complete_while_typing') and disable('enable_history_search'), + 'off': lambda: disable('complete_while_typing'), + }), + Option(title='History search', + description='When pressing the up-arrow, filter the history on input starting ' + 'with the current text. (Not compatible with "Complete while typing".)', + get_current_value=lambda: ['off', 'on'][self.enable_history_search], + get_values=lambda: { + 'on': lambda: enable('enable_history_search') and disable('complete_while_typing'), + 'off': lambda: disable('enable_history_search'), + }), + simple_option(title='Confirm on exit', + description='Require confirmation when exiting.', + field_name='confirm_exit'), + simple_option(title='Input validation', + description='In case of syntax errors, move the cursor to the error ' + 'instead of showing a traceback of a SyntaxError.', + field_name='enable_input_validation'), + Option(title='Accept input on enter', + description='Amount of ENTER presses required to execute input when the cursor ' + 'is at the end of the input. (Note that META+ENTER will always execute.)', + get_current_value=lambda: str(self.accept_input_on_enter or 'meta-enter'), + get_values=lambda: { + '2': lambda: enable('accept_input_on_enter', 2), + '3': lambda: enable('accept_input_on_enter', 3), + '4': lambda: enable('accept_input_on_enter', 4), + 'meta-enter': lambda: enable('accept_input_on_enter', None), + }), ]), OptionCategory('Display', [ - simple_option('Show signature', 'show_signature'), - simple_option('Show docstring', 'show_docstring'), - simple_option('Show line numbers', 'show_line_numbers'), - simple_option('Show status bar', 'show_status_bar'), + Option(title='Completion menu', + description='Visualisation to use for displaying the completions.', + get_current_value=get_completion_menu_value, + get_values=lambda: { + 'off': lambda: disable('show_completions_menu') and disable('show_completions_toolbar'), + 'pop-up': lambda: enable('show_completions_menu') and disable('show_completions_toolbar'), + 'toolbar': lambda: enable('show_completions_toolbar') and disable('show_completions_menu'), + }), + simple_option(title='Show signature', + description='Display function signatures.', + field_name='show_signature'), + simple_option(title='Show docstring', + description='Display function docstrings.', + field_name='show_docstring'), + simple_option(title='Show line numbers', + description='Show line numbers when the input consists of multiple lines.', + field_name='show_line_numbers'), + simple_option(title='Show status bar', + description='Show the status bar at the bottom of the terminal.', + field_name='show_status_bar'), + simple_option(title='Show sidebar help', + description='When the sidebar is visible, also show this help text.', + field_name='show_sidebar_help'), ]), OptionCategory('Colors', [ - Option('Code', lambda: { - name: partial(self.use_code_colorscheme, name) for name in self.code_styles - }, lambda: self._current_code_style_name), - Option('User interface', lambda: { - name: partial(self.use_ui_colorscheme, name) for name in self.ui_styles - }, lambda: self._current_ui_style_name), + Option(title='Code', + description='Color scheme to use for the Python code.', + get_current_value=lambda: self._current_code_style_name, + get_values=lambda: { + name: partial(self.use_code_colorscheme, name) for name in self.code_styles + }), + Option(title='User interface', + description='Color scheme to use for the user interface.', + get_current_value=lambda: self._current_ui_style_name, + get_values=lambda: { + name: partial(self.use_ui_colorscheme, name) for name in self.ui_styles + }), ]), ] diff --git a/ptpython/style.py b/ptpython/style.py index 4f77f295..82effb48 100644 --- a/ptpython/style.py +++ b/ptpython/style.py @@ -101,6 +101,7 @@ class PythonStyle(Style): Token.Sidebar.Separator: 'bg:#bbbbbb #ffffff underline', Token.Sidebar.Key: 'bg:#bbddbb #000000 bold', Token.Sidebar.Key.Description: 'bg:#bbbbbb #000000', + Token.Sidebar.HelpText: 'bg:#bbeebb #000000', # Exit confirmation. Token.ExitConfirmation: 'bg:#884444 #ffffff', From c789e91307b3e73cf993d2eb64bcc9bebaa87b1c Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Wed, 24 Jun 2015 22:21:46 +0200 Subject: [PATCH 021/470] Colorscheme improvements for Windows. --- ptpython/style.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/ptpython/style.py b/ptpython/style.py index 82effb48..1cea16c9 100644 --- a/ptpython/style.py +++ b/ptpython/style.py @@ -5,6 +5,8 @@ from pygments.styles import get_style_by_name, get_all_styles from prompt_toolkit.styles import default_style_extensions +import sys + __all__ = ( 'get_all_code_styles', 'get_all_ui_styles', @@ -108,6 +110,16 @@ class PythonStyle(Style): } +# Some changes to get a bit more contrast on Windows consoles. +# (They only support 16 colors.) +if sys.platform == 'win32': + default_ui_style.update({ + Token.Sidebar.Title: 'bg:#228822 #ffffff underline', + Token.ExitConfirmation: 'bg:#ff4444 #ffffff', + Token.Toolbar.Validation: 'bg:#ff4444 #ffffff', + }) + + blue_ui_style = {} blue_ui_style.update(default_ui_style) blue_ui_style.update({ From b439119b891a2810cdbf023ee1d2794033dbc745 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 25 Jun 2015 18:29:44 +0200 Subject: [PATCH 022/470] Better colorscheme for Windows consoles. --- ptpython/python_input.py | 5 +++++ ptpython/style.py | 34 +++++++++++++++++++++++++++++++--- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 3b4a5f80..bea93e01 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -31,6 +31,7 @@ from functools import partial import six +import sys import __future__ @@ -175,6 +176,10 @@ def __init__(self, self.ui_styles = get_all_ui_styles() self._current_code_style_name = 'default' self._current_ui_style_name = 'default' + + if sys.platform == 'win32': + self._current_code_style_name = 'win32' + self._current_style = self._generate_style() # Options to be configurable from the sidebar. diff --git a/ptpython/style.py b/ptpython/style.py index 1cea16c9..c4779603 100644 --- a/ptpython/style.py +++ b/ptpython/style.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals -from pygments.token import Token +from pygments.token import Token, Keyword, Name, Comment, String, Operator, Number from pygments.style import Style from pygments.styles import get_style_by_name, get_all_styles from prompt_toolkit.styles import default_style_extensions @@ -18,7 +18,9 @@ def get_all_code_styles(): """ Return a mapping from style names to their classes. """ - return dict((name, get_style_by_name(name).styles) for name in get_all_styles()) + result = dict((name, get_style_by_name(name).styles) for name in get_all_styles()) + result['win32'] = win32_code_style + return result def get_all_ui_styles(): @@ -48,6 +50,32 @@ class PythonStyle(Style): return PythonStyle +# Code style for Windows consoles. They support only 16 colors, +# so we choose a combination that displays nicely. +win32_code_style = { + Comment: "#00ff00", + Keyword: '#44ff44', + Number: '', + Operator: '', + String: '#ff44ff', + + Name: '', + Name.Decorator: '#ff4444', + Name.Class: '#ff4444', + Name.Function: '#ff4444', + Name.Builtin: '#ff4444', + + Name.Attribute: '', + Name.Constant: '', + Name.Entity: '', + Name.Exception: '', + Name.Label: '', + Name.Namespace: '', + Name.Tag: '', + Name.Variable: '', +} + + default_ui_style = { # (Python) Prompt: "In [1]:" Token.In: 'bold #008800', @@ -114,7 +142,7 @@ class PythonStyle(Style): # (They only support 16 colors.) if sys.platform == 'win32': default_ui_style.update({ - Token.Sidebar.Title: 'bg:#228822 #ffffff underline', + Token.Sidebar.Title: 'bg:#00ff00 #ffffff', Token.ExitConfirmation: 'bg:#ff4444 #ffffff', Token.Toolbar.Validation: 'bg:#ff4444 #ffffff', }) From 3397e9ae8342ec39b59c25711cbb4f0eaaff1f48 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 25 Jun 2015 18:50:48 +0200 Subject: [PATCH 023/470] Workaround for Jedi bug: handling of signatures that contain keyword-only arguments. --- ptpython/layout.py | 13 ++++++++++--- ptpython/python_input.py | 12 ++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/ptpython/layout.py b/ptpython/layout.py index f6927c7c..96180470 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -160,12 +160,19 @@ def get_tokens(cli): append((Signature.Operator, '(')) for i, p in enumerate(sig.params): - if i == sig.index: + # Workaround for #47: 'p' is None when we hit the '*' in the signature. + # and sig has no 'index' attribute. + # See: https://github.com/jonathanslenders/ptpython/issues/47 + # https://github.com/davidhalter/jedi/issues/598 + description = (p.description if p else '*') #or '*' + sig_index = getattr(sig, 'index', 0) + + if i == sig_index: # Note: we use `_Param.description` instead of # `_Param.name`, that way we also get the '*' before args. - append((Signature.CurrentName, str(p.description))) + append((Signature.CurrentName, str(description))) else: - append((Signature, str(p.description))) + append((Signature, str(description))) append((Signature.Operator, ', ')) if sig.params: diff --git a/ptpython/python_input.py b/ptpython/python_input.py index bea93e01..6c5a50d5 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -516,6 +516,18 @@ def run(): # of probably bugs in jedi. We can silence them. # See: https://github.com/davidhalter/jedi/issues/492 signatures = [] + else: + # Try to access the params attribute just once. For Jedi + # signatures containing the keyword-only argument star, + # this will crash when retrieving it the first time with + # AttributeError. Every following time it works. + # See: https://github.com/jonathanslenders/ptpython/issues/47 + # https://github.com/davidhalter/jedi/issues/598 + try: + if signatures: + signatures[0].params + except AttributeError: + pass else: signatures = [] From 047c955328ed58f01d84389d15dc854ed6e87f24 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 25 Jun 2015 21:13:39 +0200 Subject: [PATCH 024/470] Pypi release 0.16 --- CHANGELOG | 17 +++++++++++++++++ setup.py | 4 ++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 16d47ef2..b4fa0cac 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,23 @@ CHANGELOG ========= +0.16: 2015-06-25 +--------------- + +Upgrade to prompt_toolkit 0.42 + +Fixes: +- Workaround for Jedi bug. (Signatures of functions with keyword-only arguments.) +- Correctly show traceback on Python 3. +- Better styling of the options sidebar. + +New features: +- Exit REPL when input starts with Control-Z. +- Set terminal title. +- Display help text in options sidebar. +- Better colorscheme for Windows. + + 0.15: 2015-06-20 --------------- diff --git a/setup.py b/setup.py index 8d3e4d75..3ec3ac7f 100644 --- a/setup.py +++ b/setup.py @@ -13,13 +13,13 @@ setup( name='ptpython', author='Jonathan Slenders', - version='0.15', + version='0.16', url='https://github.com/jonathanslenders/ptpython', description='Python REPL build on top of prompt_toolkit', long_description=long_description, packages=find_packages('.'), install_requires = [ - 'prompt_toolkit==0.41', + 'prompt_toolkit==0.42', 'jedi>=0.9.0', 'docopt', ], From c59c3396fae7c002eb7d882613e2ede298bbbc78 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Mon, 13 Jul 2015 23:05:42 +0200 Subject: [PATCH 025/470] Clean up unused style. --- ptpython/style.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ptpython/style.py b/ptpython/style.py index c4779603..0ae773dc 100644 --- a/ptpython/style.py +++ b/ptpython/style.py @@ -91,7 +91,6 @@ class PythonStyle(Style): # Search toolbar. Token.Toolbar.Search: '#22aaaa noinherit', Token.Toolbar.Search.Text: 'noinherit', - Token.Toolbar.Search.Text.NoMatch: 'bg:#aa4444 #ffffff', # System toolbar Token.Toolbar.System.Prefix: '#22aaaa noinherit', From 35aef4d66b259a2dff0c6928e0ebf12d3571721f Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Mon, 13 Jul 2015 23:07:23 +0200 Subject: [PATCH 026/470] Integration with Tk eventloop. (This makes and other Tk stuff work again from the REPL.) --- ptpython/eventloop.py | 50 +++++++++++++++++++++++++++++++++++++++++++ ptpython/ipython.py | 4 ++-- ptpython/repl.py | 3 ++- 3 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 ptpython/eventloop.py diff --git a/ptpython/eventloop.py b/ptpython/eventloop.py new file mode 100644 index 00000000..ae7227e0 --- /dev/null +++ b/ptpython/eventloop.py @@ -0,0 +1,50 @@ +""" +Wrapper around the eventloop that gives some time to the Tkinter GUI to process +events when it's loaded and while we are waiting for input at the REPL. This +way we don't block the UI of for instance ``turtle`` and other Tk libraries. + +(Normally Tkinter registeres it's callbacks in ``PyOS_InputHook`` to integrate +in readline. ``prompt-toolkit`` doesn't understand that input hook, but this +will fix it for Tk.) +""" +from prompt_toolkit.shortcuts import create_eventloop as _create_eventloop +import sys + +__all__ = ( + 'create_eventloop', +) + +def _inputhook_tk(inputhook_context): + """ + Inputhook for Tk. + Run the Tk eventloop until prompt-toolkit needs to process the next input. + """ + import _tkinter, Tkinter # Keep this imports inline! + + root = Tkinter._default_root + + if root is not None: + # Add a handler that sets the stop flag when `prompt-toolkit` has input + # to process. + stop = [False] + def done(*a): + stop[0] = True + + root.createfilehandler(inputhook_context.fileno(), _tkinter.READABLE, done) + + # Run the TK event loop as long as we don't receive input. + while root.dooneevent(_tkinter.ALL_EVENTS): + if stop[0]: + break + + root.deletefilehandler(inputhook_context.fileno()) + + +def _inputhook(inputhook_context): + # Only call the real input hook when the 'Tkinter' library was loaded. + if 'Tkinter' in sys.modules: + _inputhook_tk(inputhook_context) + + +def create_eventloop(): + return _create_eventloop(inputhook=_inputhook) diff --git a/ptpython/ipython.py b/ptpython/ipython.py index 5fd73881..dc7b95e8 100644 --- a/ptpython/ipython.py +++ b/ptpython/ipython.py @@ -18,9 +18,9 @@ from prompt_toolkit.document import Document from prompt_toolkit.interface import CommandLineInterface from prompt_toolkit.layout.controls import TokenListControl -from prompt_toolkit.shortcuts import create_eventloop -from ptpython.python_input import PythonInput, PythonValidator, PythonCompleter +from .python_input import PythonInput, PythonValidator, PythonCompleter +from .eventloop import create_eventloop from IPython.terminal.embed import InteractiveShellEmbed as _InteractiveShellEmbed from IPython.terminal.ipapp import load_default_config diff --git a/ptpython/repl.py b/ptpython/repl.py index 46ad5c30..517ac78c 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -16,10 +16,11 @@ from prompt_toolkit.application import AbortAction from prompt_toolkit.interface import AcceptAction, CommandLineInterface from prompt_toolkit.layout.utils import token_list_width -from prompt_toolkit.shortcuts import create_eventloop, create_asyncio_eventloop +from prompt_toolkit.shortcuts import create_asyncio_eventloop from prompt_toolkit.utils import DummyContext, Callback from .python_input import PythonInput +from .eventloop import create_eventloop import os import six From 324b4c4abb2bd527dd9f8ef0dae84dfbb6ac2eca Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Mon, 13 Jul 2015 23:09:20 +0200 Subject: [PATCH 027/470] Added multi-column as an optional completion visualisation. --- examples/ptpython_config/config.py | 6 ++--- ptpython/filters.py | 13 ----------- ptpython/layout.py | 36 +++++++++++++++++++++++++----- ptpython/python_input.py | 27 +++++++++------------- 4 files changed, 45 insertions(+), 37 deletions(-) diff --git a/examples/ptpython_config/config.py b/examples/ptpython_config/config.py index 8c97ff59..e4077c0f 100644 --- a/examples/ptpython_config/config.py +++ b/examples/ptpython_config/config.py @@ -6,6 +6,7 @@ from __future__ import unicode_literals from prompt_toolkit.keys import Keys from pygments.token import Token +from ptpython.layout import CompletionVisualisation __all__ = ( 'configure', @@ -24,9 +25,8 @@ def configure(repl): # Show docstring (bool). repl.show_docstring = False - # Show completions. - repl.show_completions_menu = True - repl.show_completions_toolbar = False + # Show completions. (NONE, POP_UP, MULTI_COLUMN or TOOLBAR) + repl.completion_visualisation = CompletionVisualisation.MULTI_COLUMN # Show line numbers (when the input contains multiple lines.) repl.show_line_numbers = True diff --git a/ptpython/filters.py b/ptpython/filters.py index a6ec1a00..30ef8301 100644 --- a/ptpython/filters.py +++ b/ptpython/filters.py @@ -5,8 +5,6 @@ __all__ = ( 'ShowLineNumbersFilter', 'HasSignature', - 'ShowCompletionsToolbar', - 'ShowCompletionsMenu', 'ShowSidebar', 'ShowDocstring', ) @@ -31,17 +29,6 @@ def __call__(self, cli): return bool(self.python_input.signatures) -class ShowCompletionsToolbar(PythonInputFilter): - def __call__(self, cli): - return self.python_input.show_completions_toolbar - - -class ShowCompletionsMenu(PythonInputFilter): - def __call__(self, cli): - return self.python_input.show_completions_menu and \ - cli.focus_stack.current == 'default' - - class ShowSidebar(PythonInputFilter): def __call__(self, cli): return self.python_input.show_sidebar diff --git a/ptpython/layout.py b/ptpython/layout.py index 96180470..42a3c2a3 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -6,14 +6,14 @@ from prompt_toolkit.layout import Window, HSplit, VSplit, FloatContainer, Float from prompt_toolkit.layout.controls import BufferControl, TokenListControl, FillControl from prompt_toolkit.layout.dimension import LayoutDimension -from prompt_toolkit.layout.menus import CompletionsMenu +from prompt_toolkit.layout.menus import CompletionsMenu, MultiColumnCompletionsMenu from prompt_toolkit.layout.processors import HighlightSearchProcessor, HighlightSelectionProcessor, HighlightMatchingBracketProcessor, ConditionalProcessor from prompt_toolkit.layout.screen import Char from prompt_toolkit.layout.toolbars import CompletionsToolbar, ArgToolbar, SearchToolbar, ValidationToolbar, SystemToolbar, TokenListToolbar from prompt_toolkit.layout.utils import token_list_width from prompt_toolkit.selection import SelectionType -from ptpython.filters import HasSignature, ShowCompletionsMenu, ShowCompletionsToolbar, ShowSidebar, ShowLineNumbersFilter, ShowSignature, ShowDocstring +from ptpython.filters import HasSignature, ShowSidebar, ShowLineNumbersFilter, ShowSignature, ShowDocstring from pygments.lexers import PythonLexer from pygments.token import Token @@ -23,9 +23,30 @@ __all__ = ( 'create_layout', + 'CompletionVisualisation', ) +class CompletionVisualisation: + " Visualisation method for the completions. " + NONE = 'none' + POP_UP = 'pop-up' + MULTI_COLUMN = 'multi-column' + TOOLBAR = 'toolbar' + + +def show_completions_toolbar(python_input): + return Condition(lambda cli: python_input.completion_visualisation == CompletionVisualisation.TOOLBAR) + + +def show_completions_menu(python_input): + return Condition(lambda cli: python_input.completion_visualisation == CompletionVisualisation.POP_UP) + + +def show_multi_column_completions_menu(python_input): + return Condition(lambda cli: python_input.completion_visualisation == CompletionVisualisation.MULTI_COLUMN) + + class PythonSidebar(Window): """ Sidebar containing the configurable options. @@ -190,7 +211,8 @@ def get_tokens(cli): # Show only when there is a signature HasSignature(python_input) & # And there are no completions to be shown. (would cover signature pop-up.) - (~HasCompletions() | ~ShowCompletionsMenu(python_input)) + ~(HasCompletions() & (show_completions_menu(python_input) | + show_multi_column_completions_menu(python_input))) # Signature needs to be shown. & ShowSignature(python_input) & # Not done yet. @@ -400,7 +422,11 @@ def menu_position(cli): ycursor=True, content=CompletionsMenu( max_height=12, - extra_filter=ShowCompletionsMenu(python_input))), + extra_filter=show_completions_menu(python_input))), + Float(xcursor=True, + ycursor=True, + content=MultiColumnCompletionsMenu( + extra_filter=show_multi_column_completions_menu(python_input))), Float(xcursor=True, ycursor=True, content=SignatureToolbar(python_input)), @@ -413,7 +439,7 @@ def menu_position(cli): SearchToolbar(), SystemToolbar(), ValidationToolbar(), - CompletionsToolbar(extra_filter=ShowCompletionsToolbar(python_input)), + CompletionsToolbar(extra_filter=show_completions_toolbar(python_input)), # Docstring region. Window(height=D.exact(1), diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 6c5a50d5..d240bb1c 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -22,7 +22,7 @@ from ptpython.completer import PythonCompleter from ptpython.key_bindings import load_python_bindings, load_sidebar_bindings, load_confirm_exit_bindings -from ptpython.layout import PythonPrompt, create_layout +from ptpython.layout import PythonPrompt, create_layout, CompletionVisualisation from ptpython.style import get_all_code_styles, get_all_ui_styles, generate_style from ptpython.utils import get_jedi_script_from_document, document_is_multiline_python from ptpython.validator import PythonValidator @@ -40,6 +40,7 @@ 'PythonCommandLineInterface', ) + class OptionCategory(object): def __init__(self, title, options): assert isinstance(title, six.text_type) @@ -150,6 +151,8 @@ def __init__(self, self.show_docstring = False self.show_completions_toolbar = False self.show_completions_menu = True + self.completion_visualisation = CompletionVisualisation.POP_UP + self.show_line_numbers = True self.show_status_bar = True self.complete_while_typing = True @@ -343,15 +346,6 @@ def get_values(): get_values=get_values, get_current_value=get_current_value) - def get_completion_menu_value(): - " Return active value for the 'completion menu' option. " - if self.show_completions_menu: - return 'pop-up' - elif self.show_completions_toolbar: - return 'toolbar' - else: - return 'off' - return [ OptionCategory('Input', [ simple_option(title='Input mode', @@ -396,13 +390,14 @@ def get_completion_menu_value(): }), ]), OptionCategory('Display', [ - Option(title='Completion menu', - description='Visualisation to use for displaying the completions.', - get_current_value=get_completion_menu_value, + Option(title='Completions', + description='Visualisation to use for displaying the completions. (Multiple columns, one column, a toolbar or nothing.)', + get_current_value=lambda: self.completion_visualisation, get_values=lambda: { - 'off': lambda: disable('show_completions_menu') and disable('show_completions_toolbar'), - 'pop-up': lambda: enable('show_completions_menu') and disable('show_completions_toolbar'), - 'toolbar': lambda: enable('show_completions_toolbar') and disable('show_completions_menu'), + CompletionVisualisation.NONE: lambda: enable('completion_visualisation', CompletionVisualisation.NONE), + CompletionVisualisation.POP_UP: lambda: enable('completion_visualisation', CompletionVisualisation.POP_UP), + CompletionVisualisation.MULTI_COLUMN: lambda: enable('completion_visualisation', CompletionVisualisation.MULTI_COLUMN), + CompletionVisualisation.TOOLBAR: lambda: enable('completion_visualisation', CompletionVisualisation.TOOLBAR), }), simple_option(title='Show signature', description='Display function signatures.', From 7610888bef22488bd41160ecaf32b09cc724cc55 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 14 Jul 2015 22:59:17 +0200 Subject: [PATCH 028/470] Python 3 + Windows support for TK eventhook. --- ptpython/eventloop.py | 34 +++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/ptpython/eventloop.py b/ptpython/eventloop.py index ae7227e0..5e3fd9de 100644 --- a/ptpython/eventloop.py +++ b/ptpython/eventloop.py @@ -9,6 +9,7 @@ """ from prompt_toolkit.shortcuts import create_eventloop as _create_eventloop import sys +import time __all__ = ( 'create_eventloop', @@ -19,11 +20,16 @@ def _inputhook_tk(inputhook_context): Inputhook for Tk. Run the Tk eventloop until prompt-toolkit needs to process the next input. """ - import _tkinter, Tkinter # Keep this imports inline! + # Get the current TK application. + import _tkinter # Keep this imports inline! + from six.moves import tkinter + root = tkinter._default_root - root = Tkinter._default_root - - if root is not None: + def wait_using_filehandler(): + """ + Run the TK eventloop until the file handler that we got from the + inputhook becomes readable. + """ # Add a handler that sets the stop flag when `prompt-toolkit` has input # to process. stop = [False] @@ -39,10 +45,28 @@ def done(*a): root.deletefilehandler(inputhook_context.fileno()) + def wait_using_polling(): + """ + Windows TK doesn't support 'createfilehandler'. + So, run the TK eventloop and poll until input is ready. + """ + while not inputhook_context.input_is_ready(): + while root.dooneevent(_tkinter.ALL_EVENTS | _tkinter.DONT_WAIT): + pass + # Sleep to make the CPU idle, but not too long, so that the UI + # stays responsive. + time.sleep(.01) + + if root is not None: + if hasattr(root, 'createfilehandler'): + wait_using_filehandler() + else: + wait_using_polling() + def _inputhook(inputhook_context): # Only call the real input hook when the 'Tkinter' library was loaded. - if 'Tkinter' in sys.modules: + if 'Tkinter' in sys.modules or 'tkinter' in sys.modules: _inputhook_tk(inputhook_context) From 9d7c123923eda8c11d611b7c3759268fd984d1e3 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Wed, 15 Jul 2015 13:08:22 +0200 Subject: [PATCH 029/470] Pypi release 0.17 --- CHANGELOG | 11 +++++++++++ setup.py | 4 ++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index b4fa0cac..c473c1fd 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,17 @@ CHANGELOG ========= +0.17: 2015-07-15 +--------------- + +Upgrade to prompt_toolkit 0.43 + +New features: +- Integration with Tk eventloop. (This makes turtle and other Tk stuff work + again from the REPL.) +- Multi column completion visualisation. + + 0.16: 2015-06-25 --------------- diff --git a/setup.py b/setup.py index 3ec3ac7f..482849bf 100644 --- a/setup.py +++ b/setup.py @@ -13,13 +13,13 @@ setup( name='ptpython', author='Jonathan Slenders', - version='0.16', + version='0.17', url='https://github.com/jonathanslenders/ptpython', description='Python REPL build on top of prompt_toolkit', long_description=long_description, packages=find_packages('.'), install_requires = [ - 'prompt_toolkit==0.42', + 'prompt_toolkit==0.43', 'jedi>=0.9.0', 'docopt', ], From 240de503bf4ebcfd641670468cb68f75a3add457 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Wed, 15 Jul 2015 23:31:55 +0200 Subject: [PATCH 030/470] Test package using Travis. (Only syntax checks for now. -- No real unit tests.) --- .travis.yml | 12 ++++++++++++ README.rst | 5 +++++ tests/run_tests.py | 25 +++++++++++++++++++++++++ tox.ini | 10 ++++++++++ 4 files changed, 52 insertions(+) create mode 100644 .travis.yml create mode 100755 tests/run_tests.py create mode 100644 tox.ini diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..02853d77 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,12 @@ +language: python +env: + - TOXENV=py26 + - TOXENV=py27 + - TOXENV=py33 + - TOXENV=py34 + - TOXENV=pypy + - TOXENV=pypy3 +install: + - travis_retry pip install tox +script: + - tox diff --git a/README.rst b/README.rst index 075f6376..5a337214 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,8 @@ ptpython: a better Python REPL ============================== +|Build Status| + .. image:: https://pypip.in/version/ptpython/badge.svg :target: https://pypi.python.org/pypi/ptpython/ :alt: Latest Version @@ -170,6 +172,9 @@ Special thanks to - `wcwidth `_: Determine columns needed for a wide characters. - `prompt_toolkit `_ for the interface. +.. |Build Status| image:: https://api.travis-ci.org/jonathanslenders/ptpython.svg?branch=master + :target: https://travis-ci.org/jonathanslenders/ptpython# + .. |PyPI| image:: https://pypip.in/version/prompt-toolkit/badge.svg :target: https://pypi.python.org/pypi/prompt-toolkit/ :alt: Latest Version diff --git a/tests/run_tests.py b/tests/run_tests.py new file mode 100755 index 00000000..4ed78d35 --- /dev/null +++ b/tests/run_tests.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python +from __future__ import unicode_literals + +import unittest + + +# For now there are no tests here. +# However this is sufficient for Travis to do at least a syntax check. +# That way we are at least sure to restrict to the Python 2.6 syntax. + +import ptpython.completer +import ptpython.filters +#import ptpython.ipython +import ptpython.layout +import ptpython.python_input +import ptpython.style +import ptpython.validator +import ptpython.eventloop +import ptpython.key_bindings +import ptpython.repl +import ptpython.utils + + +if __name__ == '__main__': + unittest.main() diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..40a358ab --- /dev/null +++ b/tox.ini @@ -0,0 +1,10 @@ +# Tox (http://tox.testrun.org/) is a tool for running tests +# in multiple virtualenvs. This configuration file will run the +# test suite on all supported python versions. To use it, "pip install tox" +# and then run "tox" from this directory. + +[tox] +envlist = py26, py27, py33, py34, pypy, pypy3 + +[testenv] +commands = {toxinidir}/tests/run_tests.py From 819cb213d4ccb1d9e3b4af216eaef7410b4a3cb7 Mon Sep 17 00:00:00 2001 From: Alexey Zinoviev Date: Wed, 15 Jul 2015 18:33:31 +0300 Subject: [PATCH 031/470] Fix for Python 2.6 compatibility. --- ptpython/python_input.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ptpython/python_input.py b/ptpython/python_input.py index d240bb1c..8d1d4b52 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -419,15 +419,15 @@ def get_values(): Option(title='Code', description='Color scheme to use for the Python code.', get_current_value=lambda: self._current_code_style_name, - get_values=lambda: { - name: partial(self.use_code_colorscheme, name) for name in self.code_styles - }), + get_values=lambda: dict( + (name, partial(self.use_code_colorscheme, name)) for name in self.code_styles) + ), Option(title='User interface', description='Color scheme to use for the user interface.', get_current_value=lambda: self._current_ui_style_name, - get_values=lambda: { - name: partial(self.use_ui_colorscheme, name) for name in self.ui_styles - }), + get_values=lambda: dict( + (name, partial(self.use_ui_colorscheme, name)) for name in self.ui_styles) + ), ]), ] From 766864954904fb094adaed229b56e21cdccfce44 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Wed, 15 Jul 2015 23:41:43 +0200 Subject: [PATCH 032/470] Pypi release 0.18 --- CHANGELOG | 7 +++++++ setup.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index c473c1fd..73a5fa6c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,13 @@ CHANGELOG ========= +0.18: 2015-07-15 +--------------- + +Fixes: +- Python 2.6 compatibility. + + 0.17: 2015-07-15 --------------- diff --git a/setup.py b/setup.py index 482849bf..dd188242 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ setup( name='ptpython', author='Jonathan Slenders', - version='0.17', + version='0.18', url='https://github.com/jonathanslenders/ptpython', description='Python REPL build on top of prompt_toolkit', long_description=long_description, From 1c51d249ec673cc12b153d4268c62f5ebc169b41 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sat, 25 Jul 2015 13:39:54 +0200 Subject: [PATCH 033/470] Cleaned up unused variables. --- ptpython/python_input.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 8d1d4b52..c5a2d3fb 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -149,8 +149,6 @@ def __init__(self, # Settings. self.show_signature = True self.show_docstring = False - self.show_completions_toolbar = False - self.show_completions_menu = True self.completion_visualisation = CompletionVisualisation.POP_UP self.show_line_numbers = True From 00f6b5f0f5e422b7973fa985bdd6ec75ce824995 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sat, 25 Jul 2015 17:15:53 +0200 Subject: [PATCH 034/470] Use original colors when running on the ConEmu Windows console (Which supports 256 colors). --- ptpython/python_input.py | 4 ++-- ptpython/style.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/ptpython/python_input.py b/ptpython/python_input.py index c5a2d3fb..25f1e89f 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -17,7 +17,7 @@ from prompt_toolkit.history import FileHistory, History from prompt_toolkit.interface import CommandLineInterface, Application, AcceptAction from prompt_toolkit.key_binding.manager import KeyBindingManager -from prompt_toolkit.utils import Callback +from prompt_toolkit.utils import Callback, is_windows from prompt_toolkit.validation import SwitchableValidator from ptpython.completer import PythonCompleter @@ -178,7 +178,7 @@ def __init__(self, self._current_code_style_name = 'default' self._current_ui_style_name = 'default' - if sys.platform == 'win32': + if is_windows(): self._current_code_style_name = 'win32' self._current_style = self._generate_style() diff --git a/ptpython/style.py b/ptpython/style.py index 0ae773dc..4f720304 100644 --- a/ptpython/style.py +++ b/ptpython/style.py @@ -4,6 +4,7 @@ from pygments.style import Style from pygments.styles import get_style_by_name, get_all_styles from prompt_toolkit.styles import default_style_extensions +from prompt_toolkit.utils import is_windows, is_conemu_ansi import sys @@ -139,7 +140,7 @@ class PythonStyle(Style): # Some changes to get a bit more contrast on Windows consoles. # (They only support 16 colors.) -if sys.platform == 'win32': +if is_windows() and not is_conemu_ansi(): default_ui_style.update({ Token.Sidebar.Title: 'bg:#00ff00 #ffffff', Token.ExitConfirmation: 'bg:#ff4444 #ffffff', From 19a5a9134d5720fe96568200f91e7fd595ff279d Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sat, 25 Jul 2015 17:16:21 +0200 Subject: [PATCH 035/470] Make completion_menu_scroll_offset configurable. --- examples/ptpython_config/config.py | 4 ++++ ptpython/layout.py | 3 +++ ptpython/python_input.py | 1 + 3 files changed, 8 insertions(+) diff --git a/examples/ptpython_config/config.py b/examples/ptpython_config/config.py index e4077c0f..543cd287 100644 --- a/examples/ptpython_config/config.py +++ b/examples/ptpython_config/config.py @@ -28,6 +28,10 @@ def configure(repl): # Show completions. (NONE, POP_UP, MULTI_COLUMN or TOOLBAR) repl.completion_visualisation = CompletionVisualisation.MULTI_COLUMN + # When CompletionVisualisation.POP_UP has been chosen, use this + # scroll_offset in the completion menu. + repl.completion_menu_scroll_offset = 0 + # Show line numbers (when the input contains multiple lines.) repl.show_line_numbers = True diff --git a/ptpython/layout.py b/ptpython/layout.py index 42a3c2a3..37ed651f 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -11,6 +11,7 @@ from prompt_toolkit.layout.screen import Char from prompt_toolkit.layout.toolbars import CompletionsToolbar, ArgToolbar, SearchToolbar, ValidationToolbar, SystemToolbar, TokenListToolbar from prompt_toolkit.layout.utils import token_list_width +from prompt_toolkit.reactive import Integer from prompt_toolkit.selection import SelectionType from ptpython.filters import HasSignature, ShowSidebar, ShowLineNumbersFilter, ShowSignature, ShowDocstring @@ -421,6 +422,8 @@ def menu_position(cli): Float(xcursor=True, ycursor=True, content=CompletionsMenu( + scroll_offset=Integer.from_callable( + lambda: python_input.completion_menu_scroll_offset), max_height=12, extra_filter=show_completions_menu(python_input))), Float(xcursor=True, diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 25f1e89f..22648079 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -150,6 +150,7 @@ def __init__(self, self.show_signature = True self.show_docstring = False self.completion_visualisation = CompletionVisualisation.POP_UP + self.completion_menu_scroll_offset = 1 self.show_line_numbers = True self.show_status_bar = True From 70ad87b86bf312c5ed527d11f9051c8f89048fdb Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 28 Jul 2015 22:04:29 +0200 Subject: [PATCH 036/470] A few style improvements. --- ptpython/style.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/ptpython/style.py b/ptpython/style.py index 4f720304..cd1b3fc1 100644 --- a/ptpython/style.py +++ b/ptpython/style.py @@ -6,8 +6,6 @@ from prompt_toolkit.styles import default_style_extensions from prompt_toolkit.utils import is_windows, is_conemu_ansi -import sys - __all__ = ( 'get_all_code_styles', 'get_all_ui_styles', @@ -113,7 +111,7 @@ class PythonStyle(Style): # Status toolbar. Token.Toolbar.Status: 'bg:#222222 #aaaaaa', Token.Toolbar.Status.InputMode: 'bg:#222222 #ffffaa', - Token.Toolbar.Status.Off: 'bg:#222222 #888888', + Token.Toolbar.Status.Off: 'bg:#222222 #aaaaaa', Token.Toolbar.Status.On: 'bg:#222222 #ffffff', Token.Toolbar.Status.PythonVersion: 'bg:#222222 #ffffff bold', @@ -122,7 +120,7 @@ class PythonStyle(Style): # The options sidebar. Token.Sidebar: 'bg:#bbbbbb #000000', - Token.Sidebar.Title: 'bg:#668866 #ffffff underline', + Token.Sidebar.Title: 'bg:#668866 #ffffff', Token.Sidebar.Label: 'bg:#bbbbbb #222222', Token.Sidebar.Status: 'bg:#dddddd #000011', Token.Sidebar.Selected.Label: 'bg:#222222 #eeeeee', @@ -131,7 +129,7 @@ class PythonStyle(Style): Token.Sidebar.Separator: 'bg:#bbbbbb #ffffff underline', Token.Sidebar.Key: 'bg:#bbddbb #000000 bold', Token.Sidebar.Key.Description: 'bg:#bbbbbb #000000', - Token.Sidebar.HelpText: 'bg:#bbeebb #000000', + Token.Sidebar.HelpText: 'bg:#fdf6e3 #000011', # Exit confirmation. Token.ExitConfirmation: 'bg:#884444 #ffffff', @@ -145,6 +143,9 @@ class PythonStyle(Style): Token.Sidebar.Title: 'bg:#00ff00 #ffffff', Token.ExitConfirmation: 'bg:#ff4444 #ffffff', Token.Toolbar.Validation: 'bg:#ff4444 #ffffff', + + Token.Menu.Completions.Completion: 'bg:#ffffff #000000', + Token.Menu.Completions.Completion.Current: 'bg:#aaaaaa #000000', }) From 30a08e7996f4a6feec60a126f48019a69125e257 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 28 Jul 2015 22:05:06 +0200 Subject: [PATCH 037/470] Added --interactive option for ptipython. --- ptpython/entry_points/run_ptipython.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/ptpython/entry_points/run_ptipython.py b/ptpython/entry_points/run_ptipython.py index c56344e7..989bdea5 100644 --- a/ptpython/entry_points/run_ptipython.py +++ b/ptpython/entry_points/run_ptipython.py @@ -3,13 +3,14 @@ ptipython: IPython interactive shell with the `prompt_toolkit` front-end. Usage: ptpython [ --vi ] - [ --config-dir= ] + [ --config-dir= ] [ --interactive= ] [--] [ ... ] ptpython -h | --help Options: --vi : Use Vi keybindings instead of Emacs bindings. --config-dir= : Pass config directory. By default '~/.ptpython/'. + -i, --interactive= : Start interactive shell after executing this file. """ from __future__ import absolute_import, unicode_literals @@ -51,6 +52,18 @@ def run(): # the IPython shell.) user_ns = {} + # --interactive + if a['--interactive']: + path = a['--interactive'] + + if os.path.exists(path): + with open(path, 'r') as f: + code = compile(f.read(), path, 'exec') + six.exec_(code, user_ns, user_ns) + else: + print('File not found: {}\n\n'.format(path)) + sys.exit(1) + # Apply config file def configure(repl): path = os.path.join(config_dir, 'config.py') From 0facdd6218e852d998e5dc190e57486e22955b90 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 30 Jul 2015 21:33:11 +0200 Subject: [PATCH 038/470] Removed unused import. --- ptpython/python_input.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 22648079..1cf72430 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -31,7 +31,6 @@ from functools import partial import six -import sys import __future__ From 8eb6950326ffd83758b17f38ed7d5de100d0eadd Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 30 Jul 2015 21:32:20 +0200 Subject: [PATCH 039/470] Pypi release 0.19. --- CHANGELOG | 10 ++++++++++ setup.py | 4 ++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 73a5fa6c..25f932a5 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,16 @@ CHANGELOG ========= +0.19: 2015-07-30 +--------------- + +Upgrade to prompt_toolkit 0.44 + +New features: +- Added --interactive option for ptipython. +- A few style improvements. + + 0.18: 2015-07-15 --------------- diff --git a/setup.py b/setup.py index dd188242..35769ece 100644 --- a/setup.py +++ b/setup.py @@ -13,13 +13,13 @@ setup( name='ptpython', author='Jonathan Slenders', - version='0.18', + version='0.19', url='https://github.com/jonathanslenders/ptpython', description='Python REPL build on top of prompt_toolkit', long_description=long_description, packages=find_packages('.'), install_requires = [ - 'prompt_toolkit==0.43', + 'prompt_toolkit==0.44', 'jedi>=0.9.0', 'docopt', ], From bf6b5c8ffced437d95cbd7dc0bafe4dbbde3ac23 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 30 Jul 2015 23:04:35 +0200 Subject: [PATCH 040/470] Pypi release 0.20 --- CHANGELOG | 6 ++++++ setup.py | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 25f932a5..86be1643 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,12 @@ CHANGELOG ========= +0.20: 2015-07-30 +--------------- + +Upgrade to prompt_toolkit 0.45 + + 0.19: 2015-07-30 --------------- diff --git a/setup.py b/setup.py index 35769ece..2e588249 100644 --- a/setup.py +++ b/setup.py @@ -13,13 +13,13 @@ setup( name='ptpython', author='Jonathan Slenders', - version='0.19', + version='0.20', url='https://github.com/jonathanslenders/ptpython', description='Python REPL build on top of prompt_toolkit', long_description=long_description, packages=find_packages('.'), install_requires = [ - 'prompt_toolkit==0.44', + 'prompt_toolkit==0.45', 'jedi>=0.9.0', 'docopt', ], From 549ddd38d28dd21a5cd41caac399a0fd0210f56c Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Mon, 3 Aug 2015 00:22:17 +0200 Subject: [PATCH 041/470] Correctly add current directory to sys.path. Similar to IPython, this should be an empty string as the first item in sys.path https://github.com/ipython/ipython/blob/5e2eb98837352035a9b08df4e1869dec9f95937c/IPython/core/shellapp.py#L206 --- ptpython/entry_points/run_ptipython.py | 3 ++- ptpython/entry_points/run_ptpython.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/ptpython/entry_points/run_ptipython.py b/ptpython/entry_points/run_ptipython.py index 989bdea5..352d65c1 100644 --- a/ptpython/entry_points/run_ptipython.py +++ b/ptpython/entry_points/run_ptipython.py @@ -38,7 +38,8 @@ def run(): from ptpython.repl import run_config, enable_deprecation_warnings # Add the current directory to `sys.path`. - sys.path.append('.') + if sys.path[0] != '': + sys.path.insert(0, '') # When a file has been given, run that, otherwise start the shell. if a['']: diff --git a/ptpython/entry_points/run_ptpython.py b/ptpython/entry_points/run_ptpython.py index 0ec1d2de..712f44eb 100644 --- a/ptpython/entry_points/run_ptpython.py +++ b/ptpython/entry_points/run_ptpython.py @@ -45,7 +45,8 @@ def run(): startup_paths.append(a['--interactive']) # Add the current directory to `sys.path`. - sys.path.append('.') + if sys.path[0] != '': + sys.path.insert(0, '') # When a file has been given, run that, otherwise start the shell. if a['']: From 340164547fefef35dd304f554adccbc9edee2efd Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Mon, 3 Aug 2015 03:22:06 +0200 Subject: [PATCH 042/470] Added Control-E Control-E key binding to example config as alternative for Meta+Enter. --- examples/ptpython_config/config.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/examples/ptpython_config/config.py b/examples/ptpython_config/config.py index 543cd287..b8894464 100644 --- a/examples/ptpython_config/config.py +++ b/examples/ptpython_config/config.py @@ -87,6 +87,14 @@ def _(event): ' Pressing Control-B will insert "pdb.set_trace()" ' event.cli.current_buffer.insert_text('\nimport pdb; pdb.set_trace()\n') + # Typing ControlE twice should also execute the current command. + # (Alternative for Meta-Enter.) + @repl.add_key_binding(Keys.ControlE, Keys.ControlE) + def _(event): + b = event.current_buffer + if b.accept_action.is_returnable: + b.accept_action.validate_and_handle(event.cli, b) + # Custom key binding for some simple autocorrection while typing. corrections = { 'impotr': 'import', From 846f0d29dd3c6a3d8a8332f728330703fac2b850 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Fri, 7 Aug 2015 18:57:13 +0200 Subject: [PATCH 043/470] Using ConditionalContainer everywhere. --- ptpython/layout.py | 78 +++++++++++++++++++++++++--------------------- 1 file changed, 43 insertions(+), 35 deletions(-) diff --git a/ptpython/layout.py b/ptpython/layout.py index 37ed651f..47f6170f 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -3,7 +3,7 @@ from prompt_toolkit.enums import DEFAULT_BUFFER from prompt_toolkit.filters import IsDone, HasCompletions, RendererHeightIsKnown, Always, HasFocus, Condition from prompt_toolkit.key_binding.vi_state import InputMode -from prompt_toolkit.layout import Window, HSplit, VSplit, FloatContainer, Float +from prompt_toolkit.layout import Window, HSplit, VSplit, FloatContainer, Float, ConditionalContainer from prompt_toolkit.layout.controls import BufferControl, TokenListControl, FillControl from prompt_toolkit.layout.dimension import LayoutDimension from prompt_toolkit.layout.menus import CompletionsMenu, MultiColumnCompletionsMenu @@ -48,7 +48,7 @@ def show_multi_column_completions_menu(python_input): return Condition(lambda cli: python_input.completion_visualisation == CompletionVisualisation.MULTI_COLUMN) -class PythonSidebar(Window): +class PythonSidebar(ConditionalContainer): """ Sidebar containing the configurable options. """ @@ -93,15 +93,16 @@ def append(selected, label, status): return tokens super(PythonSidebar, self).__init__( - TokenListControl(get_tokens, Char(token=Token.Sidebar), - has_focus=ShowSidebar(python_input) & ~IsDone()), - width=LayoutDimension.exact(43), - height=LayoutDimension(min=3), - scroll_offset=1, + content=Window( + TokenListControl(get_tokens, Char(token=Token.Sidebar), + has_focus=ShowSidebar(python_input) & ~IsDone()), + width=LayoutDimension.exact(43), + height=LayoutDimension(min=3), + scroll_offset=1), filter=ShowSidebar(python_input) & ~IsDone()) -class PythonSidebarNavigation(Window): +class PythonSidebarNavigation(ConditionalContainer): """ Showing the navigation information for the sidebar. """ @@ -126,13 +127,14 @@ def get_tokens(cli): return tokens super(PythonSidebarNavigation, self).__init__( - TokenListControl(get_tokens, Char(token=Token.Sidebar)), - width=LayoutDimension.exact(43), - height=LayoutDimension.exact(2), + content=Window( + TokenListControl(get_tokens, Char(token=Token.Sidebar)), + width=LayoutDimension.exact(43), + height=LayoutDimension.exact(2)), filter=ShowSidebar(python_input) & ~IsDone()) -class PythonSidebarHelp(Window): +class PythonSidebarHelp(ConditionalContainer): """ Help text for the current item in the sidebar. """ @@ -155,13 +157,14 @@ def get_tokens(cli): return [(token, get_current_description())] super(PythonSidebarHelp, self).__init__( - TokenListControl(get_tokens, Char(token=token)), - height=LayoutDimension(min=3), + content=Window( + TokenListControl(get_tokens, Char(token=token)), + height=LayoutDimension(min=3)), filter=ShowSidebar(python_input) & Condition(lambda cli: python_input.show_sidebar_help) & ~IsDone()) -class SignatureToolbar(Window): +class SignatureToolbar(ConditionalContainer): def __init__(self, python_input): def get_tokens(cli): result = [] @@ -206,8 +209,9 @@ def get_tokens(cli): return result super(SignatureToolbar, self).__init__( - TokenListControl(get_tokens), - height=LayoutDimension.exact(1), + content=Window( + TokenListControl(get_tokens), + height=LayoutDimension.exact(1)), filter= # Show only when there is a signature HasSignature(python_input) & @@ -317,7 +321,7 @@ def get_inputmode_tokens(token, key_bindings_manager, python_input, cli): return result -class ShowSidebarButtonInfo(Window): +class ShowSidebarButtonInfo(ConditionalContainer): def __init__(self, python_input): token = Token.Toolbar.Status @@ -336,15 +340,16 @@ def get_tokens(cli): return tokens super(ShowSidebarButtonInfo, self).__init__( - TokenListControl(get_tokens, default_char=Char(token=token)), + content=Window( + TokenListControl(get_tokens, default_char=Char(token=token)), + height=LayoutDimension.exact(1), + width=LayoutDimension.exact(width)), filter=~IsDone() & RendererHeightIsKnown() & Condition(lambda cli: python_input.show_status_bar and - not python_input.show_exit_confirmation), - height=LayoutDimension.exact(1), - width=LayoutDimension.exact(width)) + not python_input.show_exit_confirmation)) -class ExitConfirmation(Window): +class ExitConfirmation(ConditionalContainer): """ Display exit message. """ @@ -359,8 +364,9 @@ def get_tokens(cli): visible = ~IsDone() & Condition(lambda cli: python_input.show_exit_confirmation) super(ExitConfirmation, self).__init__( - TokenListControl(get_tokens, default_char=Char(token=token), - has_focus=visible), + content=Window( + TokenListControl(get_tokens, default_char=Char(token=token), + has_focus=visible)), filter=visible) @@ -445,17 +451,19 @@ def menu_position(cli): CompletionsToolbar(extra_filter=show_completions_toolbar(python_input)), # Docstring region. - Window(height=D.exact(1), - content=FillControl('\u2500', token=Token.Separator), - filter=HasSignature(python_input) & ShowDocstring(python_input) & ~IsDone()), - Window( - BufferControl( - buffer_name='docstring', - default_token=Token.Docstring, - #lexer=PythonLexer, - ), + ConditionalContainer( + content=Window(height=D.exact(1), + content=FillControl('\u2500', token=Token.Separator)), + filter=HasSignature(python_input) & ShowDocstring(python_input) & ~IsDone()), + ConditionalContainer( + content=Window( + BufferControl( + buffer_name='docstring', + default_token=Token.Docstring, + #lexer=PythonLexer, + ), + height=D(max=12)), filter=HasSignature(python_input) & ShowDocstring(python_input) & ~IsDone(), - height=D(max=12), ), ]), ] + extra_sidebars + [ From 073237cca08cf40cdb3655e7cb9072b9f0665d62 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Fri, 7 Aug 2015 18:57:30 +0200 Subject: [PATCH 044/470] Styling fix. --- ptpython/style.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ptpython/style.py b/ptpython/style.py index cd1b3fc1..fec8e676 100644 --- a/ptpython/style.py +++ b/ptpython/style.py @@ -92,7 +92,7 @@ class PythonStyle(Style): Token.Toolbar.Search.Text: 'noinherit', # System toolbar - Token.Toolbar.System.Prefix: '#22aaaa noinherit', + Token.Toolbar.System: '#22aaaa noinherit', # "arg" toolbar. Token.Toolbar.Arg: '#22aaaa noinherit', From 34fb6241eb0dfe534ffd2f95cbdf83f4963ad1b2 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sat, 8 Aug 2015 20:48:09 +0200 Subject: [PATCH 045/470] Only show search highlighting when the search is the current input buffer. --- ptpython/layout.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ptpython/layout.py b/ptpython/layout.py index 47f6170f..8b40a6ac 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals -from prompt_toolkit.enums import DEFAULT_BUFFER +from prompt_toolkit.enums import DEFAULT_BUFFER, SEARCH_BUFFER from prompt_toolkit.filters import IsDone, HasCompletions, RendererHeightIsKnown, Always, HasFocus, Condition from prompt_toolkit.key_binding.vi_state import InputMode from prompt_toolkit.layout import Window, HSplit, VSplit, FloatContainer, Float, ConditionalContainer @@ -400,7 +400,9 @@ def menu_position(cli): ConditionalProcessor( processor=HighlightMatchingBracketProcessor(chars='[](){}'), filter=HasFocus(DEFAULT_BUFFER) & ~IsDone()), - HighlightSearchProcessor(preview_search=Always()), + ConditionalProcessor( + processor=HighlightSearchProcessor(preview_search=Always()), + filter=HasFocus(SEARCH_BUFFER)), HighlightSelectionProcessor()] + extra_buffer_processors, menu_position=menu_position, From 34b068b4640e21123c1161fa4a362d58bf703082 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sat, 8 Aug 2015 21:49:46 +0200 Subject: [PATCH 046/470] Pypi release 0.21 --- CHANGELOG | 11 +++++++++++ setup.py | 4 ++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 86be1643..deee80d0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,17 @@ CHANGELOG ========= +0.21: 2015-08-08 +--------------- + +Upgrade to prompt_toolkit 0.46 + +Fixes: +- Correctly add current directory to sys.path. +- Only show search highlighting when the search is the current input buffer. +- Styling fix. + + 0.20: 2015-07-30 --------------- diff --git a/setup.py b/setup.py index 2e588249..39c0d623 100644 --- a/setup.py +++ b/setup.py @@ -13,13 +13,13 @@ setup( name='ptpython', author='Jonathan Slenders', - version='0.20', + version='0.21', url='https://github.com/jonathanslenders/ptpython', description='Python REPL build on top of prompt_toolkit', long_description=long_description, packages=find_packages('.'), install_requires = [ - 'prompt_toolkit==0.45', + 'prompt_toolkit==0.46', 'jedi>=0.9.0', 'docopt', ], From 4199b1c93bbbd9138f16266dc44616c2e384ecae Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 13 Aug 2015 05:00:38 +0200 Subject: [PATCH 047/470] Make exit message configurable. --- ptpython/layout.py | 3 ++- ptpython/python_input.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/ptpython/layout.py b/ptpython/layout.py index 8b40a6ac..282ef79e 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -355,8 +355,9 @@ class ExitConfirmation(ConditionalContainer): """ def __init__(self, python_input, token=Token.ExitConfirmation): def get_tokens(cli): + # Show "Do you really want to exit?" return [ - (token, '\n Do you really want to exit? ([y]/n)'), + (token, '\n %s ([y]/n)' % python_input.exit_message), (Token.SetCursorPosition, ''), (token, ' \n'), ] diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 1cf72430..f72f648f 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -171,6 +171,7 @@ def __init__(self, self.show_sidebar_help = True # When the sidebar is visible, also show the help text. self.show_exit_confirmation = False # Currently show 'Do you really want to exit?' self.terminal_title = None # The title to be displayed in the terminal. (None or string.) + self.exit_message = 'Do you really want to exit?' #: Load styles. self.code_styles = get_all_code_styles() From 01dd556d45f6bae926c094250d5eaee28682691a Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 13 Aug 2015 20:07:12 +0200 Subject: [PATCH 048/470] Correctly accept file parameter in the print function of asyncssh_repl.ReplSSHServerSession. --- ptpython/contrib/asyncssh_repl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ptpython/contrib/asyncssh_repl.py b/ptpython/contrib/asyncssh_repl.py index d2d903e3..02b8fd9b 100644 --- a/ptpython/contrib/asyncssh_repl.py +++ b/ptpython/contrib/asyncssh_repl.py @@ -126,7 +126,7 @@ def _print(self, *data, **kw): # installing.) sep = kw.pop('sep', ' ') end = kw.pop('end', '\n') - file = kw.pop('end', None) + _ = kw.pop('file', None) assert not kw, 'Too many keyword-only arguments' data = sep.join(map(str, data)) From 9383f039a87284fe0805cd92107ceeeea15c2244 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 13 Aug 2015 20:08:04 +0200 Subject: [PATCH 049/470] A few fixes to stay compatible with prompt_toolkit. --- ptpython/layout.py | 9 ++++++--- ptpython/python_input.py | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/ptpython/layout.py b/ptpython/layout.py index 282ef79e..af159e64 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -6,6 +6,7 @@ from prompt_toolkit.layout import Window, HSplit, VSplit, FloatContainer, Float, ConditionalContainer from prompt_toolkit.layout.controls import BufferControl, TokenListControl, FillControl from prompt_toolkit.layout.dimension import LayoutDimension +from prompt_toolkit.layout.margins import ConditionalMargin, NumberredMargin from prompt_toolkit.layout.menus import CompletionsMenu, MultiColumnCompletionsMenu from prompt_toolkit.layout.processors import HighlightSearchProcessor, HighlightSelectionProcessor, HighlightMatchingBracketProcessor, ConditionalProcessor from prompt_toolkit.layout.screen import Char @@ -242,7 +243,7 @@ def get_tokens(cli): class PythonToolbar(TokenListToolbar): def __init__(self, key_bindings_manager, python_input, token=Token.Toolbar.Status): def get_tokens(cli): - python_buffer = cli.buffers['default'] + python_buffer = cli.buffers[DEFAULT_BUFFER] TB = token result = [] @@ -384,7 +385,7 @@ def menu_position(cli): When there is no autocompletion menu to be shown, and we have a signature, set the pop-up position at `bracket_start`. """ - b = cli.buffers['default'] + b = cli.buffers[DEFAULT_BUFFER] if b.complete_state is None and python_input.signatures: row, col = python_input.signatures[0].bracket_start @@ -395,7 +396,9 @@ def menu_position(cli): BufferControl( buffer_name=DEFAULT_BUFFER, lexer=lexer, - show_line_numbers=ShowLineNumbersFilter(python_input), + margin=ConditionalMargin( + NumberredMargin(), + filter=ShowLineNumbersFilter(python_input)), input_processors=[ # Show matching parentheses, but only while editing. ConditionalProcessor( diff --git a/ptpython/python_input.py b/ptpython/python_input.py index f72f648f..7a967412 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -435,7 +435,7 @@ def create_application(self): Create an `Application` instance for use in a `CommandLineInterface`. """ buffers = { - 'docstring': Buffer(), # XXX: make docstring read only. + 'docstring': Buffer(read_only=True), } buffers.update(self._extra_buffers or {}) From 39233b575d63e6243f2ee63a683e7a0bf0afcd72 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Fri, 14 Aug 2015 21:23:05 +0200 Subject: [PATCH 050/470] Improve start-up time: Lazy load completer grammar and lazy-import jedi. --- ptpython/completer.py | 33 ++++++++++++++++++++++++++------- ptpython/utils.py | 4 +++- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/ptpython/completer.py b/ptpython/completer.py index 88f42219..0ac8f743 100644 --- a/ptpython/completer.py +++ b/ptpython/completer.py @@ -24,9 +24,32 @@ def __init__(self, get_globals, get_locals): self.get_globals = get_globals self.get_locals = get_locals - self._path_completer_grammar, self._path_completer = self._create_path_completer() + self._path_completer_cache = None + self._path_completer_grammar_cache = None + + @property + def _path_completer(self): + if self._path_completer_cache is None: + self._path_completer_cache = GrammarCompleter( + self._path_completer_grammar, { + 'var1': PathCompleter(expanduser=True), + 'var2': PathCompleter(expanduser=True), + }) + return self._path_completer_cache + + @property + def _path_completer_grammar(self): + """ + Return the grammar for matching paths inside strings inside Python + code. + """ + # We make this lazy, because it delays startup time a little bit. + # This way, the grammar is build during the first completion. + if self._path_completer_grammar_cache is None: + self._path_completer_grammar_cache = self._create_path_completer_grammar() + return self._path_completer_grammar_cache - def _create_path_completer(self): + def _create_path_completer_grammar(self): def unwrapper(text): return re.sub(r'\\(.)', r'\1', text) @@ -61,7 +84,7 @@ def double_quoted_wrapper(text): ) """ - g = compile_grammar( + return compile_grammar( grammar, escape_funcs={ 'var1': single_quoted_wrapper, @@ -71,10 +94,6 @@ def double_quoted_wrapper(text): 'var1': unwrapper, 'var2': unwrapper, }) - return g, GrammarCompleter(g, { - 'var1': PathCompleter(expanduser=True), - 'var2': PathCompleter(expanduser=True), - }) def _complete_path_while_typing(self, document): char_before_cursor = document.char_before_cursor diff --git a/ptpython/utils.py b/ptpython/utils.py index 56bf4d00..549b33d5 100644 --- a/ptpython/utils.py +++ b/ptpython/utils.py @@ -3,7 +3,6 @@ """ from __future__ import unicode_literals -import jedi import re __all__ = ( @@ -41,6 +40,9 @@ def has_unclosed_brackets(text): def get_jedi_script_from_document(document, locals, globals): + import jedi # We keep this import in-line, to improve start-up time. + # Importing Jedi is 'slow'. + try: return jedi.Interpreter( document.text, From 7fd710db1056efacd81554766c3a11b2d3167ef2 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sat, 15 Aug 2015 22:42:26 +0200 Subject: [PATCH 051/470] Using PygmentsLexer (prompt_toolkit change.) --- ptpython/layout.py | 3 ++- ptpython/python_input.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/ptpython/layout.py b/ptpython/layout.py index af159e64..7560cb89 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -6,6 +6,7 @@ from prompt_toolkit.layout import Window, HSplit, VSplit, FloatContainer, Float, ConditionalContainer from prompt_toolkit.layout.controls import BufferControl, TokenListControl, FillControl from prompt_toolkit.layout.dimension import LayoutDimension +from prompt_toolkit.layout.lexers import SimpleLexer from prompt_toolkit.layout.margins import ConditionalMargin, NumberredMargin from prompt_toolkit.layout.menus import CompletionsMenu, MultiColumnCompletionsMenu from prompt_toolkit.layout.processors import HighlightSearchProcessor, HighlightSelectionProcessor, HighlightMatchingBracketProcessor, ConditionalProcessor @@ -465,7 +466,7 @@ def menu_position(cli): content=Window( BufferControl( buffer_name='docstring', - default_token=Token.Docstring, + lexer=SimpleLexer(default_token=Token.Docstring), #lexer=PythonLexer, ), height=D(max=12)), diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 7a967412..df8de21d 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -17,6 +17,7 @@ from prompt_toolkit.history import FileHistory, History from prompt_toolkit.interface import CommandLineInterface, Application, AcceptAction from prompt_toolkit.key_binding.manager import KeyBindingManager +from prompt_toolkit.layout.lexers import PygmentsLexer from prompt_toolkit.utils import Callback, is_windows from prompt_toolkit.validation import SwitchableValidator @@ -134,7 +135,7 @@ def __init__(self, self._completer = _completer or PythonCompleter(self.get_globals, self.get_locals) self._validator = _validator or PythonValidator(self.get_compiler_flags) self._history = FileHistory(history_filename) if history_filename else History() - self._lexer = _lexer or PythonLexer + self._lexer = _lexer or PygmentsLexer(PythonLexer) self._extra_buffers = _extra_buffers self._accept_action = _accept_action self._on_exit = _on_exit From 297936cedff98a8fa2b6c74df7c14d2aa313ef0d Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 20 Aug 2015 02:24:57 +0200 Subject: [PATCH 052/470] Upgrade to prompt_toolkit 0.47 --- ptpython/python_input.py | 4 ++-- setup.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ptpython/python_input.py b/ptpython/python_input.py index df8de21d..fa7e5c25 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -14,7 +14,7 @@ from prompt_toolkit.document import Document from prompt_toolkit.enums import DEFAULT_BUFFER from prompt_toolkit.filters import Condition, Always -from prompt_toolkit.history import FileHistory, History +from prompt_toolkit.history import FileHistory, InMemoryHistory from prompt_toolkit.interface import CommandLineInterface, Application, AcceptAction from prompt_toolkit.key_binding.manager import KeyBindingManager from prompt_toolkit.layout.lexers import PygmentsLexer @@ -134,7 +134,7 @@ def __init__(self, self._completer = _completer or PythonCompleter(self.get_globals, self.get_locals) self._validator = _validator or PythonValidator(self.get_compiler_flags) - self._history = FileHistory(history_filename) if history_filename else History() + self._history = FileHistory(history_filename) if history_filename else InMemoryHistory() self._lexer = _lexer or PygmentsLexer(PythonLexer) self._extra_buffers = _extra_buffers self._accept_action = _accept_action diff --git a/setup.py b/setup.py index 39c0d623..02af5266 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ long_description=long_description, packages=find_packages('.'), install_requires = [ - 'prompt_toolkit==0.46', + 'prompt_toolkit==0.47', 'jedi>=0.9.0', 'docopt', ], From 42b5a6e746250bcc23f43f7140ddc46adf9485da Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 20 Aug 2015 02:42:47 +0200 Subject: [PATCH 053/470] Added extra parameters for ptpdb (ptpython debugger.) --- ptpython/layout.py | 17 ++++++++++++----- ptpython/python_input.py | 11 ++++++++--- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/ptpython/layout.py b/ptpython/layout.py index 7560cb89..965db431 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -374,11 +374,14 @@ def get_tokens(cli): def create_layout(python_input, key_bindings_manager, - python_prompt_control=None, lexer=PythonLexer, extra_sidebars=None, - extra_buffer_processors=None): + python_prompt_control=None, lexer=PythonLexer, + extra_body=None, extra_toolbars=None, + extra_buffer_processors=None, input_buffer_height=None): D = LayoutDimension - extra_sidebars = extra_sidebars or [] + extra_body = [extra_body] if extra_body else [] + extra_toolbars = extra_toolbars or [] extra_buffer_processors = extra_buffer_processors or [] + input_buffer_height = input_buffer_height or D(min=6) def create_python_input_window(): def menu_position(cli): @@ -415,7 +418,9 @@ def menu_position(cli): preview_search=Always(), ), # As long as we're editing, prefer a minimal height of 6. - get_height=(lambda cli: (None if cli.is_done else D(min=6))), + get_height=(lambda cli: ( + None if cli.is_done or python_input.show_exit_confirmation + else input_buffer_height)), ) return HSplit([ @@ -427,9 +432,11 @@ def menu_position(cli): Window( python_prompt_control, dont_extend_width=True, + height=D.exact(1), ), create_python_input_window(), ]), + ] + extra_body + [ ]), floats=[ Float(xcursor=True, @@ -473,12 +480,12 @@ def menu_position(cli): filter=HasSignature(python_input) & ShowDocstring(python_input) & ~IsDone(), ), ]), - ] + extra_sidebars + [ HSplit([ PythonSidebar(python_input), PythonSidebarNavigation(python_input), ]) ]), + ] + extra_toolbars + [ VSplit([ PythonToolbar(key_bindings_manager, python_input), ShowSidebarButtonInfo(python_input), diff --git a/ptpython/python_input.py b/ptpython/python_input.py index fa7e5c25..ddd09665 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -125,7 +125,8 @@ def __init__(self, _completer=None, _validator=None, _python_prompt_control=None, _lexer=None, _extra_buffers=None, _extra_buffer_processors=None, _on_start=None, - _extra_sidebars=None, + _extra_layout_body=None, _extra_toolbars=None, + _input_buffer_height=None, _accept_action=AcceptAction.RETURN_DOCUMENT, _on_exit=AbortAction.RAISE_EXCEPTION): @@ -141,7 +142,9 @@ def __init__(self, self._on_exit = _on_exit self._on_start = _on_start - self._extra_sidebars = _extra_sidebars or [] + self._input_buffer_height = _input_buffer_height + self._extra_layout_body = _extra_layout_body or [] + self._extra_toolbars = _extra_toolbars or [] self._extra_buffer_processors = _extra_buffer_processors or [] self._python_prompt_control = _python_prompt_control or PythonPrompt(self) @@ -445,8 +448,10 @@ def create_application(self): self, self.key_bindings_manager, self._python_prompt_control, lexer=self._lexer, + input_buffer_height=self._input_buffer_height, extra_buffer_processors=self._extra_buffer_processors, - extra_sidebars=self._extra_sidebars), + extra_body=self._extra_layout_body, + extra_toolbars=self._extra_toolbars), buffer=self._create_buffer(), buffers=buffers, key_bindings_registry=self.key_bindings_registry, From 6f946bffe6774ad736b309bb81141f412f3989a3 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 20 Aug 2015 03:42:14 +0200 Subject: [PATCH 054/470] Make multi-column the default completion visualisation. --- examples/ptpython_config/config.py | 2 +- ptpython/python_input.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/ptpython_config/config.py b/examples/ptpython_config/config.py index b8894464..d8173dea 100644 --- a/examples/ptpython_config/config.py +++ b/examples/ptpython_config/config.py @@ -26,7 +26,7 @@ def configure(repl): repl.show_docstring = False # Show completions. (NONE, POP_UP, MULTI_COLUMN or TOOLBAR) - repl.completion_visualisation = CompletionVisualisation.MULTI_COLUMN + repl.completion_visualisation = CompletionVisualisation.POP_UP # When CompletionVisualisation.POP_UP has been chosen, use this # scroll_offset in the completion menu. diff --git a/ptpython/python_input.py b/ptpython/python_input.py index ddd09665..36d966bc 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -152,7 +152,7 @@ def __init__(self, # Settings. self.show_signature = True self.show_docstring = False - self.completion_visualisation = CompletionVisualisation.POP_UP + self.completion_visualisation = CompletionVisualisation.MULTI_COLUMN self.completion_menu_scroll_offset = 1 self.show_line_numbers = True From 32589090025a8b9a460d4e22e9f90eaa16e588a2 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 20 Aug 2015 05:24:24 +0200 Subject: [PATCH 055/470] Fix for ptipython (compatibility with the latest prompt-toolkit.) --- ptpython/ipython.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ptpython/ipython.py b/ptpython/ipython.py index dc7b95e8..2372e70c 100644 --- a/ptpython/ipython.py +++ b/ptpython/ipython.py @@ -18,6 +18,7 @@ from prompt_toolkit.document import Document from prompt_toolkit.interface import CommandLineInterface from prompt_toolkit.layout.controls import TokenListControl +from prompt_toolkit.layout.lexers import PygmentsLexer from .python_input import PythonInput, PythonValidator, PythonCompleter from .eventloop import create_eventloop @@ -110,8 +111,8 @@ def create_lexer(): 'filename': Token.Name, }, lexers={ - 'python': PythonLexer, - 'system': BashLexer, + 'python': PygmentsLexer(PythonLexer), + 'system': PygmentsLexer(BashLexer), }) From 797e61ee54bd82730d246f660e445e4730a0041a Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 20 Aug 2015 04:50:55 +0200 Subject: [PATCH 056/470] Implementation of a custom prompts. In_tokens and out_tokens can be customized. --- examples/python-embed-with-custom-prompt.py | 51 ++++++++++++++++++ ptpython/ipython.py | 25 ++++++--- ptpython/layout.py | 10 ++-- ptpython/prompt_style.py | 59 +++++++++++++++++++++ ptpython/python_input.py | 29 +++++++--- ptpython/repl.py | 8 +-- 6 files changed, 154 insertions(+), 28 deletions(-) create mode 100755 examples/python-embed-with-custom-prompt.py create mode 100644 ptpython/prompt_style.py diff --git a/examples/python-embed-with-custom-prompt.py b/examples/python-embed-with-custom-prompt.py new file mode 100755 index 00000000..fe889fad --- /dev/null +++ b/examples/python-embed-with-custom-prompt.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python +""" +Example of embedding a Python REPL, and setting a custom prompt. +""" +from __future__ import unicode_literals + +from ptpython.repl import embed +from ptpython.prompt_style import PromptStyle +from pygments.token import Token + + +def configure(repl): + # There are several ways to override the prompt. + + # 1. Probably, the best is to add a new PromptStyle to `all_prompt_styles` + # and activate it. This way, the other styles are still selectable from + # the menu. + class CustomPrompt(PromptStyle): + def in_tokens(self, cli): + return [ + (Token.In, 'Input['), + (Token.In.Number, '%s' % repl.current_statement_index), + (Token.In, '] >>: '), + ] + + def out_tokens(self, cli): + return [ + (Token.Out, 'Result['), + (Token.Out.Number, '%s' % repl.current_statement_index), + (Token.Out, ']: '), + ] + + repl.all_prompt_styles['custom'] = CustomPrompt() + repl.prompt_style = 'custom' + + # 2. Assign a new callable to `get_input_prompt_tokens`. This will always take effect. + ## repl.get_input_prompt_tokens = lambda cli: [(Token.In, '[hello] >>> ')] + + # 3. Also replace `get_input_prompt_tokens`, but still call the original. This inserts + # a prefix. + + ## original = repl.get_input_prompt_tokens + ## repl.get_input_prompt_tokens = lambda cli: [(Token.In, '[prefix]')] + original(cli) + + +def main(): + embed(globals(), locals(), configure=configure) + + +if __name__ == '__main__': + main() diff --git a/ptpython/ipython.py b/ptpython/ipython.py index 2372e70c..f39f4d40 100644 --- a/ptpython/ipython.py +++ b/ptpython/ipython.py @@ -17,7 +17,6 @@ from prompt_toolkit.contrib.regular_languages.lexer import GrammarLexer from prompt_toolkit.document import Document from prompt_toolkit.interface import CommandLineInterface -from prompt_toolkit.layout.controls import TokenListControl from prompt_toolkit.layout.lexers import PygmentsLexer from .python_input import PythonInput, PythonValidator, PythonCompleter @@ -30,22 +29,30 @@ from pygments.lexers import PythonLexer, BashLexer from pygments.token import Token +from ptpython.prompt_style import PromptStyle __all__ = ( 'embed', ) -class IPythonPrompt(TokenListControl): +class IPythonPrompt(PromptStyle): """ - Prompt showing something like "In [1]:". + PromptStyle that uses the templates, as set by IPython. + Usually, something like "In [1]:". """ def __init__(self, prompt_manager): - def get_tokens(cli): - text = prompt_manager.render('in', color=False, just=False) - return [(Token.In, text)] + self.prompt_manager = prompt_manager - super(IPythonPrompt, self).__init__(get_tokens) + def in_tokens(self, cli): + text = self.prompt_manager.render('in', color=False, just=False) + return [(Token.In, text)] + + def out_tokens(self, cli): + # This function is currently not used by IPython. But for completeness, + # it would look like this. + text = self.prompt_manager.render('out', color=False, just=False) + return [(Token.Out, text)] class IPythonValidator(PythonValidator): @@ -153,11 +160,13 @@ def __init__(self, ipython_shell, *a, **kw): ipython_shell.alias_manager) kw['_lexer'] = create_lexer() kw['_validator'] = IPythonValidator() - kw['_python_prompt_control'] = IPythonPrompt(ipython_shell.prompt_manager) super(IPythonInput, self).__init__(*a, **kw) self.ipython_shell = ipython_shell + self.all_prompt_styles['ipython'] = IPythonPrompt(ipython_shell.prompt_manager) + self.prompt_style = 'ipython' + class InteractiveShellEmbed(_InteractiveShellEmbed): """ diff --git a/ptpython/layout.py b/ptpython/layout.py index 965db431..57a89c94 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -232,11 +232,7 @@ class PythonPrompt(TokenListControl): """ def __init__(self, python_input): def get_tokens(cli): - return [ - (Token.In, 'In ['), - (Token.In.Number, '%s' % python_input.current_statement_index), - (Token.In, ']: '), - ] + return python_input.get_input_prompt_tokens(cli) super(PythonPrompt, self).__init__(get_tokens) @@ -374,7 +370,7 @@ def get_tokens(cli): def create_layout(python_input, key_bindings_manager, - python_prompt_control=None, lexer=PythonLexer, + lexer=PythonLexer, extra_body=None, extra_toolbars=None, extra_buffer_processors=None, input_buffer_height=None): D = LayoutDimension @@ -430,7 +426,7 @@ def menu_position(cli): content=HSplit([ VSplit([ Window( - python_prompt_control, + PythonPrompt(python_input), dont_extend_width=True, height=D.exact(1), ), diff --git a/ptpython/prompt_style.py b/ptpython/prompt_style.py new file mode 100644 index 00000000..ef2f2b1d --- /dev/null +++ b/ptpython/prompt_style.py @@ -0,0 +1,59 @@ +from __future__ import unicode_literals +from abc import ABCMeta, abstractmethod +from six import with_metaclass +from pygments.token import Token + +__all__ = ( + 'PromptStyle', + 'IPythonPrompt', + 'ClassicPrompt', +) + + +class PromptStyle(with_metaclass(ABCMeta, object)): + """ + Base class for all prompts. + """ + @abstractmethod + def in_tokens(self, cli): + " Return the input tokens. " + return [] + + @abstractmethod + def out_tokens(self, cli): + " Return the output tokens. " + return [] + + +class IPythonPrompt(PromptStyle): + """ + A prompt resembling the IPython prompt. + """ + def __init__(self, python_input): + self.python_input = python_input + + def in_tokens(self, cli): + return [ + (Token.In, 'In ['), + (Token.In.Number, '%s' % self.python_input.current_statement_index), + (Token.In, ']: '), + ] + + def out_tokens(self, cli): + return [ + (Token.Out, 'Out['), + (Token.Out.Number, '%s' % self.python_input.current_statement_index), + (Token.Out, ']:'), + (Token, ' '), + ] + + +class ClassicPrompt(PromptStyle): + """ + The classic Python prompt. + """ + def in_tokens(self, cli): + return [(Token, '>>> ')] + + def out_tokens(self, cli): + return [] diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 36d966bc..eafb37bd 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -23,13 +23,14 @@ from ptpython.completer import PythonCompleter from ptpython.key_bindings import load_python_bindings, load_sidebar_bindings, load_confirm_exit_bindings -from ptpython.layout import PythonPrompt, create_layout, CompletionVisualisation +from ptpython.layout import create_layout, CompletionVisualisation from ptpython.style import get_all_code_styles, get_all_ui_styles, generate_style from ptpython.utils import get_jedi_script_from_document, document_is_multiline_python from ptpython.validator import PythonValidator +from ptpython.prompt_style import IPythonPrompt, ClassicPrompt -from pygments.lexers import PythonLexer from functools import partial +from pygments.lexers import PythonLexer import six import __future__ @@ -122,7 +123,7 @@ def __init__(self, vi_mode=False, # For internal use. - _completer=None, _validator=None, _python_prompt_control=None, + _completer=None, _validator=None, _lexer=None, _extra_buffers=None, _extra_buffer_processors=None, _on_start=None, _extra_layout_body=None, _extra_toolbars=None, @@ -147,8 +148,6 @@ def __init__(self, self._extra_toolbars = _extra_toolbars or [] self._extra_buffer_processors = _extra_buffer_processors or [] - self._python_prompt_control = _python_prompt_control or PythonPrompt(self) - # Settings. self.show_signature = True self.show_docstring = False @@ -177,6 +176,20 @@ def __init__(self, self.terminal_title = None # The title to be displayed in the terminal. (None or string.) self.exit_message = 'Do you really want to exit?' + # Tokens to be shown at the prompt. + self.prompt_style = 'ipython' # The currently active style. + + self.all_prompt_styles = { # Styles selectable from the menu. + 'ipython': IPythonPrompt(self), + 'classic': ClassicPrompt(), + } + + self.get_input_prompt_tokens = lambda cli: \ + self.all_prompt_styles[self.prompt_style].in_tokens(cli) + + self.get_output_prompt_tokens = lambda cli: \ + self.all_prompt_styles[self.prompt_style].out_tokens(cli) + #: Load styles. self.code_styles = get_all_code_styles() self.ui_styles = get_all_ui_styles() @@ -402,6 +415,10 @@ def get_values(): CompletionVisualisation.MULTI_COLUMN: lambda: enable('completion_visualisation', CompletionVisualisation.MULTI_COLUMN), CompletionVisualisation.TOOLBAR: lambda: enable('completion_visualisation', CompletionVisualisation.TOOLBAR), }), + Option(title='Prompt', + description='Visualisation of the prompt.', + get_current_value=lambda: self.prompt_style, + get_values=lambda: dict((s, partial(enable, 'prompt_style', s)) for s in self.all_prompt_styles)), simple_option(title='Show signature', description='Display function signatures.', field_name='show_signature'), @@ -446,7 +463,7 @@ def create_application(self): return Application( layout=create_layout( self, - self.key_bindings_manager, self._python_prompt_control, + self.key_bindings_manager, lexer=self._lexer, input_buffer_height=self._input_buffer_height, extra_buffer_processors=self._extra_buffer_processors, diff --git a/ptpython/repl.py b/ptpython/repl.py index 517ac78c..5f89b22b 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -11,7 +11,6 @@ from pygments.lexers import PythonTracebackLexer, PythonLexer from pygments.styles.default import DefaultStyle -from pygments.token import Token from prompt_toolkit.application import AbortAction from prompt_toolkit.interface import AcceptAction, CommandLineInterface @@ -112,12 +111,7 @@ def compile_with_flags(code, mode): locals['_'] = locals['_%i' % self.current_statement_index] = result if result is not None: - out_tokens = [ - (Token.Out, 'Out['), - (Token.Out.Number, '%s' % self.current_statement_index), - (Token.Out, ']:'), - (Token, ' '), - ] + out_tokens = self.get_output_prompt_tokens(cli) try: result_str = '%r\n' % (result, ) From d056ae36eccd241c9933281fa7b09c8c60cf9589 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 3 Sep 2015 19:00:31 +0200 Subject: [PATCH 057/470] Made an option to show/hide highlighting for matching parenthesis. --- examples/ptpython_config/config.py | 3 +++ ptpython/layout.py | 3 ++- ptpython/python_input.py | 4 ++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/examples/ptpython_config/config.py b/examples/ptpython_config/config.py index d8173dea..3aa3e48e 100644 --- a/examples/ptpython_config/config.py +++ b/examples/ptpython_config/config.py @@ -41,6 +41,9 @@ def configure(repl): # When the sidebar is visible, also show the help text. repl.show_sidebar_help = True + # Highlight matching parethesis. + repl.highlight_matching_parenthesis = True + # Complete while typing. (Don't require tab before the # completion menu is shown.) repl.complete_while_typing = True diff --git a/ptpython/layout.py b/ptpython/layout.py index 57a89c94..acb77f9c 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -403,7 +403,8 @@ def menu_position(cli): # Show matching parentheses, but only while editing. ConditionalProcessor( processor=HighlightMatchingBracketProcessor(chars='[](){}'), - filter=HasFocus(DEFAULT_BUFFER) & ~IsDone()), + filter=HasFocus(DEFAULT_BUFFER) & ~IsDone() & + Condition(lambda cli: python_input.highlight_matching_parenthesis)), ConditionalProcessor( processor=HighlightSearchProcessor(preview_search=Always()), filter=HasFocus(SEARCH_BUFFER)), diff --git a/ptpython/python_input.py b/ptpython/python_input.py index eafb37bd..0cb4d2ee 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -170,6 +170,7 @@ def __init__(self, # history on the records starting # with the current input. + self.highlight_matching_parenthesis = True self.show_sidebar = False # Currently show the sidebar. self.show_sidebar_help = True # When the sidebar is visible, also show the help text. self.show_exit_confirmation = False # Currently show 'Do you really want to exit?' @@ -434,6 +435,9 @@ def get_values(): simple_option(title='Show sidebar help', description='When the sidebar is visible, also show this help text.', field_name='show_sidebar_help'), + simple_option(title='Highlight parenthesis', + description='Highlight matching parenthesis, when the cursor is on or right after one.', + field_name='highlight_matching_parenthesis'), ]), OptionCategory('Colors', [ Option(title='Code', From cf8350904a824297d480c8c4d6be9f32e8d66121 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 3 Sep 2015 19:00:31 +0200 Subject: [PATCH 058/470] Implementation of history-selection: a tool to select lines from the history. --- ptpython/history_browser.py | 567 ++++++++++++++++++++++++++++++++++++ ptpython/key_bindings.py | 20 +- ptpython/layout.py | 15 +- ptpython/python_input.py | 4 +- ptpython/style.py | 12 + 5 files changed, 609 insertions(+), 9 deletions(-) create mode 100644 ptpython/history_browser.py diff --git a/ptpython/history_browser.py b/ptpython/history_browser.py new file mode 100644 index 00000000..185a9a17 --- /dev/null +++ b/ptpython/history_browser.py @@ -0,0 +1,567 @@ +""" +Utility to easily select lines from the history and execute them again. + +`create_history_application` creates an `Application` instance that runs will +run as a sub application of the Repl/PythonInput. +""" +from __future__ import unicode_literals + +from prompt_toolkit.application import Application +from prompt_toolkit.buffer import Buffer, AcceptAction +from prompt_toolkit.document import Document +from prompt_toolkit.enums import DEFAULT_BUFFER +from prompt_toolkit.filters import Always, Condition, HasFocus +from prompt_toolkit.key_binding.manager import KeyBindingManager +from prompt_toolkit.keys import Keys +from prompt_toolkit.layout.containers import HSplit, VSplit, Window, FloatContainer, Float, ConditionalContainer, Layout +from prompt_toolkit.layout.controls import BufferControl, FillControl +from prompt_toolkit.layout.dimension import LayoutDimension as D +from prompt_toolkit.layout.lexers import PygmentsLexer +from prompt_toolkit.layout.margins import Margin +from prompt_toolkit.layout.processors import HighlightSearchProcessor, HighlightSelectionProcessor +from prompt_toolkit.layout.screen import Char +from prompt_toolkit.layout.toolbars import ArgToolbar, SearchToolbar +from prompt_toolkit.layout.processors import Processor +from prompt_toolkit.layout.utils import explode_tokens +from prompt_toolkit.layout.toolbars import TokenListToolbar +from prompt_toolkit.utils import Callback +from pygments.lexers import PythonLexer, RstLexer +from pygments.token import Token + +from ptpython.layout import get_inputmode_tokens +from functools import partial +import six + +HISTORY_BUFFER = 'HISTORY_BUFFER' +HELP_BUFFER = 'HELP_BUFFER' + +HISTORY_COUNT = 500 + +__all__ = ( + 'create_history_application', +) + +HELP_TEXT = """ +This interface is meant to select multiple lines from the +history and execute them together. + +Typical usage +------------- + +1. Move the ``cursor up`` in the history pane, until the + first line that you want in the output is selected. +2. Hold down the ``space bar``, or press it multiple + times. Each time it will select one line and move to + the next one. Each selected line will appear on the + right side, under current input. +3. When all the required lines are displayed on the right + side, press ``Enter``. This will go back to the Python + REPL and show these lines as the current input. They + can still be edited from there. + +Key bindings +------------ + +Many Emacs and Vi navigation key bindings should work. +Press ``F4`` to switch between Emacs andVi mode. + +Additional bindings: + +- ``Space``: Select or delect a line. +- ``Tab``: Move the focus between the history and input + pane. (Alternative: ``Ctrl-W``) +- ``Ctrl-C``: Cancel. Ignore the result and go back to + the REPL. (Alternative: ``q``.) +- ``Enter``: Accept the result and go back to the REPL. +- ``F1``: Show/hide help. (Alternative ``h``.) Press + ``Enter`` to quit this help message. + +Further, remember that searching works like in Emacs +(``Ctrl-R``) or Vi (``/``). +""" + + +class BORDER: + " Box drawing characters. " + HORIZONTAL = '\u2501' + VERTICAL = '\u2503' + TOP_LEFT = '\u250f' + TOP_RIGHT = '\u2513' + BOTTOM_LEFT = '\u2517' + BOTTOM_RIGHT = '\u251b' + LIGHT_VERTICAL = '\u2502' + + +def create_popup_window(title, body): + """ + Return the layout for a pop-up window. It consists of a title bar showing + the `title` text, and a body layout. The window is surrounded by borders. + """ + assert isinstance(title, six.text_type) + assert isinstance(body, Layout) + + return HSplit([ + VSplit([ + Window(width=D.exact(1), height=D.exact(1), + content=FillControl(BORDER.TOP_LEFT, token=Token.Window.Border)), + TokenListToolbar( + get_tokens=lambda cli: [(Token.Window.Title, ' %s ' % title)], + align_center=True, + default_char=Char(BORDER.HORIZONTAL, Token.Window.Border)), + Window(width=D.exact(1), height=D.exact(1), + content=FillControl(BORDER.TOP_RIGHT, token=Token.Window.Border)), + ]), + VSplit([ + Window(width=D.exact(1), + content=FillControl(BORDER.VERTICAL, token=Token.Window.Border)), + body, + Window(width=D.exact(1), + content=FillControl(BORDER.VERTICAL, token=Token.Window.Border)), + ]), + VSplit([ + Window(width=D.exact(1), height=D.exact(1), + content=FillControl(BORDER.BOTTOM_LEFT, token=Token.Window.Border)), + Window(height=D.exact(1), + content=FillControl(BORDER.HORIZONTAL, token=Token.Window.Border)), + Window(width=D.exact(1), height=D.exact(1), + content=FillControl(BORDER.BOTTOM_RIGHT, token=Token.Window.Border)), + ]), + ]) + + +def create_layout(python_input, history_mapping): + """ + Create and return a `Layout` instance for the history application. + """ + input_processors = [ + HighlightSearchProcessor(preview_search=Always()), + HighlightSelectionProcessor()] + + help_window = create_popup_window( + title='History Help', + body=Window(content=BufferControl( + buffer_name=HELP_BUFFER, + default_char=Char(token=Token), + lexer=PygmentsLexer(RstLexer)), + scroll_offset=2)) + + return HSplit([ + # Top title bar. + TokenListToolbar( + get_tokens=_get_top_toolbar_tokens, + align_center=True, + default_char=Char(' ', Token.Sidebar.Title)), + FloatContainer( + content=VSplit([ + # Left side: history. + HSplit([ + TokenListToolbar( + get_tokens=_get_history_toolbar_tokens, + align_center=True, + default_char=Char(' ', Token.Toolbar.Status)), + Window( + content=BufferControl( + margin=HistoryMargin(history_mapping), + buffer_name=HISTORY_BUFFER, + lexer=PygmentsLexer(PythonLexer), + input_processors=input_processors), + scroll_offset=2), + ]), + # Separator. + Window(width=D.exact(1), + content=FillControl(BORDER.LIGHT_VERTICAL, token=Token.Separator)), + # Right side: result. + HSplit([ + TokenListToolbar( + get_tokens=_get_result_toolbar_tokens, + align_center=True, + default_char=Char(' ', Token.Toolbar.Status)), + Window( + content=BufferControl( + margin=ResultMargin(history_mapping), + buffer_name=DEFAULT_BUFFER, + input_processors=input_processors + [GrayExistingText(history_mapping)], + lexer=PygmentsLexer(PythonLexer)), + scroll_offset=2) + ]) + ]), + # Help text as a float. + floats=[Float(width=60, top=3, bottom=2, + content=ConditionalContainer( + content=help_window, filter=HasFocus(HELP_BUFFER)))] + ), + # Bottom toolbars. + ArgToolbar(), + SearchToolbar(), + TokenListToolbar( + get_tokens=partial(_get_bottom_toolbar_tokens, python_input=python_input), + default_char=Char(' ', Token.Toolbar.Status)), + ]) + + +def _get_top_toolbar_tokens(cli): + return [(Token.Sidebar.Title, 'History browser - Insert from history')] + + +def _get_history_toolbar_tokens(cli): + return [(Token.Toolbar.Status, ' History (latest %i entries)' % HISTORY_COUNT)] + + +def _get_result_toolbar_tokens(cli): + return [(Token.Toolbar.Status, ' Current input')] + + +def _get_bottom_toolbar_tokens(cli, python_input): + return [ + (Token.Toolbar.Status, ' ') + ] + get_inputmode_tokens(cli, python_input) + [ + (Token.Toolbar.Status, ' '), + (Token.Toolbar.Status.Key, '[F1]'), + (Token.Toolbar.Status.On, ' Help '), + (Token.Toolbar.Status.Key, '[Space]'), + (Token.Toolbar.Status.On, ' Toggle '), + (Token.Toolbar.Status.Key, '[Tab]'), + (Token.Toolbar.Status.On, ' Focus '), + (Token.Toolbar.Status.Key, '[Enter]'), + (Token.Toolbar.Status.On, ' Accept '), + (Token.Toolbar.Status.Key, '[Ctrl-C]'), + (Token.Toolbar.Status.On, ' Cancel '), + ] + + +class HistoryMargin(Margin): + """ + Margin for the history buffer. + This displays a green bar for the selected entries. + """ + def __init__(self, history_mapping): + self.history_mapping = history_mapping + + def create_handler(self, cli, document): + lines_starting_new_entries = self.history_mapping.lines_starting_new_entries + selected_lines = self.history_mapping.selected_lines + + current_lineno = document.cursor_position_row + + def margin(line_number): + # Show stars at the start of each entry. + # (Visualises multiline entries.) + if line_number in lines_starting_new_entries: + char = '*' + else: + char = ' ' + + if line_number in selected_lines: + t = Token.History.Line.Selected + else: + t = Token.History.Line + + if line_number == current_lineno: + t = t.Current + + return [(t, char), (Token, ' ')] + return margin + + def invalidation_hash(self, cli, document): + return ( + frozenset(self.history_mapping.selected_lines), + document.cursor_position_row, + ) + + +class ResultMargin(Margin): + """ + The margin to be shown in the result pane. + """ + def __init__(self, history_mapping): + self.history_mapping = history_mapping + + def create_handler(self, cli, document): + current_lineno = document.cursor_position_row + offset = self.history_mapping.result_line_offset #original_document.cursor_position_row + + def margin(line_number): + if (line_number is None or line_number < offset or + line_number >= offset + len(self.history_mapping.selected_lines)): + t = Token + elif line_number == current_lineno: + t = Token.History.Line.Selected.Current + else: + t = Token.History.Line.Selected + + return [(t, ' '), (Token, ' ')] + return margin + + def invalidation_hash(self, cli, document): + return document.cursor_position_row + + +class GrayExistingText(Processor): + """ + Turn the existing input, before and after the inserted code gray. + """ + def __init__(self, history_mapping): + self.history_mapping = history_mapping + self._len_before = len(history_mapping.original_document.text_before_cursor) + self._len_after = len(history_mapping.original_document.text_after_cursor) + + def run(self, cli, document, tokens): + if self._len_before or self._len_after: + tokens = explode_tokens(tokens) + pos_after = len(tokens) - self._len_after + + text_before = ''.join(t[1] for t in tokens[:self._len_before]) + text_after = ''.join(t[1] for t in tokens[pos_after:]) + + return ( + [(Token.History.ExistingInput, text_before)] + + tokens[self._len_before:pos_after] + + [(Token.History.ExistingInput, text_after)] + ), lambda i: i + else: + return tokens, lambda i: i + + +class HistoryMapping(object): + """ + Keep a list of all the lines from the history and the selected lines. + """ + def __init__(self, python_history, original_document): + self.python_history = python_history + self.original_document = original_document + + self.lines_starting_new_entries = set() + self.selected_lines = set() + + # Process history. + history_lines = [] + + for entry_nr, entry in list(enumerate(python_history))[-HISTORY_COUNT:]: + self.lines_starting_new_entries.add(len(history_lines)) + + for line in entry.splitlines(): + history_lines.append(line) + + self.history_lines = history_lines + self.concatenated_history = '\n'.join(history_lines) + + # Line offset. + if self.original_document.text_before_cursor: + self.result_line_offset = self.original_document.cursor_position_row + 1 + else: + self.result_line_offset = 0 + + def get_new_document(self, cursor_pos=None): + """ + Create a `Document` instance that contains the resulting text. + """ + lines = [] + + # Original text, before cursor. + if self.original_document.text_before_cursor: + lines.append(self.original_document.text_before_cursor) + + # Selected entries from the history. + for line_no in sorted(self.selected_lines): + lines.append(self.history_lines[line_no]) + + # Original text, after cursor. + if self.original_document.text_after_cursor: + lines.append(self.original_document.text_after_cursor) + + # Create `Document` with cursor at the right position. + text = '\n'.join(lines) + if cursor_pos is not None and cursor_pos > len(text): + cursor_pos = len(text) + return Document(text, cursor_pos) + + def update_default_buffer(self, cli): + b = cli.buffers[DEFAULT_BUFFER] + + b.set_document( + self.get_new_document(b.cursor_position), bypass_readonly=True) + + +def create_key_bindings(python_input, history_mapping): + """ + Key bindings. + """ + manager = KeyBindingManager( + enable_vi_mode=Condition(lambda cli: python_input.vi_mode), + enable_extra_page_navigation=True, + vi_state=python_input.key_bindings_manager.vi_state) + handle = manager.registry.add_binding + + @handle(' ', filter=HasFocus(HISTORY_BUFFER)) + def _(event): + """ + Space: select/deselect line from history pane. + """ + b = event.current_buffer + line_no = b.document.cursor_position_row + + if line_no in history_mapping.selected_lines: + # Remove line. + history_mapping.selected_lines.remove(line_no) + history_mapping.update_default_buffer(event.cli) + else: + # Add line. + history_mapping.selected_lines.add(line_no) + history_mapping.update_default_buffer(event.cli) + + # Update cursor position + default_buffer = event.cli.buffers[DEFAULT_BUFFER] + default_lineno = sorted(history_mapping.selected_lines).index(line_no) + \ + history_mapping.result_line_offset + default_buffer.cursor_position = \ + default_buffer.document.translate_row_col_to_index(default_lineno, 0) + + # Also move the cursor to the next line. (This way they can hold + # space to select a region.) + b.cursor_position = b.document.translate_row_col_to_index(line_no + 1, 0) + + @handle(' ', filter=HasFocus(DEFAULT_BUFFER)) + @handle(Keys.Delete, filter=HasFocus(DEFAULT_BUFFER)) + @handle(Keys.ControlH, filter=HasFocus(DEFAULT_BUFFER)) + def _(event): + """ + Space: remove line from default pane. + """ + b = event.current_buffer + line_no = b.document.cursor_position_row - history_mapping.result_line_offset + + if line_no >= 0: + try: + history_lineno = sorted(history_mapping.selected_lines)[line_no] + except IndexError: + pass # When `selected_lines` is an empty set. + else: + history_mapping.selected_lines.remove(history_lineno) + + history_mapping.update_default_buffer(event.cli) + + help_focussed = HasFocus(HELP_BUFFER) + main_buffer_focussed = HasFocus(HISTORY_BUFFER) | HasFocus(DEFAULT_BUFFER) + + @handle(Keys.Tab, filter=main_buffer_focussed) + @handle(Keys.ControlX, filter=main_buffer_focussed, eager=True) + # Eager: ignore the Emacs [Ctrl-X Ctrl-X] binding. + @handle(Keys.ControlW, filter=main_buffer_focussed) + def _(event): + " Select other window. " + if event.cli.current_buffer_name == HISTORY_BUFFER: + event.cli.focus_stack.replace(DEFAULT_BUFFER) + + elif event.cli.current_buffer_name == DEFAULT_BUFFER: + event.cli.focus_stack.replace(HISTORY_BUFFER) + + @handle(Keys.F4) + def _(event): + " Switch between Emacs/Vi mode. " + python_input.vi_mode = not python_input.vi_mode + + @handle(Keys.F1) + def _(event): + " Display/hide help. " + if event.cli.focus_stack.current == HELP_BUFFER: + event.cli.focus_stack.pop() + else: + event.cli.focus_stack.push(HELP_BUFFER) + + @handle(Keys.ControlJ, filter=help_focussed) + @handle(Keys.ControlC, filter=help_focussed) + @handle(Keys.Escape, filter=help_focussed) + def _(event): + " Leave help. " + event.cli.focus_stack.pop() + + @handle('q', filter=main_buffer_focussed) + @handle(Keys.F3, filter=main_buffer_focussed) + @handle(Keys.ControlC, filter=main_buffer_focussed) + @handle(Keys.ControlG, filter=main_buffer_focussed) + def _(event): + " Cancel and go back. " + event.cli.set_return_value(None) + + enable_system_bindings = Condition(lambda cli: python_input.enable_system_bindings) + + @handle(Keys.ControlZ, filter=enable_system_bindings) + def _(event): + " Suspend to background. " + event.cli.suspend_to_background() + + return manager.registry + + +def create_history_application(python_input, original_document): + """ + Create an `Application` for the history screen. + This has to be run as a sub application of `python_input`. + + When this application runs and returns, it retuns the selected lines. + """ + history_mapping = HistoryMapping(python_input.history, original_document) + + def default_buffer_pos_changed(): + """ When the cursor changes in the default buffer. Synchronize with + history buffer. """ + # Only when this buffer has the focus. + if application.focus_stack.current == DEFAULT_BUFFER: + try: + line_no = default_buffer.document.cursor_position_row - \ + history_mapping.result_line_offset + + if line_no < 0: # When the cursor is above the inserted region. + raise IndexError + + history_lineno = sorted(history_mapping.selected_lines)[line_no] + except IndexError: + pass + else: + history_buffer.cursor_position = \ + history_buffer.document.translate_row_col_to_index(history_lineno, 0) + + def history_buffer_pos_changed(): + """ When the cursor changes in the history buffer. Synchronize. """ + # Only when this buffer has the focus. + if application.focus_stack.current == HISTORY_BUFFER: + line_no = history_buffer.document.cursor_position_row + + if line_no in history_mapping.selected_lines: + default_lineno = sorted(history_mapping.selected_lines).index(line_no) + \ + history_mapping.result_line_offset + + default_buffer.cursor_position = \ + default_buffer.document.translate_row_col_to_index(default_lineno, 0) + + history_buffer = Buffer( + initial_document=Document(history_mapping.concatenated_history), + on_cursor_position_changed=Callback(history_buffer_pos_changed), + accept_action=AcceptAction( + lambda cli, buffer: cli.set_return_value(default_buffer.document)), + read_only=True) + + default_buffer = Buffer( + initial_document=history_mapping.get_new_document(), + on_cursor_position_changed=Callback(default_buffer_pos_changed), + read_only=True) + + help_buffer = Buffer( + initial_document=Document(HELP_TEXT, 0), + accept_action=AcceptAction.IGNORE, + read_only=True + ) + + application = Application( + layout=create_layout(python_input, history_mapping), + use_alternate_screen=True, + buffers={ + HISTORY_BUFFER: history_buffer, + DEFAULT_BUFFER: default_buffer, + HELP_BUFFER: help_buffer, + }, + initial_focussed_buffer=HISTORY_BUFFER, + style=python_input._current_style, + key_bindings_registry=create_key_bindings(python_input, history_mapping) + ) + return application diff --git a/ptpython/key_bindings.py b/ptpython/key_bindings.py index 35390e91..38bf1dec 100644 --- a/ptpython/key_bindings.py +++ b/ptpython/key_bindings.py @@ -7,6 +7,8 @@ from prompt_toolkit.key_binding.vi_state import InputMode from prompt_toolkit.keys import Keys +from .history_browser import create_history_application + __all__ = ( 'load_python_bindings', 'load_sidebar_bindings', @@ -53,6 +55,22 @@ def _(event): """ python_input.show_sidebar = not python_input.show_sidebar + @handle(Keys.F3) + def _(event): + """ + Select from the history. + """ + python_input.key_bindings_manager.vi_state.input_mode = InputMode.NAVIGATION + + def done(result): + if result is not None: + event.cli.buffers[DEFAULT_BUFFER].document = result + + python_input.key_bindings_manager.vi_state.input_mode = InputMode.INSERT + + event.cli.run_sub_application(create_history_application( + python_input, event.cli.buffers[DEFAULT_BUFFER].document), done) + @handle(Keys.F4) def _(event): """ @@ -77,7 +95,7 @@ def _(event): @handle(Keys.ControlJ, filter= ~sidebar_visible & ~has_selection & ~(vi_mode_enabled & ViStateFilter(key_bindings_manager.vi_state, InputMode.NAVIGATION)) & - HasFocus('default') & IsMultiline()) + HasFocus(DEFAULT_BUFFER) & IsMultiline()) def _(event): """ Behaviour of the Enter key. diff --git a/ptpython/layout.py b/ptpython/layout.py index acb77f9c..0a55e771 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -238,16 +238,17 @@ def get_tokens(cli): class PythonToolbar(TokenListToolbar): - def __init__(self, key_bindings_manager, python_input, token=Token.Toolbar.Status): + def __init__(self, key_bindings_manager, python_input): + TB = Token.Toolbar.Status + def get_tokens(cli): python_buffer = cli.buffers[DEFAULT_BUFFER] - TB = token result = [] append = result.append append((TB, ' ')) - result.extend(get_inputmode_tokens(TB, key_bindings_manager, python_input, cli)) + result.extend(get_inputmode_tokens(cli, python_input)) append((TB, ' ')) # Position in history. @@ -275,20 +276,22 @@ def get_tokens(cli): super(PythonToolbar, self).__init__( get_tokens, - default_char=Char(token=token), + default_char=Char(token=TB), filter=~IsDone() & RendererHeightIsKnown() & Condition(lambda cli: python_input.show_status_bar and not python_input.show_exit_confirmation)) -def get_inputmode_tokens(token, key_bindings_manager, python_input, cli): +def get_inputmode_tokens(cli, python_input): """ Return current input mode as a list of (token, text) tuples for use in a toolbar. :param cli: `CommandLineInterface` instance. """ - mode = key_bindings_manager.vi_state.input_mode + token = Token.Toolbar.Status + + mode = python_input.key_bindings_manager.vi_state.input_mode result = [] append = result.append diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 0cb4d2ee..42813daf 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -136,7 +136,7 @@ def __init__(self, self._completer = _completer or PythonCompleter(self.get_globals, self.get_locals) self._validator = _validator or PythonValidator(self.get_compiler_flags) - self._history = FileHistory(history_filename) if history_filename else InMemoryHistory() + self.history = FileHistory(history_filename) if history_filename else InMemoryHistory() self._lexer = _lexer or PygmentsLexer(PythonLexer) self._extra_buffers = _extra_buffers self._accept_action = _accept_action @@ -497,7 +497,7 @@ def is_buffer_multiline(): complete_while_typing=Condition(lambda: self.complete_while_typing), enable_history_search=Condition(lambda: self.enable_history_search), tempfile_suffix='.py', - history=self._history, + history=self.history, completer=self._completer, validator=SwitchableValidator( self._validator, diff --git a/ptpython/style.py b/ptpython/style.py index fec8e676..b795b86f 100644 --- a/ptpython/style.py +++ b/ptpython/style.py @@ -111,6 +111,7 @@ class PythonStyle(Style): # Status toolbar. Token.Toolbar.Status: 'bg:#222222 #aaaaaa', Token.Toolbar.Status.InputMode: 'bg:#222222 #ffffaa', + Token.Toolbar.Status.Key: 'bg:#bbbbbb #000000', Token.Toolbar.Status.Off: 'bg:#222222 #aaaaaa', Token.Toolbar.Status.On: 'bg:#222222 #ffffff', Token.Toolbar.Status.PythonVersion: 'bg:#222222 #ffffff bold', @@ -131,6 +132,17 @@ class PythonStyle(Style): Token.Sidebar.Key.Description: 'bg:#bbbbbb #000000', Token.Sidebar.HelpText: 'bg:#fdf6e3 #000011', + # Styling for the history layout. + Token.History.Line: '', + Token.History.Line.Selected: 'bg:#008800 #000000', + Token.History.Line.Current: 'bg:#ffffff #000000', + Token.History.Line.Selected.Current: 'bg:#88ff88 #000000', + Token.History.ExistingInput: '#888888', + + # Help Window. + Token.Window.Border: '#bbbbbb', + Token.Window.Title: 'bg:#bbbbbb #000000', + # Exit confirmation. Token.ExitConfirmation: 'bg:#884444 #ffffff', } From 7109100c8811870866d470af5d3fb94949cb0de2 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sun, 6 Sep 2015 02:25:51 +0200 Subject: [PATCH 059/470] Show history shortcut in the toolbar. Moved [Meta+Enter] information up, and make in configurable. --- examples/ptpython_config/config.py | 4 ++++ ptpython/layout.py | 24 +++++++++++++++++++++--- ptpython/python_input.py | 5 +++++ ptpython/style.py | 3 +++ 4 files changed, 33 insertions(+), 3 deletions(-) diff --git a/examples/ptpython_config/config.py b/examples/ptpython_config/config.py index 3aa3e48e..6d64608b 100644 --- a/examples/ptpython_config/config.py +++ b/examples/ptpython_config/config.py @@ -25,6 +25,10 @@ def configure(repl): # Show docstring (bool). repl.show_docstring = False + # Show the "[Meta+Enter] Execute" message when pressing [Enter] only + # inserts a newline instead of executing the code. + repl.show_meta_enter_message = True + # Show completions. (NONE, POP_UP, MULTI_COLUMN or TOOLBAR) repl.completion_visualisation = CompletionVisualisation.POP_UP diff --git a/ptpython/layout.py b/ptpython/layout.py index 0a55e771..8aa02095 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -264,14 +264,13 @@ def get_tokens(cli): else: append((TB, ' ')) + append((TB.On, '[F3] History ')) + if python_input.paste_mode: append((TB.On, '[F6] Paste mode (on) ')) else: append((TB.Off, '[F6] Paste mode (off) ')) - if python_buffer.is_multiline(): - append((TB, ' [Meta+Enter] Execute')) - return result super(PythonToolbar, self).__init__( @@ -372,6 +371,23 @@ def get_tokens(cli): filter=visible) +def meta_enter_message(python_input): + """ + Create the `Layout` for the 'Meta+Enter` message. + """ + def get_tokens(cli): + return [(Token.MetaEnterMessage, ' [Meta+Enter] Execute ')] + + visible = ~IsDone() & HasFocus(DEFAULT_BUFFER) & Condition( + lambda cli: + python_input.show_meta_enter_message and + cli.buffers[DEFAULT_BUFFER].is_multiline()) + + return ConditionalContainer( + content=Window(TokenListControl(get_tokens)), + filter=visible) + + def create_layout(python_input, key_bindings_manager, lexer=PythonLexer, extra_body=None, extra_toolbars=None, @@ -456,6 +472,8 @@ def menu_position(cli): Float(left=2, bottom=1, content=ExitConfirmation(python_input)), + Float(bottom=0, right=0, height=1, + content=meta_enter_message(python_input)), Float(bottom=1, left=1, right=0, content=PythonSidebarHelp(python_input)), ]), ArgToolbar(), diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 42813daf..1d515d48 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -151,6 +151,7 @@ def __init__(self, # Settings. self.show_signature = True self.show_docstring = False + self.show_meta_enter_message = True self.completion_visualisation = CompletionVisualisation.MULTI_COLUMN self.completion_menu_scroll_offset = 1 @@ -429,6 +430,10 @@ def get_values(): simple_option(title='Show line numbers', description='Show line numbers when the input consists of multiple lines.', field_name='show_line_numbers'), + simple_option(title='Show Meta+Enter message', + description='Show the [Meta+Enter] message when this key combination is required to execute commands. ' + + '(This is the case when a simple [Enter] key press will insert a newline.', + field_name='show_meta_enter_message'), simple_option(title='Show status bar', description='Show the status bar at the bottom of the terminal.', field_name='show_status_bar'), diff --git a/ptpython/style.py b/ptpython/style.py index b795b86f..855b8f08 100644 --- a/ptpython/style.py +++ b/ptpython/style.py @@ -143,6 +143,9 @@ class PythonStyle(Style): Token.Window.Border: '#bbbbbb', Token.Window.Title: 'bg:#bbbbbb #000000', + # Meta-enter message. + Token.MetaEnterMessage: 'bg:#ffff88 #444444', + # Exit confirmation. Token.ExitConfirmation: 'bg:#884444 #ffffff', } From 1e61eba42f36bb29d12ec32349a587add524cbf7 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sun, 6 Sep 2015 02:38:21 +0200 Subject: [PATCH 060/470] Simplified the layout code a lot. No more inheritance. --- ptpython/layout.py | 483 +++++++++++++++++++++++---------------------- 1 file changed, 244 insertions(+), 239 deletions(-) diff --git a/ptpython/layout.py b/ptpython/layout.py index 8aa02095..71ce2ea2 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -1,3 +1,6 @@ +""" +Creation of the `Layout` instance for the Python input/REPL. +""" from __future__ import unicode_literals from prompt_toolkit.enums import DEFAULT_BUFFER, SEARCH_BUFFER @@ -50,235 +53,236 @@ def show_multi_column_completions_menu(python_input): return Condition(lambda cli: python_input.completion_visualisation == CompletionVisualisation.MULTI_COLUMN) -class PythonSidebar(ConditionalContainer): +def python_sidebar(python_input): """ - Sidebar containing the configurable options. + Create the `Layout` for the sidebar with the configurable options. """ - def __init__(self, python_input): - def get_tokens(cli): - tokens = [] - T = Token.Sidebar + def get_tokens(cli): + tokens = [] + T = Token.Sidebar - def append_category(category): - tokens.extend([ - (T, ' '), - (T.Title, ' %-36s' % category.title), - (T, '\n'), - ]) + def append_category(category): + tokens.extend([ + (T, ' '), + (T.Title, ' %-36s' % category.title), + (T, '\n'), + ]) - def append(selected, label, status): - token = T.Selected if selected else T + def append(selected, label, status): + token = T.Selected if selected else T - tokens.append((T, ' >' if selected else ' ')) - tokens.append((token.Label, '%-24s' % label)) - tokens.append((token.Status, ' ')) - tokens.append((token.Status, '%s' % status)) + tokens.append((T, ' >' if selected else ' ')) + tokens.append((token.Label, '%-24s' % label)) + tokens.append((token.Status, ' ')) + tokens.append((token.Status, '%s' % status)) - if selected: - tokens.append((Token.SetCursorPosition, '')) + if selected: + tokens.append((Token.SetCursorPosition, '')) - tokens.append((token.Status, ' ' * (14 - len(status)))) - tokens.append((T, '<' if selected else '')) - tokens.append((T, '\n')) + tokens.append((token.Status, ' ' * (14 - len(status)))) + tokens.append((T, '<' if selected else '')) + tokens.append((T, '\n')) - i = 0 - for category in python_input.options: - append_category(category) + i = 0 + for category in python_input.options: + append_category(category) - for option in category.options: - append(i == python_input.selected_option_index, - option.title, '%s' % option.get_current_value()) - i += 1 + for option in category.options: + append(i == python_input.selected_option_index, + option.title, '%s' % option.get_current_value()) + i += 1 - tokens.pop() # Remove last newline. + tokens.pop() # Remove last newline. - return tokens + return tokens - super(PythonSidebar, self).__init__( - content=Window( - TokenListControl(get_tokens, Char(token=Token.Sidebar), - has_focus=ShowSidebar(python_input) & ~IsDone()), - width=LayoutDimension.exact(43), - height=LayoutDimension(min=3), - scroll_offset=1), - filter=ShowSidebar(python_input) & ~IsDone()) + return ConditionalContainer( + content=Window( + TokenListControl(get_tokens, Char(token=Token.Sidebar), + has_focus=ShowSidebar(python_input) & ~IsDone()), + width=LayoutDimension.exact(43), + height=LayoutDimension(min=3), + scroll_offset=1), + filter=ShowSidebar(python_input) & ~IsDone()) -class PythonSidebarNavigation(ConditionalContainer): +def python_sidebar_navigation(python_input): """ - Showing the navigation information for the sidebar. + Create the `Layout` showing the navigation information for the sidebar. """ - def __init__(self, python_input): - def get_tokens(cli): - tokens = [] - T = Token.Sidebar - - # Show navigation info. - tokens.extend([ - (T.Separator , ' ' * 43 + '\n'), - (T, ' '), - (T.Key, '[Arrows]'), - (T, ' '), - (T.Key.Description, 'Navigate'), - (T, ' '), - (T.Key, '[Enter]'), - (T, ' '), - (T.Key.Description, 'Hide menu'), - ]) + def get_tokens(cli): + tokens = [] + T = Token.Sidebar + + # Show navigation info. + tokens.extend([ + (T.Separator , ' ' * 43 + '\n'), + (T, ' '), + (T.Key, '[Arrows]'), + (T, ' '), + (T.Key.Description, 'Navigate'), + (T, ' '), + (T.Key, '[Enter]'), + (T, ' '), + (T.Key.Description, 'Hide menu'), + ]) - return tokens + return tokens - super(PythonSidebarNavigation, self).__init__( - content=Window( - TokenListControl(get_tokens, Char(token=Token.Sidebar)), - width=LayoutDimension.exact(43), - height=LayoutDimension.exact(2)), - filter=ShowSidebar(python_input) & ~IsDone()) + return ConditionalContainer( + content=Window( + TokenListControl(get_tokens, Char(token=Token.Sidebar)), + width=LayoutDimension.exact(43), + height=LayoutDimension.exact(2)), + filter=ShowSidebar(python_input) & ~IsDone()) -class PythonSidebarHelp(ConditionalContainer): +def python_sidebar_help(python_input): """ - Help text for the current item in the sidebar. + Create the `Layout` for the help text for the current item in the sidebar. """ - def __init__(self, python_input): - token = Token.Sidebar.HelpText + token = Token.Sidebar.HelpText + + def get_current_description(): + """ + Return the description of the selected option. + """ + i = 0 + for category in python_input.options: + for option in category.options: + if i == python_input.selected_option_index: + return option.description + i += 1 + return '' - def get_current_description(): - """ - Return the description of the selected option. - """ - i = 0 - for category in python_input.options: - for option in category.options: - if i == python_input.selected_option_index: - return option.description - i += 1 - return '' - - def get_tokens(cli): - return [(token, get_current_description())] - - super(PythonSidebarHelp, self).__init__( - content=Window( - TokenListControl(get_tokens, Char(token=token)), - height=LayoutDimension(min=3)), - filter=ShowSidebar(python_input) & - Condition(lambda cli: python_input.show_sidebar_help) & ~IsDone()) - - -class SignatureToolbar(ConditionalContainer): - def __init__(self, python_input): - def get_tokens(cli): - result = [] - append = result.append - Signature = Token.Toolbar.Signature - - if python_input.signatures: - sig = python_input.signatures[0] # Always take the first one. - - append((Signature, ' ')) - try: - append((Signature, sig.full_name)) - except IndexError: - # Workaround for #37: https://github.com/jonathanslenders/python-prompt-toolkit/issues/37 - # See also: https://github.com/davidhalter/jedi/issues/490 - return [] - - append((Signature.Operator, '(')) - - for i, p in enumerate(sig.params): - # Workaround for #47: 'p' is None when we hit the '*' in the signature. - # and sig has no 'index' attribute. - # See: https://github.com/jonathanslenders/ptpython/issues/47 - # https://github.com/davidhalter/jedi/issues/598 - description = (p.description if p else '*') #or '*' - sig_index = getattr(sig, 'index', 0) - - if i == sig_index: - # Note: we use `_Param.description` instead of - # `_Param.name`, that way we also get the '*' before args. - append((Signature.CurrentName, str(description))) - else: - append((Signature, str(description))) - append((Signature.Operator, ', ')) - - if sig.params: - # Pop last comma - result.pop() - - append((Signature.Operator, ')')) - append((Signature, ' ')) - return result - - super(SignatureToolbar, self).__init__( - content=Window( - TokenListControl(get_tokens), - height=LayoutDimension.exact(1)), - filter= - # Show only when there is a signature - HasSignature(python_input) & - # And there are no completions to be shown. (would cover signature pop-up.) - ~(HasCompletions() & (show_completions_menu(python_input) | - show_multi_column_completions_menu(python_input))) - # Signature needs to be shown. - & ShowSignature(python_input) & - # Not done yet. - ~IsDone()) - - -class PythonPrompt(TokenListControl): + def get_tokens(cli): + return [(token, get_current_description())] + + return ConditionalContainer( + content=Window( + TokenListControl(get_tokens, Char(token=token)), + height=LayoutDimension(min=3)), + filter=ShowSidebar(python_input) & + Condition(lambda cli: python_input.show_sidebar_help) & ~IsDone()) + + +def signature_toolbar(python_input): """ - Prompt showing something like "In [1]:". + Return the `Layout` for the signature. """ - def __init__(self, python_input): - def get_tokens(cli): - return python_input.get_input_prompt_tokens(cli) + def get_tokens(cli): + result = [] + append = result.append + Signature = Token.Toolbar.Signature + + if python_input.signatures: + sig = python_input.signatures[0] # Always take the first one. + + append((Signature, ' ')) + try: + append((Signature, sig.full_name)) + except IndexError: + # Workaround for #37: https://github.com/jonathanslenders/python-prompt-toolkit/issues/37 + # See also: https://github.com/davidhalter/jedi/issues/490 + return [] + + append((Signature.Operator, '(')) + + for i, p in enumerate(sig.params): + # Workaround for #47: 'p' is None when we hit the '*' in the signature. + # and sig has no 'index' attribute. + # See: https://github.com/jonathanslenders/ptpython/issues/47 + # https://github.com/davidhalter/jedi/issues/598 + description = (p.description if p else '*') #or '*' + sig_index = getattr(sig, 'index', 0) + + if i == sig_index: + # Note: we use `_Param.description` instead of + # `_Param.name`, that way we also get the '*' before args. + append((Signature.CurrentName, str(description))) + else: + append((Signature, str(description))) + append((Signature.Operator, ', ')) + + if sig.params: + # Pop last comma + result.pop() - super(PythonPrompt, self).__init__(get_tokens) + append((Signature.Operator, ')')) + append((Signature, ' ')) + return result + return ConditionalContainer( + content=Window( + TokenListControl(get_tokens), + height=LayoutDimension.exact(1)), + filter= + # Show only when there is a signature + HasSignature(python_input) & + # And there are no completions to be shown. (would cover signature pop-up.) + ~(HasCompletions() & (show_completions_menu(python_input) | + show_multi_column_completions_menu(python_input))) + # Signature needs to be shown. + & ShowSignature(python_input) & + # Not done yet. + ~IsDone()) + + +def python_prompt(python_input): + """ + Create layout for the prompt. + It shows something like "In [1]:". + """ + def get_tokens(cli): + return python_input.get_input_prompt_tokens(cli) -class PythonToolbar(TokenListToolbar): - def __init__(self, key_bindings_manager, python_input): - TB = Token.Toolbar.Status + return TokenListControl(get_tokens) - def get_tokens(cli): - python_buffer = cli.buffers[DEFAULT_BUFFER] - result = [] - append = result.append +def status_bar(key_bindings_manager, python_input): + """ + Create the `Layout` for the status bar. + """ + TB = Token.Toolbar.Status - append((TB, ' ')) - result.extend(get_inputmode_tokens(cli, python_input)) + def get_tokens(cli): + python_buffer = cli.buffers[DEFAULT_BUFFER] + + result = [] + append = result.append + + append((TB, ' ')) + result.extend(get_inputmode_tokens(cli, python_input)) + append((TB, ' ')) + + # Position in history. + append((TB, '%i/%i ' % (python_buffer.working_index + 1, + len(python_buffer._working_lines)))) + + # Shortcuts. + if not python_input.vi_mode and cli.focus_stack.current == 'search': + append((TB, '[Ctrl-G] Cancel search [Enter] Go to this position.')) + elif bool(cli.current_buffer.selection_state) and not python_input.vi_mode: + # Emacs cut/copy keys. + append((TB, '[Ctrl-W] Cut [Meta-W] Copy [Ctrl-Y] Paste [Ctrl-G] Cancel')) + else: append((TB, ' ')) - # Position in history. - append((TB, '%i/%i ' % (python_buffer.working_index + 1, - len(python_buffer._working_lines)))) + append((TB.On, '[F3] History ')) - # Shortcuts. - if not python_input.vi_mode and cli.focus_stack.current == 'search': - append((TB, '[Ctrl-G] Cancel search [Enter] Go to this position.')) - elif bool(cli.current_buffer.selection_state) and not python_input.vi_mode: - # Emacs cut/copy keys. - append((TB, '[Ctrl-W] Cut [Meta-W] Copy [Ctrl-Y] Paste [Ctrl-G] Cancel')) + if python_input.paste_mode: + append((TB.On, '[F6] Paste mode (on) ')) else: - append((TB, ' ')) - - append((TB.On, '[F3] History ')) - - if python_input.paste_mode: - append((TB.On, '[F6] Paste mode (on) ')) - else: - append((TB.Off, '[F6] Paste mode (off) ')) + append((TB.Off, '[F6] Paste mode (off) ')) - return result + return result - super(PythonToolbar, self).__init__( - get_tokens, - default_char=Char(token=TB), - filter=~IsDone() & RendererHeightIsKnown() & - Condition(lambda cli: python_input.show_status_bar and - not python_input.show_exit_confirmation)) + return TokenListToolbar( + get_tokens, + default_char=Char(token=TB), + filter=~IsDone() & RendererHeightIsKnown() & + Condition(lambda cli: python_input.show_status_bar and + not python_input.show_exit_confirmation)) def get_inputmode_tokens(cli, python_input): @@ -321,54 +325,55 @@ def get_inputmode_tokens(cli, python_input): return result -class ShowSidebarButtonInfo(ConditionalContainer): - def __init__(self, python_input): - token = Token.Toolbar.Status +def show_sidebar_button_info(python_input): + """ + Create `Layout` for the information in the right-bottom corner. + (The right part of the status bar.) + """ + token = Token.Toolbar.Status - version = sys.version_info - tokens = [ - (token, ' [F2] Options'), - (token, ' - '), - (token.PythonVersion, '%s %i.%i.%i' % (platform.python_implementation(), - version[0], version[1], version[2])), - (token, ' '), - ] - width = token_list_width(tokens) + version = sys.version_info + tokens = [ + (token, ' [F2] Options'), + (token, ' - '), + (token.PythonVersion, '%s %i.%i.%i' % (platform.python_implementation(), + version[0], version[1], version[2])), + (token, ' '), + ] + width = token_list_width(tokens) - def get_tokens(cli): - # Python version - return tokens + def get_tokens(cli): + # Python version + return tokens - super(ShowSidebarButtonInfo, self).__init__( - content=Window( - TokenListControl(get_tokens, default_char=Char(token=token)), - height=LayoutDimension.exact(1), - width=LayoutDimension.exact(width)), - filter=~IsDone() & RendererHeightIsKnown() & - Condition(lambda cli: python_input.show_status_bar and - not python_input.show_exit_confirmation)) + return ConditionalContainer( + content=Window( + TokenListControl(get_tokens, default_char=Char(token=token)), + height=LayoutDimension.exact(1), + width=LayoutDimension.exact(width)), + filter=~IsDone() & RendererHeightIsKnown() & + Condition(lambda cli: python_input.show_status_bar and + not python_input.show_exit_confirmation)) -class ExitConfirmation(ConditionalContainer): +def exit_confirmation(python_input, token=Token.ExitConfirmation): """ - Display exit message. + Create `Layout` for the exit message. """ - def __init__(self, python_input, token=Token.ExitConfirmation): - def get_tokens(cli): - # Show "Do you really want to exit?" - return [ - (token, '\n %s ([y]/n)' % python_input.exit_message), - (Token.SetCursorPosition, ''), - (token, ' \n'), - ] + def get_tokens(cli): + # Show "Do you really want to exit?" + return [ + (token, '\n %s ([y]/n)' % python_input.exit_message), + (Token.SetCursorPosition, ''), + (token, ' \n'), + ] - visible = ~IsDone() & Condition(lambda cli: python_input.show_exit_confirmation) + visible = ~IsDone() & Condition(lambda cli: python_input.show_exit_confirmation) - super(ExitConfirmation, self).__init__( - content=Window( - TokenListControl(get_tokens, default_char=Char(token=token), - has_focus=visible)), - filter=visible) + return ConditionalContainer( + content=Window(TokenListControl( + get_tokens, default_char=Char(token=token), has_focus=visible)), + filter=visible) def meta_enter_message(python_input): @@ -446,7 +451,7 @@ def menu_position(cli): content=HSplit([ VSplit([ Window( - PythonPrompt(python_input), + python_prompt(python_input), dont_extend_width=True, height=D.exact(1), ), @@ -468,13 +473,13 @@ def menu_position(cli): extra_filter=show_multi_column_completions_menu(python_input))), Float(xcursor=True, ycursor=True, - content=SignatureToolbar(python_input)), + content=signature_toolbar(python_input)), Float(left=2, bottom=1, - content=ExitConfirmation(python_input)), + content=exit_confirmation(python_input)), Float(bottom=0, right=0, height=1, content=meta_enter_message(python_input)), - Float(bottom=1, left=1, right=0, content=PythonSidebarHelp(python_input)), + Float(bottom=1, left=1, right=0, content=python_sidebar_help(python_input)), ]), ArgToolbar(), SearchToolbar(), @@ -499,13 +504,13 @@ def menu_position(cli): ), ]), HSplit([ - PythonSidebar(python_input), - PythonSidebarNavigation(python_input), + python_sidebar(python_input), + python_sidebar_navigation(python_input), ]) ]), ] + extra_toolbars + [ VSplit([ - PythonToolbar(key_bindings_manager, python_input), - ShowSidebarButtonInfo(python_input), + status_bar(key_bindings_manager, python_input), + show_sidebar_button_info(python_input), ]) ]) From db044d02dd4f45b10e3378c2c99358125798fdd8 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sun, 6 Sep 2015 03:28:14 +0200 Subject: [PATCH 061/470] Many styling improvements. --- ptpython/history_browser.py | 98 ++++++++++++++++--------------------- ptpython/layout.py | 21 ++++---- ptpython/style.py | 8 +-- 3 files changed, 59 insertions(+), 68 deletions(-) diff --git a/ptpython/history_browser.py b/ptpython/history_browser.py index 185a9a17..80a6be6d 100644 --- a/ptpython/history_browser.py +++ b/ptpython/history_browser.py @@ -10,11 +10,11 @@ from prompt_toolkit.buffer import Buffer, AcceptAction from prompt_toolkit.document import Document from prompt_toolkit.enums import DEFAULT_BUFFER -from prompt_toolkit.filters import Always, Condition, HasFocus +from prompt_toolkit.filters import Always, Condition, HasFocus, InFocusStack from prompt_toolkit.key_binding.manager import KeyBindingManager from prompt_toolkit.keys import Keys from prompt_toolkit.layout.containers import HSplit, VSplit, Window, FloatContainer, Float, ConditionalContainer, Layout -from prompt_toolkit.layout.controls import BufferControl, FillControl +from prompt_toolkit.layout.controls import BufferControl, FillControl, TokenListControl from prompt_toolkit.layout.dimension import LayoutDimension as D from prompt_toolkit.layout.lexers import PygmentsLexer from prompt_toolkit.layout.margins import Margin @@ -133,62 +133,57 @@ def create_layout(python_input, history_mapping): """ Create and return a `Layout` instance for the history application. """ - input_processors = [ + default_processors = [ HighlightSearchProcessor(preview_search=Always()), HighlightSelectionProcessor()] help_window = create_popup_window( title='History Help', - body=Window(content=BufferControl( - buffer_name=HELP_BUFFER, - default_char=Char(token=Token), - lexer=PygmentsLexer(RstLexer)), - scroll_offset=2)) + body=Window( + content=BufferControl( + buffer_name=HELP_BUFFER, + default_char=Char(token=Token), + lexer=PygmentsLexer(RstLexer), + input_processors=default_processors), + scroll_offset=2)) return HSplit([ # Top title bar. TokenListToolbar( get_tokens=_get_top_toolbar_tokens, align_center=True, - default_char=Char(' ', Token.Sidebar.Title)), + default_char=Char(' ', Token.Toolbar.Status)), FloatContainer( content=VSplit([ # Left side: history. - HSplit([ - TokenListToolbar( - get_tokens=_get_history_toolbar_tokens, - align_center=True, - default_char=Char(' ', Token.Toolbar.Status)), - Window( - content=BufferControl( - margin=HistoryMargin(history_mapping), - buffer_name=HISTORY_BUFFER, - lexer=PygmentsLexer(PythonLexer), - input_processors=input_processors), - scroll_offset=2), - ]), + Window( + content=BufferControl( + margin=HistoryMargin(history_mapping), + buffer_name=HISTORY_BUFFER, + lexer=PygmentsLexer(PythonLexer), + input_processors=default_processors), + scroll_offset=2), # Separator. Window(width=D.exact(1), content=FillControl(BORDER.LIGHT_VERTICAL, token=Token.Separator)), # Right side: result. - HSplit([ - TokenListToolbar( - get_tokens=_get_result_toolbar_tokens, - align_center=True, - default_char=Char(' ', Token.Toolbar.Status)), - Window( - content=BufferControl( - margin=ResultMargin(history_mapping), - buffer_name=DEFAULT_BUFFER, - input_processors=input_processors + [GrayExistingText(history_mapping)], - lexer=PygmentsLexer(PythonLexer)), - scroll_offset=2) - ]) + Window( + content=BufferControl( + margin=ResultMargin(history_mapping), + buffer_name=DEFAULT_BUFFER, + input_processors=default_processors + [GrayExistingText(history_mapping)], + lexer=PygmentsLexer(PythonLexer)), + scroll_offset=2) ]), - # Help text as a float. - floats=[Float(width=60, top=3, bottom=2, - content=ConditionalContainer( - content=help_window, filter=HasFocus(HELP_BUFFER)))] + floats=[ + # Help text as a float. + Float(width=60, top=3, bottom=2, + content=ConditionalContainer( + # (We use InFocusStack, because it's possible to search + # through the help text as well, and at that point the search + # buffer has the focus.) + content=help_window, filter=InFocusStack(HELP_BUFFER))), + ] ), # Bottom toolbars. ArgToolbar(), @@ -200,15 +195,7 @@ def create_layout(python_input, history_mapping): def _get_top_toolbar_tokens(cli): - return [(Token.Sidebar.Title, 'History browser - Insert from history')] - - -def _get_history_toolbar_tokens(cli): - return [(Token.Toolbar.Status, ' History (latest %i entries)' % HISTORY_COUNT)] - - -def _get_result_toolbar_tokens(cli): - return [(Token.Toolbar.Status, ' Current input')] + return [(Token.Toolbar.Status.Title, 'History browser - Insert from history')] def _get_bottom_toolbar_tokens(cli, python_input): @@ -216,16 +203,14 @@ def _get_bottom_toolbar_tokens(cli, python_input): (Token.Toolbar.Status, ' ') ] + get_inputmode_tokens(cli, python_input) + [ (Token.Toolbar.Status, ' '), - (Token.Toolbar.Status.Key, '[F1]'), - (Token.Toolbar.Status.On, ' Help '), (Token.Toolbar.Status.Key, '[Space]'), - (Token.Toolbar.Status.On, ' Toggle '), + (Token.Toolbar.Status, ' Toggle '), (Token.Toolbar.Status.Key, '[Tab]'), - (Token.Toolbar.Status.On, ' Focus '), + (Token.Toolbar.Status, ' Focus '), (Token.Toolbar.Status.Key, '[Enter]'), - (Token.Toolbar.Status.On, ' Accept '), - (Token.Toolbar.Status.Key, '[Ctrl-C]'), - (Token.Toolbar.Status.On, ' Cancel '), + (Token.Toolbar.Status, ' Accept '), + (Token.Toolbar.Status.Key, '[F1]'), + (Token.Toolbar.Status, ' Help '), ] @@ -342,6 +327,9 @@ def __init__(self, python_history, original_document): for line in entry.splitlines(): history_lines.append(line) + if len(python_history) > HISTORY_COUNT: + history_lines[0] = '# *** History has been truncated to %s lines ***' % HISTORY_COUNT + self.history_lines = history_lines self.concatenated_history = '\n'.join(history_lines) diff --git a/ptpython/layout.py b/ptpython/layout.py index 71ce2ea2..fd6ca856 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -253,7 +253,7 @@ def get_tokens(cli): append((TB, ' ')) result.extend(get_inputmode_tokens(cli, python_input)) - append((TB, ' ')) + append((TB, ' ')) # Position in history. append((TB, '%i/%i ' % (python_buffer.working_index + 1, @@ -266,14 +266,17 @@ def get_tokens(cli): # Emacs cut/copy keys. append((TB, '[Ctrl-W] Cut [Meta-W] Copy [Ctrl-Y] Paste [Ctrl-G] Cancel')) else: - append((TB, ' ')) - - append((TB.On, '[F3] History ')) + result.extend([ + (TB.Key, '[F3]'), + (TB, ' History '), + (TB.Key, '[F6]'), + (TB, ' '), + ]) if python_input.paste_mode: - append((TB.On, '[F6] Paste mode (on) ')) + append((TB.PasteModeOn, 'Paste mode (on)')) else: - append((TB.Off, '[F6] Paste mode (off) ')) + append((TB, 'Paste mode')) return result @@ -305,7 +308,6 @@ def get_inputmode_tokens(cli, python_input): if bool(cli.current_buffer.selection_state): if cli.current_buffer.selection_state.type == SelectionType.LINES: append((token.InputMode, 'Vi (VISUAL LINE)')) - append((token, ' ')) elif cli.current_buffer.selection_state.type == SelectionType.CHARACTERS: append((token.InputMode, 'Vi (VISUAL)')) append((token, ' ')) @@ -334,7 +336,8 @@ def show_sidebar_button_info(python_input): version = sys.version_info tokens = [ - (token, ' [F2] Options'), + (token.Key, '[F2]'), + (token, ' Menu'), (token, ' - '), (token.PythonVersion, '%s %i.%i.%i' % (platform.python_implementation(), version[0], version[1], version[2])), @@ -381,7 +384,7 @@ def meta_enter_message(python_input): Create the `Layout` for the 'Meta+Enter` message. """ def get_tokens(cli): - return [(Token.MetaEnterMessage, ' [Meta+Enter] Execute ')] + return [(Token.AcceptMessage, ' [Meta+Enter] Execute ')] visible = ~IsDone() & HasFocus(DEFAULT_BUFFER) & Condition( lambda cli: diff --git a/ptpython/style.py b/ptpython/style.py index 855b8f08..6d27f88c 100644 --- a/ptpython/style.py +++ b/ptpython/style.py @@ -110,10 +110,10 @@ class PythonStyle(Style): # Status toolbar. Token.Toolbar.Status: 'bg:#222222 #aaaaaa', + Token.Toolbar.Status.Title: 'underline', Token.Toolbar.Status.InputMode: 'bg:#222222 #ffffaa', - Token.Toolbar.Status.Key: 'bg:#bbbbbb #000000', - Token.Toolbar.Status.Off: 'bg:#222222 #aaaaaa', - Token.Toolbar.Status.On: 'bg:#222222 #ffffff', + Token.Toolbar.Status.Key: 'bg:#000000 #888888', + Token.Toolbar.Status.PasteModeOn: 'bg:#aa4444 #ffffff', Token.Toolbar.Status.PythonVersion: 'bg:#222222 #ffffff bold', # When Control-C has been pressed. Grayed. @@ -144,7 +144,7 @@ class PythonStyle(Style): Token.Window.Title: 'bg:#bbbbbb #000000', # Meta-enter message. - Token.MetaEnterMessage: 'bg:#ffff88 #444444', + Token.AcceptMessage: 'bg:#ffff88 #444444', # Exit confirmation. Token.ExitConfirmation: 'bg:#884444 #ffffff', From a7ddfde8666af3ced8610a2ebf72f9efcca0b405 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sun, 6 Sep 2015 04:08:25 +0200 Subject: [PATCH 062/470] Only show [Meta+Enter]-message when the cursor is in the middle of a string. --- ptpython/layout.py | 13 ++++++++++--- ptpython/python_input.py | 1 + 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/ptpython/layout.py b/ptpython/layout.py index fd6ca856..846bbd95 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -386,10 +386,17 @@ def meta_enter_message(python_input): def get_tokens(cli): return [(Token.AcceptMessage, ' [Meta+Enter] Execute ')] - visible = ~IsDone() & HasFocus(DEFAULT_BUFFER) & Condition( - lambda cli: + def extra_condition(cli): + " Only show when... " + b = cli.buffers[DEFAULT_BUFFER] + + return ( python_input.show_meta_enter_message and - cli.buffers[DEFAULT_BUFFER].is_multiline()) + (not b.document.is_cursor_at_the_end or + python_input.accept_input_on_enter is None) and + b.is_multiline()) + + visible = ~IsDone() & HasFocus(DEFAULT_BUFFER) & Condition(extra_condition) return ConditionalContainer( content=Window(TokenListControl(get_tokens)), diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 1d515d48..cc2d11f6 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -495,6 +495,7 @@ def _create_buffer(self): """ def is_buffer_multiline(): return (self.paste_mode or + self.accept_input_on_enter is None or document_is_multiline_python(python_buffer.document)) python_buffer = Buffer( From 9a8b8d54ff895f67b9caa4d8262be1db6d15edc8 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sun, 6 Sep 2015 04:16:18 +0200 Subject: [PATCH 063/470] Use SimpleLexer (prompt-toolkit compatibility fix.) --- ptpython/ipython.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/ptpython/ipython.py b/ptpython/ipython.py index f39f4d40..813a6981 100644 --- a/ptpython/ipython.py +++ b/ptpython/ipython.py @@ -17,7 +17,7 @@ from prompt_toolkit.contrib.regular_languages.lexer import GrammarLexer from prompt_toolkit.document import Document from prompt_toolkit.interface import CommandLineInterface -from prompt_toolkit.layout.lexers import PygmentsLexer +from prompt_toolkit.layout.lexers import PygmentsLexer, SimpleLexer from .python_input import PythonInput, PythonValidator, PythonCompleter from .eventloop import create_eventloop @@ -112,12 +112,10 @@ def create_lexer(): return GrammarLexer( g, - tokens={ - 'percent': Token.Operator, - 'magic': Token.Keyword, - 'filename': Token.Name, - }, lexers={ + 'percent': SimpleLexer(Token.Operator), + 'magic': SimpleLexer(Token.Keyword), + 'filename': SimpleLexer(Token.Name), 'python': PygmentsLexer(PythonLexer), 'system': PygmentsLexer(BashLexer), }) From a9ec8540981797233e601599c8b747daa0356c56 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sun, 6 Sep 2015 04:16:38 +0200 Subject: [PATCH 064/470] Create config directory if it doesn't exist yet (For IPython entry point.) --- ptpython/entry_points/run_ptipython.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ptpython/entry_points/run_ptipython.py b/ptpython/entry_points/run_ptipython.py index 352d65c1..799f8e32 100644 --- a/ptpython/entry_points/run_ptipython.py +++ b/ptpython/entry_points/run_ptipython.py @@ -26,6 +26,10 @@ def run(): vi_mode = bool(a['--vi']) config_dir = os.path.expanduser(a['--config-dir'] or '~/.ptpython/') + # Create config directory. + if not os.path.isdir(config_dir): + os.mkdir(config_dir) + # If IPython is not available, show message and exit here with error status # code. try: From abb1d403f391f574f9b762a166d61e471de84553 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sun, 6 Sep 2015 04:25:28 +0200 Subject: [PATCH 065/470] Rephrased history help text. --- ptpython/history_browser.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/ptpython/history_browser.py b/ptpython/history_browser.py index 80a6be6d..2ad88a44 100644 --- a/ptpython/history_browser.py +++ b/ptpython/history_browser.py @@ -49,11 +49,11 @@ ------------- 1. Move the ``cursor up`` in the history pane, until the - first line that you want in the output is selected. + cursor is on the first desired line. 2. Hold down the ``space bar``, or press it multiple times. Each time it will select one line and move to the next one. Each selected line will appear on the - right side, under current input. + right side. 3. When all the required lines are displayed on the right side, press ``Enter``. This will go back to the Python REPL and show these lines as the current input. They @@ -63,7 +63,7 @@ ------------ Many Emacs and Vi navigation key bindings should work. -Press ``F4`` to switch between Emacs andVi mode. +Press ``F4`` to switch between Emacs and Vi mode. Additional bindings: @@ -71,13 +71,13 @@ - ``Tab``: Move the focus between the history and input pane. (Alternative: ``Ctrl-W``) - ``Ctrl-C``: Cancel. Ignore the result and go back to - the REPL. (Alternative: ``q``.) + the REPL. (Alternatives: ``q`` and ``Control-G``.) - ``Enter``: Accept the result and go back to the REPL. -- ``F1``: Show/hide help. (Alternative ``h``.) Press - ``Enter`` to quit this help message. +- ``F1``: Show/hide help. Press ``Enter`` to quit this + help message. Further, remember that searching works like in Emacs -(``Ctrl-R``) or Vi (``/``). +(using ``Ctrl-R``) or Vi (using ``/``). """ @@ -458,6 +458,7 @@ def _(event): @handle(Keys.ControlJ, filter=help_focussed) @handle(Keys.ControlC, filter=help_focussed) + @handle(Keys.ControlG, filter=help_focussed) @handle(Keys.Escape, filter=help_focussed) def _(event): " Leave help. " From 5278d24049d928da8c02af4f832179283240ac3a Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sun, 6 Sep 2015 04:45:18 +0200 Subject: [PATCH 066/470] Upgrade to prompt-toolkit 0.49 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 02af5266..ab430967 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ long_description=long_description, packages=find_packages('.'), install_requires = [ - 'prompt_toolkit==0.47', + 'prompt_toolkit==0.49', 'jedi>=0.9.0', 'docopt', ], From 101fa01a3280bc90349ce8300e18ae8ca5d88d67 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sun, 6 Sep 2015 04:48:57 +0200 Subject: [PATCH 067/470] Added history_browser to tests. --- tests/run_tests.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/run_tests.py b/tests/run_tests.py index 4ed78d35..a23fddec 100755 --- a/tests/run_tests.py +++ b/tests/run_tests.py @@ -11,14 +11,15 @@ import ptpython.completer import ptpython.filters #import ptpython.ipython -import ptpython.layout -import ptpython.python_input -import ptpython.style -import ptpython.validator import ptpython.eventloop +import ptpython.history_browser import ptpython.key_bindings +import ptpython.layout +import ptpython.python_input import ptpython.repl +import ptpython.style import ptpython.utils +import ptpython.validator if __name__ == '__main__': From 0e394236ed25d7952a311dadf67bcb85a60ed85c Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sun, 6 Sep 2015 06:50:36 +0200 Subject: [PATCH 068/470] Added MANIFEST.in --- MANIFEST.in | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 MANIFEST.in diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 00000000..b010432b --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +include *rst LICENSE CHANGELOG MANIFEST.in +recursive-include examples *.py +prune examples/sample?/build From e75ac1f87ec66e63cd08723843ba1c0858498780 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sun, 6 Sep 2015 06:38:30 +0200 Subject: [PATCH 069/470] Pypi release 0.22 --- CHANGELOG | 21 +++++++++++++++++++++ setup.py | 4 ++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index deee80d0..69881fc7 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,27 @@ CHANGELOG ========= +0.22: 2015-09-06 +---------------- + +Upgrade to prompt_toolkit 0.50 + +Fixes: +- Correctly accept file parameter in the print function of + asyncssh_repl.ReplSSHServerSession. +- Create config directory if it doesn't exist yet (For IPython entry point.) + +New features: +- Implementation of history-selection: a tool to select lines from the history. +- Make exit message configurable. +- Improve start-up time: Lazy load completer grammar and lazy-import Jedi. +- Make multi-column the default completion visualisation. +- Implementation of a custom prompts. In_tokens and out_tokens can be + customized. +- Made an option to show/hide highlighting for matching parenthesis. +- Some styling improvements. + + 0.21: 2015-08-08 --------------- diff --git a/setup.py b/setup.py index ab430967..d1f6becc 100644 --- a/setup.py +++ b/setup.py @@ -13,13 +13,13 @@ setup( name='ptpython', author='Jonathan Slenders', - version='0.21', + version='0.22', url='https://github.com/jonathanslenders/ptpython', description='Python REPL build on top of prompt_toolkit', long_description=long_description, packages=find_packages('.'), install_requires = [ - 'prompt_toolkit==0.49', + 'prompt_toolkit==0.50', 'jedi>=0.9.0', 'docopt', ], From 2817c83149e9cafc3cd52c8d4b09ef9c7e98c3e6 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sun, 6 Sep 2015 20:41:11 +0200 Subject: [PATCH 070/470] Use Pygments Python3Lexer for Python3. --- ptpython/history_browser.py | 8 +++++++- ptpython/python_input.py | 5 ++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/ptpython/history_browser.py b/ptpython/history_browser.py index 2ad88a44..a4bc301e 100644 --- a/ptpython/history_browser.py +++ b/ptpython/history_browser.py @@ -25,13 +25,19 @@ from prompt_toolkit.layout.utils import explode_tokens from prompt_toolkit.layout.toolbars import TokenListToolbar from prompt_toolkit.utils import Callback -from pygments.lexers import PythonLexer, RstLexer +from pygments.lexers import RstLexer from pygments.token import Token from ptpython.layout import get_inputmode_tokens from functools import partial import six +if six.PY3: + from pygments.lexers import Python3Lexer as PythonLexer +else: + from pygments.lexers import PythonLexer + + HISTORY_BUFFER = 'HISTORY_BUFFER' HELP_BUFFER = 'HELP_BUFFER' diff --git a/ptpython/python_input.py b/ptpython/python_input.py index cc2d11f6..50abc61b 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -30,11 +30,14 @@ from ptpython.prompt_style import IPythonPrompt, ClassicPrompt from functools import partial -from pygments.lexers import PythonLexer import six import __future__ +if six.PY3: + from pygments.lexers import Python3Lexer as PythonLexer +else: + from pygments.lexers import PythonLexer __all__ = ( 'PythonInput', From f908f74cb08f8f3e4e105ebdd9dba5193e9d8d54 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Wed, 9 Sep 2015 04:11:09 +0200 Subject: [PATCH 071/470] Processor.run now get 'buffer' instead of 'document' as second argument. --- ptpython/history_browser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ptpython/history_browser.py b/ptpython/history_browser.py index a4bc301e..64e09c0a 100644 --- a/ptpython/history_browser.py +++ b/ptpython/history_browser.py @@ -296,7 +296,7 @@ def __init__(self, history_mapping): self._len_before = len(history_mapping.original_document.text_before_cursor) self._len_after = len(history_mapping.original_document.text_after_cursor) - def run(self, cli, document, tokens): + def run(self, cli, buffer, tokens): if self._len_before or self._len_after: tokens = explode_tokens(tokens) pos_after = len(tokens) - self._len_after From 5e8ad2e6273de764f8abebb007818dd1e83ef5a6 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 10 Sep 2015 06:37:15 +0200 Subject: [PATCH 072/470] Refactoring of the margins. (Needs the newest prompt-toolkit.) --- examples/ptpython_config/config.py | 3 ++ ptpython/filters.py | 7 ---- ptpython/history_browser.py | 66 +++++++++++++++++++---------- ptpython/layout.py | 67 ++++++++++++++++++++---------- ptpython/prompt_style.py | 18 ++++++++ ptpython/python_input.py | 6 +-- 6 files changed, 112 insertions(+), 55 deletions(-) diff --git a/examples/ptpython_config/config.py b/examples/ptpython_config/config.py index 6d64608b..b138833f 100644 --- a/examples/ptpython_config/config.py +++ b/examples/ptpython_config/config.py @@ -58,6 +58,9 @@ def configure(repl): # Paste mode. (When True, don't insert whitespace after new line.) repl.paste_mode = False + # Use the classic prompt. (Display '>>>' instead of 'In [1]'.) + repl.prompt_style = 'classic' # 'classic' or 'ipython' + # History Search. # When True, going back in history will filter the history on the records # starting with the current input. (Like readline.) diff --git a/ptpython/filters.py b/ptpython/filters.py index 30ef8301..1180af13 100644 --- a/ptpython/filters.py +++ b/ptpython/filters.py @@ -3,7 +3,6 @@ from prompt_toolkit.filters import Filter __all__ = ( - 'ShowLineNumbersFilter', 'HasSignature', 'ShowSidebar', 'ShowDocstring', @@ -18,12 +17,6 @@ def __call__(self, cli): raise NotImplementedError -class ShowLineNumbersFilter(PythonInputFilter): - def __call__(self, cli): - return ('\n' in cli.buffers['default'].text and - self.python_input.show_line_numbers) - - class HasSignature(PythonInputFilter): def __call__(self, cli): return bool(self.python_input.signatures) diff --git a/ptpython/history_browser.py b/ptpython/history_browser.py index 64e09c0a..e92d9ac8 100644 --- a/ptpython/history_browser.py +++ b/ptpython/history_browser.py @@ -13,11 +13,11 @@ from prompt_toolkit.filters import Always, Condition, HasFocus, InFocusStack from prompt_toolkit.key_binding.manager import KeyBindingManager from prompt_toolkit.keys import Keys -from prompt_toolkit.layout.containers import HSplit, VSplit, Window, FloatContainer, Float, ConditionalContainer, Layout -from prompt_toolkit.layout.controls import BufferControl, FillControl, TokenListControl +from prompt_toolkit.layout.containers import HSplit, VSplit, Window, FloatContainer, Float, ConditionalContainer, Layout, ScrollOffsets +from prompt_toolkit.layout.controls import BufferControl, FillControl from prompt_toolkit.layout.dimension import LayoutDimension as D from prompt_toolkit.layout.lexers import PygmentsLexer -from prompt_toolkit.layout.margins import Margin +from prompt_toolkit.layout.margins import Margin, ScrollbarMargin from prompt_toolkit.layout.processors import HighlightSearchProcessor, HighlightSelectionProcessor from prompt_toolkit.layout.screen import Char from prompt_toolkit.layout.toolbars import ArgToolbar, SearchToolbar @@ -41,7 +41,7 @@ HISTORY_BUFFER = 'HISTORY_BUFFER' HELP_BUFFER = 'HELP_BUFFER' -HISTORY_COUNT = 500 +HISTORY_COUNT = 2000 __all__ = ( 'create_history_application', @@ -151,7 +151,8 @@ def create_layout(python_input, history_mapping): default_char=Char(token=Token), lexer=PygmentsLexer(RstLexer), input_processors=default_processors), - scroll_offset=2)) + right_margins=[ScrollbarMargin()], + scroll_offsets=ScrollOffsets(top=2, bottom=2))) return HSplit([ # Top title bar. @@ -164,22 +165,24 @@ def create_layout(python_input, history_mapping): # Left side: history. Window( content=BufferControl( - margin=HistoryMargin(history_mapping), buffer_name=HISTORY_BUFFER, + wrap_lines=False, lexer=PygmentsLexer(PythonLexer), input_processors=default_processors), - scroll_offset=2), + left_margins=[HistoryMargin(history_mapping)], + scroll_offsets=ScrollOffsets(top=2, bottom=2)), # Separator. Window(width=D.exact(1), content=FillControl(BORDER.LIGHT_VERTICAL, token=Token.Separator)), # Right side: result. Window( content=BufferControl( - margin=ResultMargin(history_mapping), buffer_name=DEFAULT_BUFFER, + wrap_lines=False, input_processors=default_processors + [GrayExistingText(history_mapping)], lexer=PygmentsLexer(PythonLexer)), - scroll_offset=2) + left_margins=[ResultMargin(history_mapping)], + scroll_offsets=ScrollOffsets(top=2, bottom=2)), ]), floats=[ # Help text as a float. @@ -228,13 +231,23 @@ class HistoryMargin(Margin): def __init__(self, history_mapping): self.history_mapping = history_mapping - def create_handler(self, cli, document): + def get_width(self, cli): + return 2 + + def create_margin(self, cli, window_render_info, width, height): + document = cli.buffers[HISTORY_BUFFER].document + lines_starting_new_entries = self.history_mapping.lines_starting_new_entries selected_lines = self.history_mapping.selected_lines current_lineno = document.cursor_position_row - def margin(line_number): + visible_line_to_input_line = window_render_info.visible_line_to_input_line + result = [] + + for y in range(height): + line_number = visible_line_to_input_line.get(y) + # Show stars at the start of each entry. # (Visualises multiline entries.) if line_number in lines_starting_new_entries: @@ -250,14 +263,10 @@ def margin(line_number): if line_number == current_lineno: t = t.Current - return [(t, char), (Token, ' ')] - return margin + result.append((t, char)) + result.append((Token, '\n')) - def invalidation_hash(self, cli, document): - return ( - frozenset(self.history_mapping.selected_lines), - document.cursor_position_row, - ) + return result class ResultMargin(Margin): @@ -267,11 +276,22 @@ class ResultMargin(Margin): def __init__(self, history_mapping): self.history_mapping = history_mapping - def create_handler(self, cli, document): + def get_width(self, cli): + return 2 + + def create_margin(self, cli, window_render_info, width, height): + document = cli.buffers[DEFAULT_BUFFER].document + current_lineno = document.cursor_position_row offset = self.history_mapping.result_line_offset #original_document.cursor_position_row - def margin(line_number): + visible_line_to_input_line = window_render_info.visible_line_to_input_line + + result = [] + + for y in range(height): + line_number = visible_line_to_input_line.get(y) + if (line_number is None or line_number < offset or line_number >= offset + len(self.history_mapping.selected_lines)): t = Token @@ -280,8 +300,10 @@ def margin(line_number): else: t = Token.History.Line.Selected - return [(t, ' '), (Token, ' ')] - return margin + result.append((t, ' ')) + result.append((Token, '\n')) + + return result def invalidation_hash(self, cli, document): return document.cursor_position_row diff --git a/ptpython/layout.py b/ptpython/layout.py index 846bbd95..2ac0533f 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -6,11 +6,11 @@ from prompt_toolkit.enums import DEFAULT_BUFFER, SEARCH_BUFFER from prompt_toolkit.filters import IsDone, HasCompletions, RendererHeightIsKnown, Always, HasFocus, Condition from prompt_toolkit.key_binding.vi_state import InputMode -from prompt_toolkit.layout import Window, HSplit, VSplit, FloatContainer, Float, ConditionalContainer +from prompt_toolkit.layout.containers import Window, HSplit, VSplit, FloatContainer, Float, ConditionalContainer, ScrollOffsets from prompt_toolkit.layout.controls import BufferControl, TokenListControl, FillControl from prompt_toolkit.layout.dimension import LayoutDimension from prompt_toolkit.layout.lexers import SimpleLexer -from prompt_toolkit.layout.margins import ConditionalMargin, NumberredMargin +from prompt_toolkit.layout.margins import Margin from prompt_toolkit.layout.menus import CompletionsMenu, MultiColumnCompletionsMenu from prompt_toolkit.layout.processors import HighlightSearchProcessor, HighlightSelectionProcessor, HighlightMatchingBracketProcessor, ConditionalProcessor from prompt_toolkit.layout.screen import Char @@ -18,8 +18,9 @@ from prompt_toolkit.layout.utils import token_list_width from prompt_toolkit.reactive import Integer from prompt_toolkit.selection import SelectionType +from prompt_toolkit.utils import get_cwidth -from ptpython.filters import HasSignature, ShowSidebar, ShowLineNumbersFilter, ShowSignature, ShowDocstring +from ptpython.filters import HasSignature, ShowSidebar, ShowSignature, ShowDocstring from pygments.lexers import PythonLexer from pygments.token import Token @@ -102,7 +103,7 @@ def append(selected, label, status): has_focus=ShowSidebar(python_input) & ~IsDone()), width=LayoutDimension.exact(43), height=LayoutDimension(min=3), - scroll_offset=1), + scroll_offsets=ScrollOffsets(top=1, bottom=1)), filter=ShowSidebar(python_input) & ~IsDone()) @@ -228,15 +229,42 @@ def get_tokens(cli): ~IsDone()) -def python_prompt(python_input): +class PromptMargin(Margin): """ - Create layout for the prompt. + Create margin that displays the prompt. It shows something like "In [1]:". """ - def get_tokens(cli): - return python_input.get_input_prompt_tokens(cli) + def __init__(self, python_input): + self.python_input = python_input + + def _get_prompt_style(self): + return self.python_input.all_prompt_styles[self.python_input.prompt_style] + + def get_width(self, cli): + # Take the width from the first line. + text = ''.join(t[1] for t in self.python_input.get_input_prompt_tokens(cli)) + return get_cwidth(text) + + def create_margin(self, cli, window_render_info, width, height): + style = self._get_prompt_style() + + # First line. + tokens = style.in_tokens(cli) - return TokenListControl(get_tokens) + # Next lines. (Show line numbering when numbering is enabled.) + tokens2 = style.in2_tokens(cli, width) + show_numbers = self.python_input.show_line_numbers + visible_line_to_input_line = window_render_info.visible_line_to_input_line + + for y in range(1, min(window_render_info.content_height, height)): + tokens.append((Token, '\n')) + if show_numbers: + line_number = visible_line_to_input_line.get(y) + tokens.append((Token.LineNumber, ('%i ' % (line_number + 1)).rjust(width))) + else: + tokens.extend(tokens2) + + return tokens def status_bar(key_bindings_manager, python_input): @@ -430,9 +458,6 @@ def menu_position(cli): BufferControl( buffer_name=DEFAULT_BUFFER, lexer=lexer, - margin=ConditionalMargin( - NumberredMargin(), - filter=ShowLineNumbersFilter(python_input)), input_processors=[ # Show matching parentheses, but only while editing. ConditionalProcessor( @@ -448,6 +473,10 @@ def menu_position(cli): # Make sure that we always see the result of an reverse-i-search: preview_search=Always(), ), + left_margins=[PromptMargin(python_input)], + # Scroll offsets. The 1 at the bottom is important to make sure the + # cursor is never below the "Press [Meta+Enter]" message which is a float. + scroll_offsets=ScrollOffsets(bottom=1, left=4, right=4), # As long as we're editing, prefer a minimal height of 6. get_height=(lambda cli: ( None if cli.is_done or python_input.show_exit_confirmation @@ -458,17 +487,9 @@ def menu_position(cli): VSplit([ HSplit([ FloatContainer( - content=HSplit([ - VSplit([ - Window( - python_prompt(python_input), - dont_extend_width=True, - height=D.exact(1), - ), - create_python_input_window(), - ]), - ] + extra_body + [ - ]), + content=HSplit( + [create_python_input_window()] + extra_body + ), floats=[ Float(xcursor=True, ycursor=True, diff --git a/ptpython/prompt_style.py b/ptpython/prompt_style.py index ef2f2b1d..158759e0 100644 --- a/ptpython/prompt_style.py +++ b/ptpython/prompt_style.py @@ -19,6 +19,16 @@ def in_tokens(self, cli): " Return the input tokens. " return [] + @abstractmethod + def in2_tokens(self, cli, width): + """ + Tokens for every following input line. + + :param width: The available width. This is coming from the width taken + by `in_tokens`. + """ + return [] + @abstractmethod def out_tokens(self, cli): " Return the output tokens. " @@ -39,6 +49,11 @@ def in_tokens(self, cli): (Token.In, ']: '), ] + def in2_tokens(self, cli, width): + return [ + (Token.In, '...: '.rjust(width)), + ] + def out_tokens(self, cli): return [ (Token.Out, 'Out['), @@ -55,5 +70,8 @@ class ClassicPrompt(PromptStyle): def in_tokens(self, cli): return [(Token, '>>> ')] + def in2_tokens(self, cli, width): + return [(Token, '...')] + def out_tokens(self, cli): return [] diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 50abc61b..53525445 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -158,7 +158,7 @@ def __init__(self, self.completion_visualisation = CompletionVisualisation.MULTI_COLUMN self.completion_menu_scroll_offset = 1 - self.show_line_numbers = True + self.show_line_numbers = False self.show_status_bar = True self.complete_while_typing = True self.vi_mode = vi_mode @@ -182,7 +182,7 @@ def __init__(self, self.exit_message = 'Do you really want to exit?' # Tokens to be shown at the prompt. - self.prompt_style = 'ipython' # The currently active style. + self.prompt_style = 'classic' # The currently active style. self.all_prompt_styles = { # Styles selectable from the menu. 'ipython': IPythonPrompt(self), @@ -421,7 +421,7 @@ def get_values(): CompletionVisualisation.TOOLBAR: lambda: enable('completion_visualisation', CompletionVisualisation.TOOLBAR), }), Option(title='Prompt', - description='Visualisation of the prompt.', + description="Visualisation of the prompt. ('>>>' or 'In [1]:')", get_current_value=lambda: self.prompt_style, get_values=lambda: dict((s, partial(enable, 'prompt_style', s)) for s in self.all_prompt_styles)), simple_option(title='Show signature', From c86bf3b9a103980f5379b9b6ad5baf7c5fee542d Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Wed, 16 Sep 2015 06:49:33 +0200 Subject: [PATCH 073/470] Make classic prompt bold by default. --- ptpython/prompt_style.py | 4 ++-- ptpython/style.py | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/ptpython/prompt_style.py b/ptpython/prompt_style.py index 158759e0..5a78d19c 100644 --- a/ptpython/prompt_style.py +++ b/ptpython/prompt_style.py @@ -68,10 +68,10 @@ class ClassicPrompt(PromptStyle): The classic Python prompt. """ def in_tokens(self, cli): - return [(Token, '>>> ')] + return [(Token.Prompt, '>>> ')] def in2_tokens(self, cli, width): - return [(Token, '...')] + return [(Token.Prompt.Dots, '...')] def out_tokens(self, cli): return [] diff --git a/ptpython/style.py b/ptpython/style.py index 6d27f88c..d2043025 100644 --- a/ptpython/style.py +++ b/ptpython/style.py @@ -76,7 +76,11 @@ class PythonStyle(Style): default_ui_style = { - # (Python) Prompt: "In [1]:" + # Classic prompt. + Token.Prompt: 'bold', + Token.Prompt.Dots: 'noinherit', + + # (IPython) Prompt: "In [1]:" Token.In: 'bold #008800', Token.In.Number: '', From b04380cec65c4866c98b193f816074e92029df69 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Wed, 16 Sep 2015 06:53:42 +0200 Subject: [PATCH 074/470] Fix Prompt for ptipython. --- ptpython/ipython.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ptpython/ipython.py b/ptpython/ipython.py index 813a6981..e338edaa 100644 --- a/ptpython/ipython.py +++ b/ptpython/ipython.py @@ -48,6 +48,10 @@ def in_tokens(self, cli): text = self.prompt_manager.render('in', color=False, just=False) return [(Token.In, text)] + def in2_tokens(self, cli, width): + text = self.prompt_manager.render('in2', color=False, just=False) + return [(Token.In, text.rjust(width))] + def out_tokens(self, cli): # This function is currently not used by IPython. But for completeness, # it would look like this. From 912af98c3c13633c3e9907ccc7cc8f4ff3c7a07a Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Wed, 16 Sep 2015 07:05:00 +0200 Subject: [PATCH 075/470] Added option for wrapping lines. --- ptpython/layout.py | 1 + ptpython/python_input.py | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/ptpython/layout.py b/ptpython/layout.py index 2ac0533f..fd63ef9a 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -469,6 +469,7 @@ def menu_position(cli): filter=HasFocus(SEARCH_BUFFER)), HighlightSelectionProcessor()] + extra_buffer_processors, menu_position=menu_position, + wrap_lines=Condition(lambda cli: python_input.wrap_lines), # Make sure that we always see the result of an reverse-i-search: preview_search=Always(), diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 53525445..e4ab5d4d 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -160,6 +160,7 @@ def __init__(self, self.show_line_numbers = False self.show_status_bar = True + self.wrap_lines = True self.complete_while_typing = True self.vi_mode = vi_mode self.paste_mode = False # When True, don't insert whitespace after newline. @@ -437,6 +438,9 @@ def get_values(): description='Show the [Meta+Enter] message when this key combination is required to execute commands. ' + '(This is the case when a simple [Enter] key press will insert a newline.', field_name='show_meta_enter_message'), + simple_option(title='Wrap lines', + description='Wrap lines instead of scrolling horizontally.', + field_name='wrap_lines'), simple_option(title='Show status bar', description='Show the status bar at the bottom of the terminal.', field_name='show_status_bar'), From b8c44a77f921b15a81d86ad439d1f4b8b1447997 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 17 Sep 2015 05:39:41 +0200 Subject: [PATCH 076/470] Added auto-suggest option. (+ input validator fix.) --- ptpython/layout.py | 6 ++++-- ptpython/python_input.py | 14 ++++++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/ptpython/layout.py b/ptpython/layout.py index fd63ef9a..dad90d55 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -12,7 +12,7 @@ from prompt_toolkit.layout.lexers import SimpleLexer from prompt_toolkit.layout.margins import Margin from prompt_toolkit.layout.menus import CompletionsMenu, MultiColumnCompletionsMenu -from prompt_toolkit.layout.processors import HighlightSearchProcessor, HighlightSelectionProcessor, HighlightMatchingBracketProcessor, ConditionalProcessor +from prompt_toolkit.layout.processors import HighlightSearchProcessor, HighlightSelectionProcessor, HighlightMatchingBracketProcessor, ConditionalProcessor, AppendAutoSuggestion from prompt_toolkit.layout.screen import Char from prompt_toolkit.layout.toolbars import CompletionsToolbar, ArgToolbar, SearchToolbar, ValidationToolbar, SystemToolbar, TokenListToolbar from prompt_toolkit.layout.utils import token_list_width @@ -467,7 +467,9 @@ def menu_position(cli): ConditionalProcessor( processor=HighlightSearchProcessor(preview_search=Always()), filter=HasFocus(SEARCH_BUFFER)), - HighlightSelectionProcessor()] + extra_buffer_processors, + HighlightSelectionProcessor(), + AppendAutoSuggestion(), + ] + extra_buffer_processors, menu_position=menu_position, wrap_lines=Condition(lambda cli: python_input.wrap_lines), diff --git a/ptpython/python_input.py b/ptpython/python_input.py index e4ab5d4d..738bf99d 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -19,7 +19,8 @@ from prompt_toolkit.key_binding.manager import KeyBindingManager from prompt_toolkit.layout.lexers import PygmentsLexer from prompt_toolkit.utils import Callback, is_windows -from prompt_toolkit.validation import SwitchableValidator +from prompt_toolkit.validation import ConditionalValidator +from prompt_toolkit.auto_suggest import AutoSuggestFromHistory, ConditionalAutoSuggest from ptpython.completer import PythonCompleter from ptpython.key_bindings import load_python_bindings, load_sidebar_bindings, load_confirm_exit_bindings @@ -170,6 +171,7 @@ def __init__(self, self.enable_open_in_editor = True self.enable_system_bindings = True self.enable_input_validation = True + self.enable_auto_suggest = False self.enable_history_search = False # When True, like readline, going # back in history will filter the # history on the records starting @@ -222,6 +224,7 @@ def __init__(self, enable_vi_mode=Condition(lambda cli: self.vi_mode), enable_open_in_editor=Condition(lambda cli: self.enable_open_in_editor), enable_system_bindings=Condition(lambda cli: self.enable_system_bindings), + enable_auto_suggest_bindings=Condition(lambda cli: self.enable_auto_suggest), # Disable all default key bindings when the sidebar or the exit confirmation # are shown. @@ -400,6 +403,10 @@ def get_values(): description='In case of syntax errors, move the cursor to the error ' 'instead of showing a traceback of a SyntaxError.', field_name='enable_input_validation'), + simple_option(title='Auto suggestion', + description='Auto suggest inputs by looking at the history.' + 'Pressing right arrow or Ctrl-E will complete the entry.', + field_name='enable_auto_suggest'), Option(title='Accept input on enter', description='Amount of ENTER presses required to execute input when the cursor ' 'is at the end of the input. (Note that META+ENTER will always execute.)', @@ -512,9 +519,12 @@ def is_buffer_multiline(): tempfile_suffix='.py', history=self.history, completer=self._completer, - validator=SwitchableValidator( + validator=ConditionalValidator( self._validator, Condition(lambda: self.enable_input_validation)), + auto_suggest=ConditionalAutoSuggest( + AutoSuggestFromHistory(), + Condition(lambda cli: self.enable_auto_suggest)), accept_action=self._accept_action) return python_buffer From a0a41e352f0a3ff01a307e14b6fb1ce89f1184c4 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 17 Sep 2015 07:08:11 +0200 Subject: [PATCH 077/470] Added wrap_lines and enable_auto_suggest to config template. --- examples/ptpython_config/config.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/examples/ptpython_config/config.py b/examples/ptpython_config/config.py index b138833f..7b2a9fd2 100644 --- a/examples/ptpython_config/config.py +++ b/examples/ptpython_config/config.py @@ -48,6 +48,9 @@ def configure(repl): # Highlight matching parethesis. repl.highlight_matching_parenthesis = True + # Line wrapping. (Instead of horizontal scrolling.) + repl.wrap_lines = True + # Complete while typing. (Don't require tab before the # completion menu is shown.) repl.complete_while_typing = True @@ -69,6 +72,10 @@ def configure(repl): # browse through the available completions instead of the history. repl.enable_history_search = False + # Enable auto suggestions. (Pressing right arrow will complete the input, + # based on the history.) + repl.enable_auto_suggest = False + # Enable open-in-editor. Pressing C-X C-E in emacs mode or 'v' in # Vi navigation mode will open the input in the current editor. repl.enable_open_in_editor = True From b294b0c7ea038b04b7bdfaf421ddbe292847995f Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sun, 20 Sep 2015 00:29:48 +0200 Subject: [PATCH 078/470] Use Transformation class in processor. (New prompt-toolkit compatibility.) --- ptpython/history_browser.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ptpython/history_browser.py b/ptpython/history_browser.py index e92d9ac8..9c6d176b 100644 --- a/ptpython/history_browser.py +++ b/ptpython/history_browser.py @@ -21,7 +21,7 @@ from prompt_toolkit.layout.processors import HighlightSearchProcessor, HighlightSelectionProcessor from prompt_toolkit.layout.screen import Char from prompt_toolkit.layout.toolbars import ArgToolbar, SearchToolbar -from prompt_toolkit.layout.processors import Processor +from prompt_toolkit.layout.processors import Processor, Transformation from prompt_toolkit.layout.utils import explode_tokens from prompt_toolkit.layout.toolbars import TokenListToolbar from prompt_toolkit.utils import Callback @@ -318,7 +318,7 @@ def __init__(self, history_mapping): self._len_before = len(history_mapping.original_document.text_before_cursor) self._len_after = len(history_mapping.original_document.text_after_cursor) - def run(self, cli, buffer, tokens): + def apply_transformation(self, cli, buffer, tokens): if self._len_before or self._len_after: tokens = explode_tokens(tokens) pos_after = len(tokens) - self._len_after @@ -326,13 +326,13 @@ def run(self, cli, buffer, tokens): text_before = ''.join(t[1] for t in tokens[:self._len_before]) text_after = ''.join(t[1] for t in tokens[pos_after:]) - return ( + return Transformation( [(Token.History.ExistingInput, text_before)] + tokens[self._len_before:pos_after] + [(Token.History.ExistingInput, text_after)] - ), lambda i: i + ) else: - return tokens, lambda i: i + return Transformation(tokens) class HistoryMapping(object): From 96bb936c086bd5459c6adc54b9b0497441150fbf Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sun, 20 Sep 2015 20:24:58 +0200 Subject: [PATCH 079/470] Added mouse support. --- ptpython/history_browser.py | 1 + ptpython/python_input.py | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/ptpython/history_browser.py b/ptpython/history_browser.py index 9c6d176b..3af466a3 100644 --- a/ptpython/history_browser.py +++ b/ptpython/history_browser.py @@ -579,6 +579,7 @@ def history_buffer_pos_changed(): }, initial_focussed_buffer=HISTORY_BUFFER, style=python_input._current_style, + mouse_support=Condition(lambda cli: python_input.enable_mouse_support), key_bindings_registry=create_key_bindings(python_input, history_mapping) ) return application diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 738bf99d..12870b37 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -172,6 +172,7 @@ def __init__(self, self.enable_system_bindings = True self.enable_input_validation = True self.enable_auto_suggest = False + self.enable_mouse_support = False self.enable_history_search = False # When True, like readline, going # back in history will filter the # history on the records starting @@ -396,6 +397,10 @@ def get_values(): 'on': lambda: enable('enable_history_search') and disable('complete_while_typing'), 'off': lambda: disable('enable_history_search'), }), + simple_option(title='Mouse support', + description='Respond to mouse clicks and scrolling for positioning the cursor, ' + 'selecting text and scrolling through windows.', + field_name='enable_mouse_support'), simple_option(title='Confirm on exit', description='Require confirmation when exiting.', field_name='confirm_exit'), @@ -496,6 +501,7 @@ def create_application(self): buffers=buffers, key_bindings_registry=self.key_bindings_registry, paste_mode=Condition(lambda cli: self.paste_mode), + mouse_support=Condition(lambda cli: self.enable_mouse_support), on_abort=AbortAction.RETRY, on_exit=self._on_exit, get_style=lambda: self._current_style, From 533b020e7459e8f5a1789b55bba4798547fe1d40 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 22 Sep 2015 06:34:02 +0200 Subject: [PATCH 080/470] A few changes for compatibility with the latest prompt-toolkit. --- ptpython/history_browser.py | 14 +++++++------- ptpython/layout.py | 11 +++++++++-- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/ptpython/history_browser.py b/ptpython/history_browser.py index 3af466a3..cdacd48f 100644 --- a/ptpython/history_browser.py +++ b/ptpython/history_browser.py @@ -179,7 +179,7 @@ def create_layout(python_input, history_mapping): content=BufferControl( buffer_name=DEFAULT_BUFFER, wrap_lines=False, - input_processors=default_processors + [GrayExistingText(history_mapping)], + input_processors=[GrayExistingText(history_mapping)] + default_processors, lexer=PygmentsLexer(PythonLexer)), left_margins=[ResultMargin(history_mapping)], scroll_offsets=ScrollOffsets(top=2, bottom=2)), @@ -318,7 +318,7 @@ def __init__(self, history_mapping): self._len_before = len(history_mapping.original_document.text_before_cursor) self._len_after = len(history_mapping.original_document.text_after_cursor) - def apply_transformation(self, cli, buffer, tokens): + def apply_transformation(self, cli, document, tokens): if self._len_before or self._len_after: tokens = explode_tokens(tokens) pos_after = len(tokens) - self._len_after @@ -327,12 +327,12 @@ def apply_transformation(self, cli, buffer, tokens): text_after = ''.join(t[1] for t in tokens[pos_after:]) return Transformation( - [(Token.History.ExistingInput, text_before)] + - tokens[self._len_before:pos_after] + - [(Token.History.ExistingInput, text_after)] - ) + document=document, + tokens=explode_tokens([(Token.History.ExistingInput, text_before)] + + tokens[self._len_before:pos_after] + + [(Token.History.ExistingInput, text_after)])) else: - return Transformation(tokens) + return Transformation(document, tokens) class HistoryMapping(object): diff --git a/ptpython/layout.py b/ptpython/layout.py index dad90d55..c17e4454 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -97,9 +97,16 @@ def append(selected, label, status): return tokens + class Control(TokenListControl): + def move_cursor_down(self, cli): + python_input.selected_option_index += 1 + + def move_cursor_up(self, cli): + python_input.selected_option_index -= 1 + return ConditionalContainer( content=Window( - TokenListControl(get_tokens, Char(token=Token.Sidebar), + Control(get_tokens, Char(token=Token.Sidebar), has_focus=ShowSidebar(python_input) & ~IsDone()), width=LayoutDimension.exact(43), height=LayoutDimension(min=3), @@ -259,7 +266,7 @@ def create_margin(self, cli, window_render_info, width, height): for y in range(1, min(window_render_info.content_height, height)): tokens.append((Token, '\n')) if show_numbers: - line_number = visible_line_to_input_line.get(y) + line_number = visible_line_to_input_line.get(y) or 0 tokens.append((Token.LineNumber, ('%i ' % (line_number + 1)).rjust(width))) else: tokens.extend(tokens2) From 3711b2a72daf37a6bd85cfa806b157c7657857b9 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 22 Sep 2015 06:50:22 +0200 Subject: [PATCH 081/470] Commented weird defaults in example configuration. --- examples/ptpython_config/config.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/examples/ptpython_config/config.py b/examples/ptpython_config/config.py index 7b2a9fd2..8271a8d1 100644 --- a/examples/ptpython_config/config.py +++ b/examples/ptpython_config/config.py @@ -95,8 +95,10 @@ def configure(repl): repl.use_code_colorscheme('pastie') # Install custom colorscheme named 'my-colorscheme' and use it. + """ repl.install_ui_colorscheme('my-colorscheme', _custom_ui_colorscheme) repl.use_ui_colorscheme('my-colorscheme') + """ # Add custom key binding for PDB. @repl.add_key_binding(Keys.ControlB) @@ -112,6 +114,7 @@ def _(event): if b.accept_action.is_returnable: b.accept_action.validate_and_handle(event.cli, b) + """ # Custom key binding for some simple autocorrection while typing. corrections = { 'impotr': 'import', @@ -130,6 +133,7 @@ def _(event): b.insert_text(corrections[w]) b.insert_text(' ') + """ # Custom colorscheme for the UI. See `ptpython/layout.py` and From 79c24fe25a8d6bfe18543caf0ef8ea8ab2a3cc1d Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 22 Sep 2015 06:50:35 +0200 Subject: [PATCH 082/470] Added 'enable_mouse_support' option to example configuration. --- examples/ptpython_config/config.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/examples/ptpython_config/config.py b/examples/ptpython_config/config.py index 8271a8d1..ddcda9bd 100644 --- a/examples/ptpython_config/config.py +++ b/examples/ptpython_config/config.py @@ -51,6 +51,9 @@ def configure(repl): # Line wrapping. (Instead of horizontal scrolling.) repl.wrap_lines = True + # Mouse support. + repl.enable_mouse_support = True + # Complete while typing. (Don't require tab before the # completion menu is shown.) repl.complete_while_typing = True From edfb628ef162881c52777c3365d2ae0e554dba43 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 24 Sep 2015 03:44:10 +0200 Subject: [PATCH 083/470] Correctly exit cli, using set_exit when typing Ctrl-Z. --- ptpython/repl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ptpython/repl.py b/ptpython/repl.py index 5f89b22b..e8c8f002 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -96,7 +96,7 @@ def compile_with_flags(code, mode): if line.lstrip().startswith('\x1a'): # When the input starts with Ctrl-Z, quit the REPL. - cli.set_return_value(None) + cli.set_exit() elif line.lstrip().startswith('!'): # Run as shell command From a4f69351f59c0c236e1494aa2e437701e2b20c21 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 24 Sep 2015 03:44:26 +0200 Subject: [PATCH 084/470] Enable search/abort/exit bindings again. (Disabled by default in new prompt-toolkit. --- ptpython/python_input.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 12870b37..359d6802 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -222,6 +222,8 @@ def __init__(self, # Use a KeyBindingManager for loading the key bindings. self.key_bindings_manager = KeyBindingManager( + enable_abort_and_exit_bindings=True, + enable_search=True, enable_vi_mode=Condition(lambda cli: self.vi_mode), enable_open_in_editor=Condition(lambda cli: self.enable_open_in_editor), enable_system_bindings=Condition(lambda cli: self.enable_system_bindings), From d4f96c7d8f4a8869844f0852b5ae4cf50ee33396 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Wed, 23 Sep 2015 20:28:42 +0200 Subject: [PATCH 085/470] Fixed typo in help text. --- ptpython/python_input.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 359d6802..6a6bd940 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -411,7 +411,7 @@ def get_values(): 'instead of showing a traceback of a SyntaxError.', field_name='enable_input_validation'), simple_option(title='Auto suggestion', - description='Auto suggest inputs by looking at the history.' + description='Auto suggest inputs by looking at the history. ' 'Pressing right arrow or Ctrl-E will complete the entry.', field_name='enable_auto_suggest'), Option(title='Accept input on enter', From d72adfa5dec8f9263083a149bfd7ffbce614fa19 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 24 Sep 2015 04:33:56 +0200 Subject: [PATCH 086/470] Upgrade to prompt_toolkit 0.51. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index d1f6becc..ac6f47fe 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ long_description=long_description, packages=find_packages('.'), install_requires = [ - 'prompt_toolkit==0.50', + 'prompt_toolkit==0.51', 'jedi>=0.9.0', 'docopt', ], From 459124a75c6d85d468f26ab213ac48cfd490c5f6 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 24 Sep 2015 04:38:29 +0200 Subject: [PATCH 087/470] Pypi release 0.23. --- CHANGELOG | 12 ++++++++++++ setup.py | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 69881fc7..9c7a056d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,18 @@ CHANGELOG ========= +0.23: 2015-09-24 +---------------- + +Upgrade to prompt_toolkit 0.51 + +New features: +- Mouse support +- Fish style auto suggestion. +- Optionally disabling of line wraps. +- Use Python3Lexer for Python 3. + + 0.22: 2015-09-06 ---------------- diff --git a/setup.py b/setup.py index ac6f47fe..48d8d6bc 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ setup( name='ptpython', author='Jonathan Slenders', - version='0.22', + version='0.23', url='https://github.com/jonathanslenders/ptpython', description='Python REPL build on top of prompt_toolkit', long_description=long_description, From 8a10e56b7eafcbbcf4aebd26735466ba14bdd18d Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 24 Sep 2015 05:04:12 +0200 Subject: [PATCH 088/470] Hide auto suggestion after accepting the input. (On pressing enter.) --- ptpython/layout.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ptpython/layout.py b/ptpython/layout.py index c17e4454..a10ffd62 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -475,7 +475,9 @@ def menu_position(cli): processor=HighlightSearchProcessor(preview_search=Always()), filter=HasFocus(SEARCH_BUFFER)), HighlightSelectionProcessor(), - AppendAutoSuggestion(), + ConditionalProcessor( + processor=AppendAutoSuggestion(), + filter=~IsDone()) ] + extra_buffer_processors, menu_position=menu_position, wrap_lines=Condition(lambda cli: python_input.wrap_lines), From b4c9aa4d952c65a6ec4c00fd92254b3de992cd0f Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 24 Sep 2015 05:13:42 +0200 Subject: [PATCH 089/470] Pypi release 0.24 --- CHANGELOG | 6 ++++++ setup.py | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 9c7a056d..6eeb1c92 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,12 @@ CHANGELOG ========= +0.24: 2015-09-24 +---------------- + +Upgrade to prompt_toolkit 0.52 + + 0.23: 2015-09-24 ---------------- diff --git a/setup.py b/setup.py index 48d8d6bc..d8956290 100644 --- a/setup.py +++ b/setup.py @@ -13,13 +13,13 @@ setup( name='ptpython', author='Jonathan Slenders', - version='0.23', + version='0.24', url='https://github.com/jonathanslenders/ptpython', description='Python REPL build on top of prompt_toolkit', long_description=long_description, packages=find_packages('.'), install_requires = [ - 'prompt_toolkit==0.51', + 'prompt_toolkit==0.52', 'jedi>=0.9.0', 'docopt', ], From 622fb5cdf4e8c6d929f9efd31300f3452cb2dbf0 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 24 Sep 2015 06:16:32 +0200 Subject: [PATCH 090/470] Enable search bindings in history browser. --- ptpython/history_browser.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ptpython/history_browser.py b/ptpython/history_browser.py index cdacd48f..d494357c 100644 --- a/ptpython/history_browser.py +++ b/ptpython/history_browser.py @@ -403,6 +403,7 @@ def create_key_bindings(python_input, history_mapping): Key bindings. """ manager = KeyBindingManager( + enable_search=True, enable_vi_mode=Condition(lambda cli: python_input.vi_mode), enable_extra_page_navigation=True, vi_state=python_input.key_bindings_manager.vi_state) From 7363ae2def12b6853ee83eecc1143e50fd130dd5 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Wed, 7 Oct 2015 20:04:33 +0200 Subject: [PATCH 091/470] Handle bug in Jedi. --- ptpython/utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ptpython/utils.py b/ptpython/utils.py index 549b33d5..7b7291dc 100644 --- a/ptpython/utils.py +++ b/ptpython/utils.py @@ -64,6 +64,9 @@ def get_jedi_script_from_document(document, locals, globals): except KeyError: # Workaroud for a crash when the input is "u'", the start of a unicode string. return None + except jedi.ParseError: + # Workaround for: ParseError: incomplete input: type='ENDMARKER', value='', start_pos=(4, 0) + return None _multiline_string_delims = re.compile('''[']{3}|["]{3}''') From 06105153f0d38b90b6953ad5f9b1db97d21893b2 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Wed, 7 Oct 2015 20:05:00 +0200 Subject: [PATCH 092/470] Correctly consider input multiline when there's a colon at the end of the line. --- ptpython/utils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ptpython/utils.py b/ptpython/utils.py index 7b7291dc..22d5e68a 100644 --- a/ptpython/utils.py +++ b/ptpython/utils.py @@ -92,8 +92,11 @@ def ends_in_multiline_string(): if '\n' in document.text or ends_in_multiline_string(): return True + def line_ends_with_colon(): + return document.current_line.rstrip()[-1:] == ':' + # If we just typed a colon, or still have open brackets, always insert a real newline. - if document.text_before_cursor.rstrip()[-1:] == ':' or \ + if line_ends_with_colon() or \ (document.is_cursor_at_the_end and has_unclosed_brackets(document.text_before_cursor)) or \ document.text.startswith('@'): From a16d6c3215062662b555ed28eebc08e72ff50de1 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Wed, 14 Oct 2015 18:39:07 +0200 Subject: [PATCH 093/470] Use DEFAULT_BUFFER enum. --- ptpython/repl.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ptpython/repl.py b/ptpython/repl.py index e8c8f002..856c4541 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -17,6 +17,7 @@ from prompt_toolkit.layout.utils import token_list_width from prompt_toolkit.shortcuts import create_asyncio_eventloop from prompt_toolkit.utils import DummyContext, Callback +from prompt_toolkit.enums import DEFAULT_BUFFER from .python_input import PythonInput from .eventloop import create_eventloop @@ -79,7 +80,7 @@ def _process_document(self, cli, buffer): # Append to history and reset. cli.search_state.text = '' - cli.buffers['default'].reset(append_to_history=True) + cli.buffers[DEFAULT_BUFFER].reset(append_to_history=True) self.key_bindings_manager.reset() def _execute(self, cli, line): From 27a19e2b3652b9e42103f8cb027e7e94f9836b9d Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Fri, 16 Oct 2015 22:02:03 +0200 Subject: [PATCH 094/470] Using DynamicStyle and Container: changes for the latest prompt-toolkit. --- ptpython/history_browser.py | 7 ++++--- ptpython/python_input.py | 5 +++-- ptpython/repl.py | 3 ++- ptpython/style.py | 4 ++-- setup.py | 2 +- 5 files changed, 12 insertions(+), 9 deletions(-) diff --git a/ptpython/history_browser.py b/ptpython/history_browser.py index d494357c..0fa16fcf 100644 --- a/ptpython/history_browser.py +++ b/ptpython/history_browser.py @@ -13,7 +13,7 @@ from prompt_toolkit.filters import Always, Condition, HasFocus, InFocusStack from prompt_toolkit.key_binding.manager import KeyBindingManager from prompt_toolkit.keys import Keys -from prompt_toolkit.layout.containers import HSplit, VSplit, Window, FloatContainer, Float, ConditionalContainer, Layout, ScrollOffsets +from prompt_toolkit.layout.containers import HSplit, VSplit, Window, FloatContainer, Float, ConditionalContainer, Container, ScrollOffsets from prompt_toolkit.layout.controls import BufferControl, FillControl from prompt_toolkit.layout.dimension import LayoutDimension as D from prompt_toolkit.layout.lexers import PygmentsLexer @@ -104,7 +104,7 @@ def create_popup_window(title, body): the `title` text, and a body layout. The window is surrounded by borders. """ assert isinstance(title, six.text_type) - assert isinstance(body, Layout) + assert isinstance(body, Container) return HSplit([ VSplit([ @@ -137,7 +137,8 @@ def create_popup_window(title, body): def create_layout(python_input, history_mapping): """ - Create and return a `Layout` instance for the history application. + Create and return a `Container` instance for the history + application. """ default_processors = [ HighlightSearchProcessor(preview_search=Always()), diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 6a6bd940..efbca28c 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -10,6 +10,7 @@ from __future__ import unicode_literals from prompt_toolkit import AbortAction +from prompt_toolkit.auto_suggest import AutoSuggestFromHistory, ConditionalAutoSuggest from prompt_toolkit.buffer import Buffer from prompt_toolkit.document import Document from prompt_toolkit.enums import DEFAULT_BUFFER @@ -18,9 +19,9 @@ from prompt_toolkit.interface import CommandLineInterface, Application, AcceptAction from prompt_toolkit.key_binding.manager import KeyBindingManager from prompt_toolkit.layout.lexers import PygmentsLexer +from prompt_toolkit.styles import DynamicStyle from prompt_toolkit.utils import Callback, is_windows from prompt_toolkit.validation import ConditionalValidator -from prompt_toolkit.auto_suggest import AutoSuggestFromHistory, ConditionalAutoSuggest from ptpython.completer import PythonCompleter from ptpython.key_bindings import load_python_bindings, load_sidebar_bindings, load_confirm_exit_bindings @@ -506,7 +507,7 @@ def create_application(self): mouse_support=Condition(lambda cli: self.enable_mouse_support), on_abort=AbortAction.RETRY, on_exit=self._on_exit, - get_style=lambda: self._current_style, + style=DynamicStyle(lambda: self._current_style), get_title=lambda: self.terminal_title, on_start=self._on_start, on_input_timeout=Callback(self._on_input_timeout)) diff --git a/ptpython/repl.py b/ptpython/repl.py index 856c4541..d82de251 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -18,6 +18,7 @@ from prompt_toolkit.shortcuts import create_asyncio_eventloop from prompt_toolkit.utils import DummyContext, Callback from prompt_toolkit.enums import DEFAULT_BUFFER +from prompt_toolkit.styles import PygmentsStyle from .python_input import PythonInput from .eventloop import create_eventloop @@ -162,7 +163,7 @@ def _handle_exception(cls, cli, e): # (We use the default style. Most other styles result # in unreadable colors for the traceback.) tokens = _lex_python_traceback(tb) - cli.print_tokens(tokens, style=DefaultStyle) + cli.print_tokens(tokens, style=PygmentsStyle(DefaultStyle)) output.write('%s\n\n' % e) output.flush() diff --git a/ptpython/style.py b/ptpython/style.py index d2043025..0eb5b647 100644 --- a/ptpython/style.py +++ b/ptpython/style.py @@ -3,7 +3,7 @@ from pygments.token import Token, Keyword, Name, Comment, String, Operator, Number from pygments.style import Style from pygments.styles import get_style_by_name, get_all_styles -from prompt_toolkit.styles import default_style_extensions +from prompt_toolkit.styles import default_style_extensions, PygmentsStyle from prompt_toolkit.utils import is_windows, is_conemu_ansi __all__ = ( @@ -46,7 +46,7 @@ class PythonStyle(Style): styles.update(python_style) styles.update(ui_style) - return PythonStyle + return PygmentsStyle(PythonStyle) # Code style for Windows consoles. They support only 16 colors, diff --git a/setup.py b/setup.py index d8956290..fd89266f 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ long_description=long_description, packages=find_packages('.'), install_requires = [ - 'prompt_toolkit==0.52', + 'prompt_toolkit==0.53', 'jedi>=0.9.0', 'docopt', ], From eed3a5eb9d13c621ba58d86ebb52e8ea8fa94866 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 29 Oct 2015 20:44:58 +0100 Subject: [PATCH 095/470] By default: hide signatures and parenthesis highlighting. --- ptpython/python_input.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ptpython/python_input.py b/ptpython/python_input.py index efbca28c..98c0b939 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -154,7 +154,7 @@ def __init__(self, self._extra_buffer_processors = _extra_buffer_processors or [] # Settings. - self.show_signature = True + self.show_signature = False self.show_docstring = False self.show_meta_enter_message = True self.completion_visualisation = CompletionVisualisation.MULTI_COLUMN @@ -179,7 +179,7 @@ def __init__(self, # history on the records starting # with the current input. - self.highlight_matching_parenthesis = True + self.highlight_matching_parenthesis = False self.show_sidebar = False # Currently show the sidebar. self.show_sidebar_help = True # When the sidebar is visible, also show the help text. self.show_exit_confirmation = False # Currently show 'Do you really want to exit?' From de87d13a7b92629ef723e9c89eff3f198227a884 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 29 Oct 2015 20:45:29 +0100 Subject: [PATCH 096/470] Upgrade to prompt-toolkit 0.54. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index fd89266f..79223c39 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ long_description=long_description, packages=find_packages('.'), install_requires = [ - 'prompt_toolkit==0.53', + 'prompt_toolkit==0.54', 'jedi>=0.9.0', 'docopt', ], From 172b6d2898faf862433f9c24bd24ffee37c6f184 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 29 Oct 2015 20:52:27 +0100 Subject: [PATCH 097/470] Release 0.25 --- CHANGELOG | 11 +++++++++++ setup.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 6eeb1c92..ca39cf2f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,17 @@ CHANGELOG ========= +0.25: 2015-10-29 +---------------- + +Upgrade to prompt_toolkit 0.54 + +Fixes: +- Consider input multiline when there's a colon at the end of the line. +- Handle bug in Jedi. +- Enable search bindings in history browser. + + 0.24: 2015-09-24 ---------------- diff --git a/setup.py b/setup.py index 79223c39..b4a2687c 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ setup( name='ptpython', author='Jonathan Slenders', - version='0.24', + version='0.25', url='https://github.com/jonathanslenders/ptpython', description='Python REPL build on top of prompt_toolkit', long_description=long_description, From 1e20081f112b6741f58dc7dd72fb8d87a0dc2ff0 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Wed, 18 Nov 2015 19:44:35 +0100 Subject: [PATCH 098/470] Renamed cli.set_exit to cli.exit (Change in prompt-toolkit.) --- ptpython/key_bindings.py | 2 +- ptpython/repl.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ptpython/key_bindings.py b/ptpython/key_bindings.py index 38bf1dec..0ebb3ae2 100644 --- a/ptpython/key_bindings.py +++ b/ptpython/key_bindings.py @@ -204,7 +204,7 @@ def _(event): """ Really quit. """ - event.cli.set_exit() + event.cli.exit() @handle(Keys.Any, filter=confirmation_visible) def _(event): diff --git a/ptpython/repl.py b/ptpython/repl.py index d82de251..61bb90b2 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -98,7 +98,7 @@ def compile_with_flags(code, mode): if line.lstrip().startswith('\x1a'): # When the input starts with Ctrl-Z, quit the REPL. - cli.set_exit() + cli.exit() elif line.lstrip().startswith('!'): # Run as shell command From 42ab82463741053ebfb4d65a7321037b337d5766 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Wed, 18 Nov 2015 19:45:05 +0100 Subject: [PATCH 099/470] Display prompt-toolkit options italic. --- ptpython/style.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ptpython/style.py b/ptpython/style.py index 0eb5b647..e318a24e 100644 --- a/ptpython/style.py +++ b/ptpython/style.py @@ -126,7 +126,7 @@ class PythonStyle(Style): # The options sidebar. Token.Sidebar: 'bg:#bbbbbb #000000', Token.Sidebar.Title: 'bg:#668866 #ffffff', - Token.Sidebar.Label: 'bg:#bbbbbb #222222', + Token.Sidebar.Label: 'bg:#bbbbbb #222222 italic', Token.Sidebar.Status: 'bg:#dddddd #000011', Token.Sidebar.Selected.Label: 'bg:#222222 #eeeeee', Token.Sidebar.Selected.Status: 'bg:#444444 #ffffff bold', From 0f9f701a36af87c1865697dae6872fb4d1d962f6 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Wed, 25 Nov 2015 18:30:07 +0100 Subject: [PATCH 100/470] Remove italic from styling, it isn't displayed very well by default on all terminals. --- ptpython/style.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ptpython/style.py b/ptpython/style.py index e318a24e..0eb5b647 100644 --- a/ptpython/style.py +++ b/ptpython/style.py @@ -126,7 +126,7 @@ class PythonStyle(Style): # The options sidebar. Token.Sidebar: 'bg:#bbbbbb #000000', Token.Sidebar.Title: 'bg:#668866 #ffffff', - Token.Sidebar.Label: 'bg:#bbbbbb #222222 italic', + Token.Sidebar.Label: 'bg:#bbbbbb #222222', Token.Sidebar.Status: 'bg:#dddddd #000011', Token.Sidebar.Selected.Label: 'bg:#222222 #eeeeee', Token.Sidebar.Selected.Status: 'bg:#444444 #ffffff bold', From 6146d92edd9ee55a9505a5720368f58fcc5b140e Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Mon, 7 Dec 2015 20:43:16 +0100 Subject: [PATCH 101/470] Handle mouse click events on the input mode, paste mode and the sidebar button. --- ptpython/layout.py | 46 +++++++++++++++++++++++++++++++++------------- 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/ptpython/layout.py b/ptpython/layout.py index a10ffd62..2230d9a8 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -16,6 +16,7 @@ from prompt_toolkit.layout.screen import Char from prompt_toolkit.layout.toolbars import CompletionsToolbar, ArgToolbar, SearchToolbar, ValidationToolbar, SystemToolbar, TokenListToolbar from prompt_toolkit.layout.utils import token_list_width +from prompt_toolkit.mouse_events import MouseEventTypes from prompt_toolkit.reactive import Integer from prompt_toolkit.selection import SelectionType from prompt_toolkit.utils import get_cwidth @@ -280,6 +281,12 @@ def status_bar(key_bindings_manager, python_input): """ TB = Token.Toolbar.Status + def toggle_paste_mode(cli, mouse_event): + if mouse_event.event_type == MouseEventTypes.MOUSE_DOWN: + python_input.paste_mode = not python_input.paste_mode + else: + return NotImplemented + def get_tokens(cli): python_buffer = cli.buffers[DEFAULT_BUFFER] @@ -304,14 +311,14 @@ def get_tokens(cli): result.extend([ (TB.Key, '[F3]'), (TB, ' History '), - (TB.Key, '[F6]'), - (TB, ' '), + (TB.Key, '[F6]', toggle_paste_mode), + (TB, ' ', toggle_paste_mode), ]) if python_input.paste_mode: - append((TB.PasteModeOn, 'Paste mode (on)')) + append((TB.PasteModeOn, 'Paste mode (on)', toggle_paste_mode)) else: - append((TB, 'Paste mode')) + append((TB, 'Paste mode', toggle_paste_mode)) return result @@ -330,33 +337,39 @@ def get_inputmode_tokens(cli, python_input): :param cli: `CommandLineInterface` instance. """ + def toggle_vi_mode(cli, mouse_event): + if mouse_event.event_type == MouseEventTypes.MOUSE_DOWN: + python_input.vi_mode = not python_input.vi_mode + else: + return NotImplemented + token = Token.Toolbar.Status mode = python_input.key_bindings_manager.vi_state.input_mode result = [] append = result.append - append((token.InputMode, '[F4] ')) + append((token.InputMode, '[F4] ', toggle_vi_mode)) # InputMode if python_input.vi_mode: if bool(cli.current_buffer.selection_state): if cli.current_buffer.selection_state.type == SelectionType.LINES: - append((token.InputMode, 'Vi (VISUAL LINE)')) + append((token.InputMode, 'Vi (VISUAL LINE)', toggle_vi_mode)) elif cli.current_buffer.selection_state.type == SelectionType.CHARACTERS: - append((token.InputMode, 'Vi (VISUAL)')) + append((token.InputMode, 'Vi (VISUAL)', toggle_vi_mode)) append((token, ' ')) elif mode == InputMode.INSERT: - append((token.InputMode, 'Vi (INSERT)')) + append((token.InputMode, 'Vi (INSERT)', toggle_vi_mode)) append((token, ' ')) elif mode == InputMode.NAVIGATION: - append((token.InputMode, 'Vi (NAV)')) + append((token.InputMode, 'Vi (NAV)', toggle_vi_mode)) append((token, ' ')) elif mode == InputMode.REPLACE: - append((token.InputMode, 'Vi (REPLACE)')) + append((token.InputMode, 'Vi (REPLACE)', toggle_vi_mode)) append((token, ' ')) else: - append((token.InputMode, 'Emacs')) + append((token.InputMode, 'Emacs', toggle_vi_mode)) append((token, ' ')) return result @@ -367,12 +380,19 @@ def show_sidebar_button_info(python_input): Create `Layout` for the information in the right-bottom corner. (The right part of the status bar.) """ + def toggle_sidebar(cli, mouse_event): + " Click handler for the menu. " + if mouse_event.event_type == MouseEventTypes.MOUSE_DOWN: + python_input.show_sidebar = not python_input.show_sidebar + else: + return NotImplemented + token = Token.Toolbar.Status version = sys.version_info tokens = [ - (token.Key, '[F2]'), - (token, ' Menu'), + (token.Key, '[F2]', toggle_sidebar), + (token, ' Menu', toggle_sidebar), (token, ' - '), (token.PythonVersion, '%s %i.%i.%i' % (platform.python_implementation(), version[0], version[1], version[2])), From de41264c8591e804344ff4a4c215a385ae0341ff Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sat, 19 Dec 2015 05:08:34 +0100 Subject: [PATCH 102/470] Compatibility with prompt_toolkit: get_vi_state API change. --- ptpython/key_bindings.py | 2 +- ptpython/layout.py | 2 +- ptpython/repl.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ptpython/key_bindings.py b/ptpython/key_bindings.py index 0ebb3ae2..d2713d96 100644 --- a/ptpython/key_bindings.py +++ b/ptpython/key_bindings.py @@ -94,7 +94,7 @@ def _(event): @handle(Keys.ControlJ, filter= ~sidebar_visible & ~has_selection & ~(vi_mode_enabled & - ViStateFilter(key_bindings_manager.vi_state, InputMode.NAVIGATION)) & + ViStateFilter(key_bindings_manager.get_vi_state, InputMode.NAVIGATION)) & HasFocus(DEFAULT_BUFFER) & IsMultiline()) def _(event): """ diff --git a/ptpython/layout.py b/ptpython/layout.py index 2230d9a8..6dc275fc 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -345,7 +345,7 @@ def toggle_vi_mode(cli, mouse_event): token = Token.Toolbar.Status - mode = python_input.key_bindings_manager.vi_state.input_mode + mode = python_input.key_bindings_manager.get_vi_state(cli).input_mode result = [] append = result.append diff --git a/ptpython/repl.py b/ptpython/repl.py index 61bb90b2..dd66ba51 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -82,7 +82,7 @@ def _process_document(self, cli, buffer): # Append to history and reset. cli.search_state.text = '' cli.buffers[DEFAULT_BUFFER].reset(append_to_history=True) - self.key_bindings_manager.reset() + self.key_bindings_manager.reset(cli) def _execute(self, cli, line): """ From 134808efe8d168e78bd74da1ffd30e1258961e72 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 22 Dec 2015 06:31:09 +0100 Subject: [PATCH 103/470] Bugfix in get_vi_mode (from previous commit). --- ptpython/key_bindings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ptpython/key_bindings.py b/ptpython/key_bindings.py index d2713d96..09dd68cb 100644 --- a/ptpython/key_bindings.py +++ b/ptpython/key_bindings.py @@ -60,13 +60,13 @@ def _(event): """ Select from the history. """ - python_input.key_bindings_manager.vi_state.input_mode = InputMode.NAVIGATION + python_input.key_bindings_manager.get_vi_state(event.cli).input_mode = InputMode.NAVIGATION def done(result): if result is not None: event.cli.buffers[DEFAULT_BUFFER].document = result - python_input.key_bindings_manager.vi_state.input_mode = InputMode.INSERT + python_input.key_bindings_manager.get_vi_state(event.cli).input_mode = InputMode.INSERT event.cli.run_sub_application(create_history_application( python_input, event.cli.buffers[DEFAULT_BUFFER].document), done) From 9c3ead8f4682788dc1fa8227f58f7cd3ab9f0ad7 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 22 Dec 2015 06:31:31 +0100 Subject: [PATCH 104/470] Use Search/Selection Highlighter for better performance. --- ptpython/layout.py | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/ptpython/layout.py b/ptpython/layout.py index 6dc275fc..a9068121 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -12,7 +12,8 @@ from prompt_toolkit.layout.lexers import SimpleLexer from prompt_toolkit.layout.margins import Margin from prompt_toolkit.layout.menus import CompletionsMenu, MultiColumnCompletionsMenu -from prompt_toolkit.layout.processors import HighlightSearchProcessor, HighlightSelectionProcessor, HighlightMatchingBracketProcessor, ConditionalProcessor, AppendAutoSuggestion +from prompt_toolkit.layout.highlighters import SearchHighlighter, SelectionHighlighter, MatchingBracketHighlighter, ConditionalHighlighter +from prompt_toolkit.layout.processors import HighlightMatchingBracketProcessor, ConditionalProcessor, AppendAutoSuggestion from prompt_toolkit.layout.screen import Char from prompt_toolkit.layout.toolbars import CompletionsToolbar, ArgToolbar, SearchToolbar, ValidationToolbar, SystemToolbar, TokenListToolbar from prompt_toolkit.layout.utils import token_list_width @@ -485,20 +486,22 @@ def menu_position(cli): BufferControl( buffer_name=DEFAULT_BUFFER, lexer=lexer, + highlighters=[ + # Show matching parentheses, but only while editing. + ConditionalHighlighter( + highlighter=MatchingBracketHighlighter(chars='[](){}'), + filter=HasFocus(DEFAULT_BUFFER) & ~IsDone() & + Condition(lambda cli: python_input.highlight_matching_parenthesis)), + ConditionalHighlighter( + highlighter=SearchHighlighter(preview_search=Always()), + filter=HasFocus(SEARCH_BUFFER)), + SelectionHighlighter(), + ], input_processors=[ - # Show matching parentheses, but only while editing. - ConditionalProcessor( - processor=HighlightMatchingBracketProcessor(chars='[](){}'), - filter=HasFocus(DEFAULT_BUFFER) & ~IsDone() & - Condition(lambda cli: python_input.highlight_matching_parenthesis)), - ConditionalProcessor( - processor=HighlightSearchProcessor(preview_search=Always()), - filter=HasFocus(SEARCH_BUFFER)), - HighlightSelectionProcessor(), - ConditionalProcessor( - processor=AppendAutoSuggestion(), - filter=~IsDone()) - ] + extra_buffer_processors, + ConditionalProcessor( + processor=AppendAutoSuggestion(), + filter=~IsDone()) + ] + extra_buffer_processors, menu_position=menu_position, wrap_lines=Condition(lambda cli: python_input.wrap_lines), From c09b000f1afb9bc8205d53184a96e4c9e8de2d4f Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 31 Dec 2015 01:02:40 +0100 Subject: [PATCH 105/470] Bug fix in history_browser: compatibility with latest prompt_toolkit. --- ptpython/history_browser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ptpython/history_browser.py b/ptpython/history_browser.py index 0fa16fcf..1458d0b6 100644 --- a/ptpython/history_browser.py +++ b/ptpython/history_browser.py @@ -407,7 +407,7 @@ def create_key_bindings(python_input, history_mapping): enable_search=True, enable_vi_mode=Condition(lambda cli: python_input.vi_mode), enable_extra_page_navigation=True, - vi_state=python_input.key_bindings_manager.vi_state) + get_vi_state=python_input.key_bindings_manager.get_vi_state) handle = manager.registry.add_binding @handle(' ', filter=HasFocus(HISTORY_BUFFER)) From 217c59f4ce9ca89085ce7d7b9a35a32315e7e393 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 31 Dec 2015 05:12:13 +0100 Subject: [PATCH 106/470] Handle KeyError in Jedi completions() call. (Work-around for Jedi bug.) --- ptpython/completer.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ptpython/completer.py b/ptpython/completer.py index 0ac8f743..7e3e71de 100644 --- a/ptpython/completer.py +++ b/ptpython/completer.py @@ -139,6 +139,10 @@ def get_completions(self, document, complete_event): except ValueError: # Jedi issue: "ValueError: invalid \x escape" pass + except KeyError: + # Jedi issue: "KeyError: u'a_lambda'." + # https://github.com/jonathanslenders/ptpython/issues/89 + pass else: for c in completions: yield Completion(c.name_with_symbols, len(c.complete) - len(c.name_with_symbols), From a32e44c17fcc7e10624c3476fe1042779c587bdc Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 31 Dec 2015 05:17:31 +0100 Subject: [PATCH 107/470] Take compiler flags into account for ptipython. --- ptpython/ipython.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ptpython/ipython.py b/ptpython/ipython.py index e338edaa..d94e7437 100644 --- a/ptpython/ipython.py +++ b/ptpython/ipython.py @@ -161,7 +161,8 @@ def __init__(self, ipython_shell, *a, **kw): ipython_shell.magics_manager, ipython_shell.alias_manager) kw['_lexer'] = create_lexer() - kw['_validator'] = IPythonValidator() + kw['_validator'] = IPythonValidator( + get_compiler_flags=self.get_compiler_flags) super(IPythonInput, self).__init__(*a, **kw) self.ipython_shell = ipython_shell From fe1530aa11b5c7fd76ef7d16adf4d950a4fd4570 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 31 Dec 2015 05:36:26 +0100 Subject: [PATCH 108/470] Correctly handle sys.argv when pt(i)python is started with the --interactive option. --- ptpython/entry_points/run_ptipython.py | 9 +++++---- ptpython/entry_points/run_ptpython.py | 9 +++++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/ptpython/entry_points/run_ptipython.py b/ptpython/entry_points/run_ptipython.py index 799f8e32..8fbba170 100644 --- a/ptpython/entry_points/run_ptipython.py +++ b/ptpython/entry_points/run_ptipython.py @@ -4,7 +4,7 @@ Usage: ptpython [ --vi ] [ --config-dir= ] [ --interactive= ] - [--] [ ... ] + [--] [ ... ] ptpython -h | --help Options: @@ -46,9 +46,9 @@ def run(): sys.path.insert(0, '') # When a file has been given, run that, otherwise start the shell. - if a['']: - sys.argv = [a['']] + a[''] - six.exec_(compile(open(a[''], "rb").read(), a[''], 'exec')) + if a[''] and not a['--interactive']: + sys.argv = a[''] + six.exec_(compile(open(a[''][0], "rb").read(), a[''][0], 'exec')) else: enable_deprecation_warnings() @@ -60,6 +60,7 @@ def run(): # --interactive if a['--interactive']: path = a['--interactive'] + sys.argv = [a['--interactive']] + a[''] if os.path.exists(path): with open(path, 'r') as f: diff --git a/ptpython/entry_points/run_ptpython.py b/ptpython/entry_points/run_ptpython.py index 712f44eb..67e9c072 100644 --- a/ptpython/entry_points/run_ptpython.py +++ b/ptpython/entry_points/run_ptpython.py @@ -4,7 +4,7 @@ Usage: ptpython [ --vi ] [ --config-dir= ] [ --interactive= ] - [--] [ ... ] + [--] [ ... ] ptpython -h | --help Options: @@ -43,15 +43,16 @@ def run(): # --interactive if a['--interactive']: startup_paths.append(a['--interactive']) + sys.argv = [a['--interactive']] + a[''] # Add the current directory to `sys.path`. if sys.path[0] != '': sys.path.insert(0, '') # When a file has been given, run that, otherwise start the shell. - if a['']: - sys.argv = [a['']] + a[''] - six.exec_(compile(open(a[''], "rb").read(), a[''], 'exec')) + if a[''] and not a['--interactive']: + sys.argv = a[''] + six.exec_(compile(open(a[''][0], "rb").read(), a[''][0], 'exec')) # Run interactive shell. else: From 0fa994e6c555091ebd781d478fad189edb61bde8 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 31 Dec 2015 05:44:29 +0100 Subject: [PATCH 109/470] Handle IOError in Jedi completions() call. (Work-around for Jedi bug.) --- ptpython/completer.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ptpython/completer.py b/ptpython/completer.py index 7e3e71de..6be64bef 100644 --- a/ptpython/completer.py +++ b/ptpython/completer.py @@ -143,6 +143,10 @@ def get_completions(self, document, complete_event): # Jedi issue: "KeyError: u'a_lambda'." # https://github.com/jonathanslenders/ptpython/issues/89 pass + except IOError: + # Jedi issue: "IOError: No such file or directory." + # https://github.com/jonathanslenders/ptpython/issues/71 + pass else: for c in completions: yield Completion(c.name_with_symbols, len(c.complete) - len(c.name_with_symbols), From 94f6c9c48c9189177d50ba785faba9b6ca389d32 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sat, 2 Jan 2016 23:58:11 +0100 Subject: [PATCH 110/470] Support for 24bit true color. --- examples/ptpython_config/config.py | 4 ++++ ptpython/python_input.py | 18 +++++++++++++++--- ptpython/repl.py | 8 ++++---- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/examples/ptpython_config/config.py b/examples/ptpython_config/config.py index ddcda9bd..84e3f5f5 100644 --- a/examples/ptpython_config/config.py +++ b/examples/ptpython_config/config.py @@ -97,6 +97,10 @@ def configure(repl): # Use this colorscheme for the code. repl.use_code_colorscheme('pastie') + # Enable 24bit True color. (Not all terminals support this. -- maybe check + # $TERM before changing.) + repl.true_color = False + # Install custom colorscheme named 'my-colorscheme' and use it. """ repl.install_ui_colorscheme('my-colorscheme', _custom_ui_colorscheme) diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 98c0b939..d5d12d1a 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -19,6 +19,7 @@ from prompt_toolkit.interface import CommandLineInterface, Application, AcceptAction from prompt_toolkit.key_binding.manager import KeyBindingManager from prompt_toolkit.layout.lexers import PygmentsLexer +from prompt_toolkit.shortcuts import create_output from prompt_toolkit.styles import DynamicStyle from prompt_toolkit.utils import Callback, is_windows from prompt_toolkit.validation import ConditionalValidator @@ -120,7 +121,7 @@ class PythonInput(object): python_input = PythonInput(...) application = python_input.create_application() - cli = CommandLineInterface(application=application) + cli = PythonCommandLineInterface(application=application) python_code = cli.run() """ def __init__(self, @@ -210,6 +211,7 @@ def __init__(self, self._current_code_style_name = 'win32' self._current_style = self._generate_style() + self.true_color = False # Options to be configurable from the sidebar. self.options = self._create_options() @@ -479,6 +481,9 @@ def get_values(): get_values=lambda: dict( (name, partial(self.use_ui_colorscheme, name)) for name in self.ui_styles) ), + simple_option(title='True color (24 bit)', + description='Use 24 bit colors instead of 265 colors', + field_name='true_color'), ]), ] @@ -613,8 +618,15 @@ def on_reset(self, cli): class PythonCommandLineInterface(CommandLineInterface): - def __init__(self, eventloop=None, input=None, output=None): - python_input = PythonInput() + def __init__(self, eventloop=None, python_input=None, input=None, output=None): + assert python_input is None or isinstance(python_input, PythonInput) + + python_input = python_input or PythonInput() + + # Make sure that the prompt_toolkit 'renderer' knows about the + # 'true_color' property of PythonInput. + if output is None: + output=create_output(true_color=Condition(lambda: python_input.true_color)) super(PythonCommandLineInterface, self).__init__( application=python_input.create_application(), diff --git a/ptpython/repl.py b/ptpython/repl.py index dd66ba51..b385f387 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -13,14 +13,14 @@ from pygments.styles.default import DefaultStyle from prompt_toolkit.application import AbortAction +from prompt_toolkit.enums import DEFAULT_BUFFER from prompt_toolkit.interface import AcceptAction, CommandLineInterface from prompt_toolkit.layout.utils import token_list_width from prompt_toolkit.shortcuts import create_asyncio_eventloop -from prompt_toolkit.utils import DummyContext, Callback -from prompt_toolkit.enums import DEFAULT_BUFFER from prompt_toolkit.styles import PygmentsStyle +from prompt_toolkit.utils import DummyContext, Callback -from .python_input import PythonInput +from .python_input import PythonInput, PythonCommandLineInterface from .eventloop import create_eventloop import os @@ -291,7 +291,7 @@ def get_locals(): if configure: configure(repl) - cli = CommandLineInterface(application=repl.create_application(), eventloop=eventloop) + cli = PythonCommandLineInterface(python_input=repl, eventloop=eventloop) # Start repl. patch_context = cli.patch_stdout_context() if patch_stdout else DummyContext() From f1b68e76b0ca9a7cb78ae2f3166987d55c392ff4 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sun, 3 Jan 2016 04:13:55 +0100 Subject: [PATCH 111/470] Using the focus_stack from BufferMapping. (Change in prompt_toolkit.) --- ptpython/history_browser.py | 12 ++++++------ ptpython/layout.py | 2 +- ptpython/python_input.py | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/ptpython/history_browser.py b/ptpython/history_browser.py index 1458d0b6..eff7ede2 100644 --- a/ptpython/history_browser.py +++ b/ptpython/history_browser.py @@ -468,10 +468,10 @@ def _(event): def _(event): " Select other window. " if event.cli.current_buffer_name == HISTORY_BUFFER: - event.cli.focus_stack.replace(DEFAULT_BUFFER) + event.cli.focus(DEFAULT_BUFFER) elif event.cli.current_buffer_name == DEFAULT_BUFFER: - event.cli.focus_stack.replace(HISTORY_BUFFER) + event.cli.focus(HISTORY_BUFFER) @handle(Keys.F4) def _(event): @@ -481,10 +481,10 @@ def _(event): @handle(Keys.F1) def _(event): " Display/hide help. " - if event.cli.focus_stack.current == HELP_BUFFER: - event.cli.focus_stack.pop() + if event.cli.current_buffer_name == HELP_BUFFER: + event.cli.pop_focus() else: - event.cli.focus_stack.push(HELP_BUFFER) + event.cli.push_focus(HELP_BUFFER) @handle(Keys.ControlJ, filter=help_focussed) @handle(Keys.ControlC, filter=help_focussed) @@ -492,7 +492,7 @@ def _(event): @handle(Keys.Escape, filter=help_focussed) def _(event): " Leave help. " - event.cli.focus_stack.pop() + event.cli.pop_focus() @handle('q', filter=main_buffer_focussed) @handle(Keys.F3, filter=main_buffer_focussed) diff --git a/ptpython/layout.py b/ptpython/layout.py index a9068121..47b1ea52 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -303,7 +303,7 @@ def get_tokens(cli): len(python_buffer._working_lines)))) # Shortcuts. - if not python_input.vi_mode and cli.focus_stack.current == 'search': + if not python_input.vi_mode and cli.current_buffer_name == SEARCH_BUFFER: append((TB, '[Ctrl-G] Cancel search [Enter] Go to this position.')) elif bool(cli.current_buffer.selection_state) and not python_input.vi_mode: # Emacs cut/copy keys. diff --git a/ptpython/python_input.py b/ptpython/python_input.py index d5d12d1a..f17399db 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -548,7 +548,7 @@ def _on_input_timeout(self, cli): When there is no input activity, in another thread, get the signature of the current code. """ - if cli.focus_stack.current != DEFAULT_BUFFER: + if cli.current_buffer_name != DEFAULT_BUFFER: return # Never run multiple get-signature threads. From 0710c7788b668b17047c091d472f4ccc6114337b Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sun, 3 Jan 2016 21:00:54 +0100 Subject: [PATCH 112/470] Use BufferMapping for the focus_stack. --- ptpython/history_browser.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/ptpython/history_browser.py b/ptpython/history_browser.py index eff7ede2..cff81e3f 100644 --- a/ptpython/history_browser.py +++ b/ptpython/history_browser.py @@ -8,6 +8,7 @@ from prompt_toolkit.application import Application from prompt_toolkit.buffer import Buffer, AcceptAction +from prompt_toolkit.buffer_mapping import BufferMapping from prompt_toolkit.document import Document from prompt_toolkit.enums import DEFAULT_BUFFER from prompt_toolkit.filters import Always, Condition, HasFocus, InFocusStack @@ -19,11 +20,11 @@ from prompt_toolkit.layout.lexers import PygmentsLexer from prompt_toolkit.layout.margins import Margin, ScrollbarMargin from prompt_toolkit.layout.processors import HighlightSearchProcessor, HighlightSelectionProcessor +from prompt_toolkit.layout.processors import Processor, Transformation from prompt_toolkit.layout.screen import Char from prompt_toolkit.layout.toolbars import ArgToolbar, SearchToolbar -from prompt_toolkit.layout.processors import Processor, Transformation -from prompt_toolkit.layout.utils import explode_tokens from prompt_toolkit.layout.toolbars import TokenListToolbar +from prompt_toolkit.layout.utils import explode_tokens from prompt_toolkit.utils import Callback from pygments.lexers import RstLexer from pygments.token import Token @@ -525,7 +526,7 @@ def default_buffer_pos_changed(): """ When the cursor changes in the default buffer. Synchronize with history buffer. """ # Only when this buffer has the focus. - if application.focus_stack.current == DEFAULT_BUFFER: + if buffer_mapping.focus_stack[-1] == DEFAULT_BUFFER: try: line_no = default_buffer.document.cursor_position_row - \ history_mapping.result_line_offset @@ -543,7 +544,7 @@ def default_buffer_pos_changed(): def history_buffer_pos_changed(): """ When the cursor changes in the history buffer. Synchronize. """ # Only when this buffer has the focus. - if application.focus_stack.current == HISTORY_BUFFER: + if buffer_mapping.focus_stack[-1] == HISTORY_BUFFER: line_no = history_buffer.document.cursor_position_row if line_no in history_mapping.selected_lines: @@ -571,15 +572,16 @@ def history_buffer_pos_changed(): read_only=True ) + buffer_mapping = BufferMapping({ + HISTORY_BUFFER: history_buffer, + DEFAULT_BUFFER: default_buffer, + HELP_BUFFER: help_buffer, + }, initial=HISTORY_BUFFER) + application = Application( layout=create_layout(python_input, history_mapping), use_alternate_screen=True, - buffers={ - HISTORY_BUFFER: history_buffer, - DEFAULT_BUFFER: default_buffer, - HELP_BUFFER: help_buffer, - }, - initial_focussed_buffer=HISTORY_BUFFER, + buffers=buffer_mapping, style=python_input._current_style, mouse_support=Condition(lambda cli: python_input.enable_mouse_support), key_bindings_registry=create_key_bindings(python_input, history_mapping) From 7969b7d935ed71f51aac306e7195eae6bdac4b8e Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sun, 3 Jan 2016 21:01:25 +0100 Subject: [PATCH 113/470] Release 0.26 --- CHANGELOG | 11 +++++++++++ setup.py | 4 ++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index ca39cf2f..8093bda6 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,17 @@ CHANGELOG ========= +0.26: 2016-01-03 +---------------- + +Upgrade to prompt_toolkit 0.55 + +Fixes: +- Handle several bugs in Jedi. +- Correctly handle sys.argv when pt(i)python is started with --interactive. +- Support for 24bit true color. +- Take compiler flags into account for ptipython. + 0.25: 2015-10-29 ---------------- diff --git a/setup.py b/setup.py index b4a2687c..925596da 100644 --- a/setup.py +++ b/setup.py @@ -13,13 +13,13 @@ setup( name='ptpython', author='Jonathan Slenders', - version='0.25', + version='0.26', url='https://github.com/jonathanslenders/ptpython', description='Python REPL build on top of prompt_toolkit', long_description=long_description, packages=find_packages('.'), install_requires = [ - 'prompt_toolkit==0.54', + 'prompt_toolkit==0.55', 'jedi>=0.9.0', 'docopt', ], From 8c0cc3f7a778872b4185fd554ded005711231d2f Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sun, 3 Jan 2016 21:19:17 +0100 Subject: [PATCH 114/470] Release 0.27 --- CHANGELOG | 7 +++++++ setup.py | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 8093bda6..3c3790e8 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,12 @@ CHANGELOG ========= +0.27: 2016-01-03 +---------------- + +Upgrade to prompt_toolkit 0.56 + + 0.26: 2016-01-03 ---------------- @@ -12,6 +18,7 @@ Fixes: - Support for 24bit true color. - Take compiler flags into account for ptipython. + 0.25: 2015-10-29 ---------------- diff --git a/setup.py b/setup.py index 925596da..646ffa99 100644 --- a/setup.py +++ b/setup.py @@ -13,13 +13,13 @@ setup( name='ptpython', author='Jonathan Slenders', - version='0.26', + version='0.27', url='https://github.com/jonathanslenders/ptpython', description='Python REPL build on top of prompt_toolkit', long_description=long_description, packages=find_packages('.'), install_requires = [ - 'prompt_toolkit==0.55', + 'prompt_toolkit==0.56', 'jedi>=0.9.0', 'docopt', ], From 8b0dfad0b77e70afc1660f67c193738793dfee0c Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 5 Jan 2016 06:06:59 +0100 Subject: [PATCH 115/470] Release 0.28 --- CHANGELOG | 6 ++++++ setup.py | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 3c3790e8..c35509ac 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,12 @@ CHANGELOG ========= +0.28: 2016-01-04 +---------------- + +Upgrade to prompt_toolkit 0.57 + + 0.27: 2016-01-03 ---------------- diff --git a/setup.py b/setup.py index 646ffa99..510e00fa 100644 --- a/setup.py +++ b/setup.py @@ -13,13 +13,13 @@ setup( name='ptpython', author='Jonathan Slenders', - version='0.27', + version='0.28', url='https://github.com/jonathanslenders/ptpython', description='Python REPL build on top of prompt_toolkit', long_description=long_description, packages=find_packages('.'), install_requires = [ - 'prompt_toolkit==0.56', + 'prompt_toolkit==0.57', 'jedi>=0.9.0', 'docopt', ], From 76f1a3e805aa276ba03ecb6a5355ccd5a70f83b9 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Wed, 6 Jan 2016 23:55:21 +0100 Subject: [PATCH 116/470] Improved mouse support. --- ptpython/history_browser.py | 46 ++++++++++++++++++++--------- ptpython/key_bindings.py | 13 +-------- ptpython/layout.py | 58 ++++++++++++++++++++++--------------- ptpython/python_input.py | 31 +++++++++++++++----- ptpython/utils.py | 17 +++++++++++ 5 files changed, 109 insertions(+), 56 deletions(-) diff --git a/ptpython/history_browser.py b/ptpython/history_browser.py index cff81e3f..a4265c76 100644 --- a/ptpython/history_browser.py +++ b/ptpython/history_browser.py @@ -29,6 +29,8 @@ from pygments.lexers import RstLexer from pygments.token import Token +from .utils import if_mousedown + from ptpython.layout import get_inputmode_tokens from functools import partial import six @@ -210,18 +212,26 @@ def _get_top_toolbar_tokens(cli): def _get_bottom_toolbar_tokens(cli, python_input): + @if_mousedown + def f1(cli, mouse_event): + _toggle_help(cli) + + @if_mousedown + def tab(cli, mouse_event): + _select_other_window(cli) + return [ (Token.Toolbar.Status, ' ') ] + get_inputmode_tokens(cli, python_input) + [ (Token.Toolbar.Status, ' '), (Token.Toolbar.Status.Key, '[Space]'), (Token.Toolbar.Status, ' Toggle '), - (Token.Toolbar.Status.Key, '[Tab]'), - (Token.Toolbar.Status, ' Focus '), + (Token.Toolbar.Status.Key, '[Tab]', tab), + (Token.Toolbar.Status, ' Focus ', tab), (Token.Toolbar.Status.Key, '[Enter]'), (Token.Toolbar.Status, ' Accept '), - (Token.Toolbar.Status.Key, '[F1]'), - (Token.Toolbar.Status, ' Help '), + (Token.Toolbar.Status.Key, '[F1]', f1), + (Token.Toolbar.Status, ' Help ', f1), ] @@ -400,6 +410,23 @@ def update_default_buffer(self, cli): self.get_new_document(b.cursor_position), bypass_readonly=True) +def _toggle_help(cli): + " Display/hide help. " + if cli.current_buffer_name == HELP_BUFFER: + cli.pop_focus() + else: + cli.push_focus(HELP_BUFFER) + + +def _select_other_window(cli): + " Toggle focus between left/right window. " + if cli.current_buffer_name == HISTORY_BUFFER: + cli.focus(DEFAULT_BUFFER) + + elif cli.current_buffer_name == DEFAULT_BUFFER: + cli.focus(HISTORY_BUFFER) + + def create_key_bindings(python_input, history_mapping): """ Key bindings. @@ -468,11 +495,7 @@ def _(event): @handle(Keys.ControlW, filter=main_buffer_focussed) def _(event): " Select other window. " - if event.cli.current_buffer_name == HISTORY_BUFFER: - event.cli.focus(DEFAULT_BUFFER) - - elif event.cli.current_buffer_name == DEFAULT_BUFFER: - event.cli.focus(HISTORY_BUFFER) + _select_other_window(event.cli) @handle(Keys.F4) def _(event): @@ -482,10 +505,7 @@ def _(event): @handle(Keys.F1) def _(event): " Display/hide help. " - if event.cli.current_buffer_name == HELP_BUFFER: - event.cli.pop_focus() - else: - event.cli.push_focus(HELP_BUFFER) + _toggle_help(event.cli) @handle(Keys.ControlJ, filter=help_focussed) @handle(Keys.ControlC, filter=help_focussed) diff --git a/ptpython/key_bindings.py b/ptpython/key_bindings.py index 09dd68cb..a5a89172 100644 --- a/ptpython/key_bindings.py +++ b/ptpython/key_bindings.py @@ -7,8 +7,6 @@ from prompt_toolkit.key_binding.vi_state import InputMode from prompt_toolkit.keys import Keys -from .history_browser import create_history_application - __all__ = ( 'load_python_bindings', 'load_sidebar_bindings', @@ -60,16 +58,7 @@ def _(event): """ Select from the history. """ - python_input.key_bindings_manager.get_vi_state(event.cli).input_mode = InputMode.NAVIGATION - - def done(result): - if result is not None: - event.cli.buffers[DEFAULT_BUFFER].document = result - - python_input.key_bindings_manager.get_vi_state(event.cli).input_mode = InputMode.INSERT - - event.cli.run_sub_application(create_history_application( - python_input, event.cli.buffers[DEFAULT_BUFFER].document), done) + python_input.enter_history(event.cli) @handle(Keys.F4) def _(event): diff --git a/ptpython/layout.py b/ptpython/layout.py index 47b1ea52..2af8e54c 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -13,16 +13,16 @@ from prompt_toolkit.layout.margins import Margin from prompt_toolkit.layout.menus import CompletionsMenu, MultiColumnCompletionsMenu from prompt_toolkit.layout.highlighters import SearchHighlighter, SelectionHighlighter, MatchingBracketHighlighter, ConditionalHighlighter -from prompt_toolkit.layout.processors import HighlightMatchingBracketProcessor, ConditionalProcessor, AppendAutoSuggestion +from prompt_toolkit.layout.processors import ConditionalProcessor, AppendAutoSuggestion from prompt_toolkit.layout.screen import Char from prompt_toolkit.layout.toolbars import CompletionsToolbar, ArgToolbar, SearchToolbar, ValidationToolbar, SystemToolbar, TokenListToolbar from prompt_toolkit.layout.utils import token_list_width -from prompt_toolkit.mouse_events import MouseEventTypes from prompt_toolkit.reactive import Integer from prompt_toolkit.selection import SelectionType from prompt_toolkit.utils import get_cwidth -from ptpython.filters import HasSignature, ShowSidebar, ShowSignature, ShowDocstring +from .filters import HasSignature, ShowSidebar, ShowSignature, ShowDocstring +from .utils import if_mousedown from pygments.lexers import PythonLexer from pygments.token import Token @@ -71,18 +71,31 @@ def append_category(category): (T, '\n'), ]) - def append(selected, label, status): + def append(index, label, status): + selected = index == python_input.selected_option_index + + @if_mousedown + def select_item(cli, mouse_event): + python_input.selected_option_index = index + + @if_mousedown + def goto_next(cli, mouse_event): + " Select item and go to next value. " + python_input.selected_option_index = index + option = python_input.selected_option + option.activate_next() + token = T.Selected if selected else T tokens.append((T, ' >' if selected else ' ')) - tokens.append((token.Label, '%-24s' % label)) - tokens.append((token.Status, ' ')) - tokens.append((token.Status, '%s' % status)) + tokens.append((token.Label, '%-24s' % label, select_item)) + tokens.append((token.Status, ' ', select_item)) + tokens.append((token.Status, '%s' % status, goto_next)) if selected: tokens.append((Token.SetCursorPosition, '')) - tokens.append((token.Status, ' ' * (14 - len(status)))) + tokens.append((token.Status, ' ' * (14 - len(status)), goto_next)) tokens.append((T, '<' if selected else '')) tokens.append((T, '\n')) @@ -91,8 +104,7 @@ def append(selected, label, status): append_category(category) for option in category.options: - append(i == python_input.selected_option_index, - option.title, '%s' % option.get_current_value()) + append(i, option.title, '%s' % option.get_current_value()) i += 1 tokens.pop() # Remove last newline. @@ -282,11 +294,13 @@ def status_bar(key_bindings_manager, python_input): """ TB = Token.Toolbar.Status + @if_mousedown def toggle_paste_mode(cli, mouse_event): - if mouse_event.event_type == MouseEventTypes.MOUSE_DOWN: - python_input.paste_mode = not python_input.paste_mode - else: - return NotImplemented + python_input.paste_mode = not python_input.paste_mode + + @if_mousedown + def enter_history(cli, mouse_event): + python_input.enter_history(cli) def get_tokens(cli): python_buffer = cli.buffers[DEFAULT_BUFFER] @@ -310,8 +324,8 @@ def get_tokens(cli): append((TB, '[Ctrl-W] Cut [Meta-W] Copy [Ctrl-Y] Paste [Ctrl-G] Cancel')) else: result.extend([ - (TB.Key, '[F3]'), - (TB, ' History '), + (TB.Key, '[F3]', enter_history), + (TB, ' History ', enter_history), (TB.Key, '[F6]', toggle_paste_mode), (TB, ' ', toggle_paste_mode), ]) @@ -338,11 +352,9 @@ def get_inputmode_tokens(cli, python_input): :param cli: `CommandLineInterface` instance. """ + @if_mousedown def toggle_vi_mode(cli, mouse_event): - if mouse_event.event_type == MouseEventTypes.MOUSE_DOWN: - python_input.vi_mode = not python_input.vi_mode - else: - return NotImplemented + python_input.vi_mode = not python_input.vi_mode token = Token.Toolbar.Status @@ -381,12 +393,10 @@ def show_sidebar_button_info(python_input): Create `Layout` for the information in the right-bottom corner. (The right part of the status bar.) """ + @if_mousedown def toggle_sidebar(cli, mouse_event): " Click handler for the menu. " - if mouse_event.event_type == MouseEventTypes.MOUSE_DOWN: - python_input.show_sidebar = not python_input.show_sidebar - else: - return NotImplemented + python_input.show_sidebar = not python_input.show_sidebar token = Token.Toolbar.Status diff --git a/ptpython/python_input.py b/ptpython/python_input.py index f17399db..5785d72c 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -18,19 +18,21 @@ from prompt_toolkit.history import FileHistory, InMemoryHistory from prompt_toolkit.interface import CommandLineInterface, Application, AcceptAction from prompt_toolkit.key_binding.manager import KeyBindingManager +from prompt_toolkit.key_binding.vi_state import InputMode from prompt_toolkit.layout.lexers import PygmentsLexer from prompt_toolkit.shortcuts import create_output from prompt_toolkit.styles import DynamicStyle from prompt_toolkit.utils import Callback, is_windows from prompt_toolkit.validation import ConditionalValidator -from ptpython.completer import PythonCompleter -from ptpython.key_bindings import load_python_bindings, load_sidebar_bindings, load_confirm_exit_bindings -from ptpython.layout import create_layout, CompletionVisualisation -from ptpython.style import get_all_code_styles, get_all_ui_styles, generate_style -from ptpython.utils import get_jedi_script_from_document, document_is_multiline_python -from ptpython.validator import PythonValidator -from ptpython.prompt_style import IPythonPrompt, ClassicPrompt +from .completer import PythonCompleter +from .history_browser import create_history_application +from .key_bindings import load_python_bindings, load_sidebar_bindings, load_confirm_exit_bindings +from .layout import create_layout, CompletionVisualisation +from .prompt_style import IPythonPrompt, ClassicPrompt +from .style import get_all_code_styles, get_all_ui_styles, generate_style +from .utils import get_jedi_script_from_document, document_is_multiline_python +from .validator import PythonValidator from functools import partial @@ -616,6 +618,21 @@ def on_reset(self, cli): self.key_bindings_manager.reset() self.signatures = [] + def enter_history(self, cli): + """ + Display the history. + """ + self.key_bindings_manager.get_vi_state(cli).input_mode = InputMode.NAVIGATION + + def done(result): + if result is not None: + cli.buffers[DEFAULT_BUFFER].document = result + + self.key_bindings_manager.get_vi_state(cli).input_mode = InputMode.INSERT + + cli.run_sub_application(create_history_application( + self, cli.buffers[DEFAULT_BUFFER].document), done) + class PythonCommandLineInterface(CommandLineInterface): def __init__(self, eventloop=None, python_input=None, input=None, output=None): diff --git a/ptpython/utils.py b/ptpython/utils.py index 22d5e68a..f6d0f1a1 100644 --- a/ptpython/utils.py +++ b/ptpython/utils.py @@ -3,6 +3,7 @@ """ from __future__ import unicode_literals +from prompt_toolkit.mouse_events import MouseEventTypes import re __all__ = ( @@ -108,3 +109,19 @@ def line_ends_with_colon(): return True return False + + +def if_mousedown(handler): + """ + Decorator for mouse handlers. + Only handle event when the user pressed mouse down. + + (When applied to a token list. Scroll events will bubble up and are handled + by the Window.) + """ + def handle_if_mouse_down(cli, mouse_event): + if mouse_event.event_type == MouseEventTypes.MOUSE_DOWN: + return handler(cli, mouse_event) + else: + return NotImplemented + return handle_if_mouse_down From 86b71a89626114b18898a0af463978bdb32eeb70 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Wed, 6 Jan 2016 23:56:23 +0100 Subject: [PATCH 117/470] A few styling fixes. --- ptpython/eventloop.py | 1 + ptpython/layout.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/ptpython/eventloop.py b/ptpython/eventloop.py index 5e3fd9de..b51e877b 100644 --- a/ptpython/eventloop.py +++ b/ptpython/eventloop.py @@ -15,6 +15,7 @@ 'create_eventloop', ) + def _inputhook_tk(inputhook_context): """ Inputhook for Tk. diff --git a/ptpython/layout.py b/ptpython/layout.py index 2af8e54c..b330c4e5 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -138,7 +138,7 @@ def get_tokens(cli): # Show navigation info. tokens.extend([ - (T.Separator , ' ' * 43 + '\n'), + (T.Separator, ' ' * 43 + '\n'), (T, ' '), (T.Key, '[Arrows]'), (T, ' '), From cfc9b2e712520daab46d8cc875d95d8b0360a3b2 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Fri, 8 Jan 2016 04:35:56 +0100 Subject: [PATCH 118/470] Improved the README. --- README.rst | 83 ++++++++++++++++---------- docs/images/example1.png | Bin 45666 -> 28700 bytes docs/images/file-completion.png | Bin 42066 -> 37364 bytes docs/images/multiline.png | Bin 27156 -> 33129 bytes docs/images/ptpython-history-help.png | Bin 0 -> 103337 bytes docs/images/ptpython-menu.png | Bin 0 -> 60225 bytes docs/images/ptpython.png | Bin 0 -> 28700 bytes 7 files changed, 51 insertions(+), 32 deletions(-) create mode 100644 docs/images/ptpython-history-help.png create mode 100644 docs/images/ptpython-menu.png create mode 100644 docs/images/ptpython.png diff --git a/README.rst b/README.rst index 5a337214..ffcb8a62 100644 --- a/README.rst +++ b/README.rst @@ -1,64 +1,85 @@ -ptpython: a better Python REPL -============================== +ptpython +======== -|Build Status| +*A better Python REPL* -.. image:: https://pypip.in/version/ptpython/badge.svg - :target: https://pypi.python.org/pypi/ptpython/ - :alt: Latest Version +:: + + pip install ptpython + +.. image :: https://github.com/jonathanslenders/ptpython/raw/master/docs/images/example1.png -``ptpython`` is an advanced Python REPL built on top of the `prompt_toolkit -`_ library. +|Build Status| -It works best on all Posix systems like Linux, BSD and OS X. But it should work -as well on Windows. It works on all Python versions from 2.6 up to 3.4. +Ptpython is an advanced Python REPL. It should work on all +Python versions from 2.6 up to 3.5 and work cross platform (Linux, +BSD and OS X, Window). Installation ************ -To install ``ptpython``, type: +Install it using pip: :: pip install ptpython +Start it by typing ``ptpython``. + -The REPL +Features ******** -Run ``ptpython`` to get an interactive Python prompt with syntax highlighting, -code completion, etc... +- Syntax highlighting. +- Multiline editing (the up arrow works). +- Autocompletion. +- Mouse support. [1] +- Support for color schemes. +- Support for `bracketed paste `_ [2]. +- Both Vi and Emacs key bindings. +- Support for double width (Chinese) characters. +- ... and many other stuff. -.. image :: https://github.com/jonathanslenders/ptpython/raw/master/docs/images/example1.png -By default, you will have Emacs key bindings, but if you prefer Vi bindings -(like in the above screenshot) then run ``ptpython --vi``. +[1] Disabled by default. (Enable in the menu.) -If you want to embed the REPL inside your application at one point, do: +[2] If the terminal supports it (most terminals do), this allows pasting +without going into paste mode. It will keep the indentation. -.. code:: python - from ptpython.repl import embed - embed(globals(), locals()) +More screenshots +**************** +The configuration menu: -Autocompletion -************** +.. image :: https://github.com/jonathanslenders/ptpython/raw/master/docs/images/ptpython-menu.png + +The history page and its help: -``Tab`` and ``shift+tab`` complete the input. -In Vi-mode, you can also use ``Ctrl+N`` and ``Ctrl+P``. +.. image :: https://github.com/jonathanslenders/ptpython/raw/master/docs/images/ptpython-history-help.png -There is even completion on file names inside strings: +Autocompletion: .. image :: https://github.com/jonathanslenders/ptpython/raw/master/docs/images/file-completion.png +Embedding the REPL +****************** + +Embedding the REPL in any Python application is easy: + +.. code:: python + + from ptpython.repl import embed + embed(globals(), locals()) + + Multiline editing ***************** -Usually, multi-line editing mode will automatically turn on when you press enter -after a colon, however you can always turn it on by pressing ``F7``. +Multi-line editing mode will automatically turn on when you press enter after a +colon. To execute the input in multi-line mode, you can either press ``Alt+Enter``, or ``Esc`` followed by ``Enter``. (If you want the first to work in the OS X @@ -128,10 +149,8 @@ There is an experimental PDB replacement: `ptpdb About Windows support ********************* -``prompt_toolkit`` works still a little better on systems like Linux and OS X -than on Windows, but it certainly is usable. One thing that still needs -attention is the colorscheme. Windows terminals don't support all colors, so we -have to create another colorscheme for Windows. +``prompt_toolkit`` and ``ptpython`` are better tested on Linux and OS X than on +Windows, but it should be usable: .. image :: https://github.com/jonathanslenders/ptpython/raw/master/docs/images/windows.png diff --git a/docs/images/example1.png b/docs/images/example1.png index 3c2047eb9bf58b1db4500ddba2fe3fd6d63eb560..c46b55a8e6bfb489a558cf67390f10c7c7c5a606 100644 GIT binary patch literal 28700 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 literal 45666 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 eNXy|+-P5~^$l&HkslJEXh2-+i9v`NnU& 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 AhyVZp literal 42066 zcmb4q1z1#H_b#EL5{h&yAl)b(DkvQaNT;OKkV6lkfPjE>gHnTlLwAGZ5CXzbLx%$l zHFOO3;P?BU|GnSs1M|Rh&Y8XU*?aA^-uGSWglnoR5Z|S}i-UthtfVOa1_uZ41P2FK z{q`;33`XCM68Lk=LPbFy=j!_BYcn(+ICBT2sPBe@L!t(JGH~aPGIW8H1nx>|FA3&x zsmYm0YMOjXfm1Z@FZJBzoE;sloZNBbT&>LAtt=mc?c8l2Dk!OG>IRWe;ov;PQIdbI z?KQoH1p65w{MRpsXHmrdzwY3%>OCv_l(~@^=lHV1K_!;=adahL#c~Ne-hp1cd|7Ra zJ-pIopLNQ^aw>Y+W2^Tqt9}h@v8Hjtr0>n!fe%M~Q)P~2AVgxUH{v*MD1EtTG39Uh zm@0leE4iLQbGeViON=Z{>$>99ao7vC;oT0U&;dqA1O&=X5f4n95wLyryvedXWQnXS>=?r zGV}G3d_R3K6tUSZK!o$Tq5Bbh)s?G`>blYTk01P!xsj1OI3Nl7cMqOE5g8GtW>3m6 zT5}>;xoNDeGxSy|^6$VNs)v2xC_iAGAw!-*H?~n9nz}ArJF&-eOl>rh=Z=0WYd77G zx84TwU-##VoB0XF&_)xsUScQ|xxX~4WMyP>e<2eAF8=sZzQ7Cwy6v!Ve5yWkDx|ef zP8y4fSI)m3#&Y6kvbCeXyXH{Zj0jJebE%QHx{1pbQNivX1Uy69Is_xMC%^Q$blwvl zb>cUN31E=*y=WCsiw~3WlnrQg?q1_$79tB?g25PZ?%v(+CJq=*F39Z}Iy>8n*muDI zJscby2?~hGyS(PWn8fvwqZu+;SCy zJd|{&FVr+-Tr-4Z`BbPYlqbGgui zb1^}%{k2Tsqm@3rgpr25J>B;NCG*0->FJQvMLxRusn1sWJVp9#S(n_DyB zz(r6>(GK@KHmLli^QM$E#w#xNJ(P@pP{5M7Q*kXtO^V$pm1QZl$HA^$iZT zue!e0g?u~eH*-Cn5}w7mmrI zx`6+GBSaVOopADT4cyo)u&Z!DSs3x@uI+v6Je+*BZ)qV5(mrOFMu;L`1wuUOBIrL6e8>+u85r zL2H=;n`XB-kyROjZM9{J`Ejgq;YwDv;u#x1sbb-;R8$Nl>W2H)TJFt%7_?qS47aqE zFY`Wm@+4}*pP!$5^=xw@A95+xRip#|t}m8-buz_I%}Ao9`DOS=?QB~le`|j%OBB82 zMBJU-VausegCx`Hgk+JXSN{I^J<+w6co#8#e;9E>!@|P5EG{;tsBGWAAG#>vW_A@X zr@7@+o38ns5x5Uele!;TSxmsU=by;~@2KD*>J-`7vy76IXV3bi0sG}>@a?%g9}`j< zb;qdL|5EB~GG-2Auv3Tw0lK6o=}GzO^VXXt)EwAs*P|0GCJGFcMUFu>h6ikfphXV? zGq!keELeJgU0~vkCI%L+hhF%Jl&ZRC4cBXVKJJij7lOl?)Jv|MDs1spVE0(%{N6z& z2$=oBooUGi4lbS)?XbaA-`&Aj#tWreCnSyd@ZZ!7r1er>QYa-gZ}0jN{btwS_WJE@ zg*{HS=T+X80S40LepBQ=;~MmsIjgA64Lab-*|fh5Gi*9{ErND)nXcUP5DJIv%&sCf zv@Ei*iOvODT!}f&x!f4I{cH2~LZuosgK_R#M5<4qtdP7-h4hJfnEfdC&ZkcU+N)Oi zUb9Xr>}0BftANQ4o+Y7Ubr}uV+ERr(Y7fh|#C!%e<85vw_mV zglWb`v}75)GbU|To745h=~3Sl7D1M8;Fi-h4s*7d)MKY6dtSk|Pn3BmSz;BxD6%T1 zT3i3{O*c0aC88%G;7cZ~or&sM2DVmFS9q@#J7pwU)T_PICb3N6b>E>1p3tyPG5RAK zm$668%v2htS8Y@5Qw>H#16KlZeJi~cKx3U$odNcL2S+h=5 zPPO*k#t2JKQNSU?=j()B}BI!|*r zD~H)$EiRUU%)z8-)JCduBe6%fJsY2wwRLV$+?+ISFm2FmXAmJL zHUk=ifphD5sUPg>u{j=f_|Qc&wGkbhk4@e-M2uLg;a06R?xi4aaE#BOotprAF<#)r zB1tq6eLlYo2&+60j2?bM%AowFg_TM*vZdzs5D{Y~af-G{lX^ z%V159FFuw!V@d8IWO_|aFDagyk&?!y-d_mQBE>rOkWOJ7!#q@F~kGw^RXzS^Sx8A;PsX}NOUEP#hmT? zP?mK5vMv?xy8v|xU8+*eWeUIuIB^4Ngd?ed_<#dB=|+hEGVDRT^$*%qL1_LNWb)AL zP#q=PWJ&w{dWrj|RW{bxDU8g#dk5AKkI8t$w8>Z^bMDQiN0oFZ$wLU8I-2ogj(s|yU`+R| zUvz27$!x;R@Y62tse@Qk^+HEIJuzZ}Yy6Uh?-yon|$E}iEXgISBn;JGb zT`o!Hex51Q(y|fh*Km|O=3_1smORMNc)J0wd_d)DY-cmCZ=bdIX2`pKRcsJafV&KlXW(s<;l0zVb%Ch$fiE9}DIUqJZRe2h=*&_3O@o}GR1USGT z+1sc>Ago@ODoR;+q(N*~nULY|18WY7t}VlY2?OJ2EAExtU|V!Jlo>g4doMY=k6}sm z)T`%`KaFd;(|Xl@a^F6y&gQpUcF`$^Y0E}DnUm~JrV8fSq4VD1xGB9GjKB3T`J!mg zN`gh2T7<6JoABhQI;N~0J0I(UQIFOu;vvfs$Hqj*5V{S4UNckW> z3zy$m2)F``VuN`X(k2fJe)Gw;Jr~!KJXrN& z(=jy(ONKOH=~8>LA(!?FQJ?Hp=5ATq%vy#BNFqO;s!Aj2i%THrP)~ ze=14#OWo?hqi0Uufh8|?dglZ7xr0}9Z@75(+)6l2eD133cjK-Ua6;(0iB!hQWESHZ zjQI7Ncscd&Fx3Y)Byt+W|A%&lzi)Fv6j zm%$f=B0?Xr#=x)fLVDz=fy!J!PaW_ zH|u^}NLUlVUm@%~9|McG7&=<*=Z0h&k(h0-fk~4&Uw9f8kf33eL_w)$#$Lhd6%kFq zVPP5(2GNux8V5igkdue8z+0re7%$pWyn;kE;Cg8)mD?40ecZ|bTc_P z{|Wk(keasVJh8rO@_T`?t^Y=3uJ32ZS3a(5DPKp+qGV|>M5=}oIMsByh~26gqBj$L zX9fw9F+Yx_J4Gk_!lk7N>al(2C!^_CWNx%!AUK(PPm>5sQ`5AoeRkMik2La!>dgtamG?@1)BgymPf$U!W`838?|^tY+B&oH~OGyPa05&Q*#^v6N(( zea`-vutI!aonSkV3r7zmmmx*b zD}NNb5Gugril-8<>-Wk)CmNJVbfzJEy0^V9ql7UdoUcE!_sdypDPNtf4+BrvlJ2iz z{$yP({LzFLy63kro7j6N$Gn|a3vc}rbn?rtw3$ncFwA_NfPisbMMbvH%pxT((em|n z6Xp$n3%w{(?E?{5p}j0fyggr2L+}^lw^k2oD5AaGSX!QdEMa^tE7|ARs=kZ{9deE<$y>U!r4`dUlUY0Yj`P}o;IQCsrqQ*YPZ-Q~Bz2!lC>!=;wF z&R;6l&`CnH*-q?h#BVb6a&;0ImEGZmCX08N>0r>xvXnGlo7e20OO7sIPFBOIlk$9z zNM3|{s-4$kk2Vxe2JB;Y7uOkYeQk?}A3L3GzVH$t25 zaDmoGr6ygwjl(26rDqQ}=h^KFb|s`Pvgxo9`KM1)5fBi(_H>{)M)SOKMnmL9XY`IV znG|93sh?HX`pLuuWO1Zf_U0(ulFz_D0PiAV_0*k-VJS0v0$;Pf!fYTebvZl?5VLKmOFOwu6TkB$Deo3bw8lH_{XmY^5TGGUB<9tQhT)~fsC3!W!DVW>z&-cs{xzc}~$MVioYNj0C(U38a<`!Y5 z%W|Qjkg{)HWWU*I3c|@}qxO4Wde1!U{O*rMb?ckTyAtZM7kO57nL#HHpW6h!Fm0zv zuGxwt{v|Q{JWDi;*}2`Dmqpl$6cw}VPM2!M&HjFD`S8ZR^gwG%$|UH4^RJM_vdm*2 zjaOPfL*vDoJZD?J52uRq#`iOs!dbXwhdZn9sW)6&&KYt&?=%yhf<3by=K|bAJ!peA ztnSZ@($UW|A?X0{U`vRs;bB<31olLejtM(xqY&JVtt?Q}sK^wzcWP`RFF-_agwPdaRq8{N_pXv(E-i>hRffl-nq#$UMyyV-K})n$Pi zQ^D=sylTwc$03>slXRT^!p>i*(XKzAj_d>C(wXP=7XL&~weypNay=@m%y!a23l2{r z0o|4ICimLsIHdY+4W(YmiqvS{XVR;}ti}w$Syu15X<&?*5`O85$ z+WxASr2kJ>b8t+rIqLuByZ^pQzVKJ){EwFZzpj4zABpdOeD~j1U;j%Y{XbuI?Q$)= z&8H#$?J=4945HrfCL1sxz*npr!f$NH*OT4{rYbg7L(cv0ll?RHURC#z&_95A*PXR) zGaSw~>wx!vctE7S`$dyfgL;Hpg*WQM){t;rU4Lwo@%f0Z2r1KDtvUx>eTv66YBRgD z=~WI>a;WKi?H{eLc-60)=2H!1QhIf2D%k>_mgkm>^q${JhaUdqd;>tvg(vhSoDFva zzlb^ysI|2+oZO_Fsj;Y>sw?n%oNaLMx`{-CEoyAdO#SUmoFAPG^fY41rhW@elm;_N z%|x}}W&VCy-#};mDkr2$6XesZ_-d|+P&_AdX7ePyr{6cDYWctRFoz?X5wQ!FWR!Ny z(co!vp2oi*b45;FoZ-vTWBdRYV4r;=;J7$6;5N+UA4QcmzkoWH1vn$4vGx-G0R7dU zx$J#ek$}x6sx%~8`3kWtW8V<8CSu|LLF`hdVts)2(BQJvbbXS}@OzU7gU(K)I`8Z2 zCod3f8}_u`TSCg>F5H_xks$EdJee-y2l=CD0rSlzQBG0vvGA9V7gY8OsoNT|Yc2Bw z^C1#T3WW5A@G;v3ETdftSyw#HHBM`TNrSU`HAguVY3!nrLAgKbDnHn$A?nWsaBfz+ zmDAKHW%sDE0;-=#V^FFxt_acjA3HK@u9p@Z!B%O&y;>46e--#p+?LL=$_5Z84leXw zVEq(D>bkn#)qtD>dQz=l2zOV~-_L)X>eL;r0KHsdY(`ymQKP<>4rL8qWN=+`0}er{ zQ?n_d)BoV638kAqzohMjPPlJN+<0b-x)^Z#k?!fXOT}-dkLrn{o6AQff)$+TEA)?{ z*?Ep|lW3{)TjfEIh{8T>6}>r&rth?-<~f@rxXqhN&^B?k!%p<1)`eL(Y*=pf%s#~f zdAHi(+E}@N=jo1o-*jfSN}bhfNV1R670S4c>1~9_Va(tMrWxp!)#j;&RAVFo5PnKH zTi%a)JMK)w?;XhwB}E(i`S!GCjd@V~eyim@GcM+zC0s!B9`$e(8P>VXT-(5mX%ha! z-dI7To5X|zR{yfqFI2$|zdgit>*4g6hJ)FsjQYy9Tg_oHak8xsxD#_0Lu!S*UpAb# zRrH$by~GA{#l=#4XgGd7H~U~vb3NI37k#Jy6Swf1<8!rNcVhHI5vC;dnUnG2nSev8R+JnA+CarA^YL9HTfNXY)(Bj$$4M;hct1Xt66+{eyDO)%fKh3xFl%a8Ike{CdztwH3a1{OvJj zn3JhdR615PQE}7HSGFT z;G%pAzv-b;OU(x1dP4T_Vm|YN^iGJx>_{ibS2T3&Ikuz7tsf2&6EGUOQ^0VV-}Yp@ z;@VTgG=+WGXIdu@7#iL+&l-E5ixV6dSnaRYV&i54P9Hi~JBgPfkoCTnUPB#RM9}4u zb}uP<@za&?)LLi?czAI305Hk?Y8z7 z7MzR0nw|Y?9@a7?x(7nbQC%!$z4yWml)F@@Tc6Uf&f?#r+C$Q{ihplXSFN~tauf}b zhU&O?6Yqkfcw)Sh7R9kS2cx-;=|^9P6oBN!3h3i%wy)Xf`pngs&EA^TSam65U`}H7 zKi&HTGSxn4n%Lhiq&hxi3^fsw>@rbYwEPGXLN@w;t%JZT-74=N7Z*Kk<~`e< zizH{`Mc3;^zF;aPT`n^r?ZS`qKcfuXKaqzqzGI*k_Ngpt2xMdJt{}Ag_2S}8UTfewy0^3+Q1w#Hl{7wFKOJd4Ru@C}Wc$6LaQiL> zdS)dc=K!&|7#`dl_BXxx7!bL&jhF9loHeU*+FnC~dLbGvyz8;V!oz%aZN6&ryjs@M zuwk^(9nLpf?H?<6s>OMWgppYcdbhDhcn1QFAm9!Wq?<2rbK-urKPSo1*SArfskam6r89X{CA-jx%wvO_2OG)7XJi7 z;kX9)8ucghW|ATpQJbCV@wqj?qYAyG%@HQ!?n33Q zgl=-yFZvj@S84_$ccSxLjp&Kj&4;jOfzjYO zs|kX+7)MLgXvDoHCh&9IP!^=+?9^0M(~*QVbI`YM48m{PGsIUCk^-$<91u>lBF^To z=g@Uc?8m{k$;mYuT;@YLB-HrIrsy4XP&j`}fan$caK+r@7J8R01-9TbxjR`#I=6 z=w8MVTbrSg)JD0mBpnYScPfA|h0_OLp_UAM(tf;OVf=S3K=df`mttQI?VG(D=caxr z%k%FUYIf)x!7w%pK?Bci>%zu#FIK#ykU@b$qsFw1(NWd=$OdQG%tHmm#}DfKWR#P6 z*lL>`v`zeb3~ePrUlbv-pKqM3a>JONR}fHq{$^Fbymjoa;!>+MGW~4i!kc^`GhQ6#GG<*iRcO?#?D*<>=fXp5 z>WsB#4+C(brLXY%Y!6CMaHmFM-M3$ccxPmo`k!5K5(+neYY%0 zZVX1J`bzOeW@R?s*48#V_R*$Ee&Jg_v$wuZwvAGg^_={!SIvwJ@Z4giiOD{yvwWhg z;U?Vml(3JB>nBJ|@l}qOtDBHrGVx_9;8U8H+5B`TH)}iON~Kyr#YebZ=5V_Q}E}n9_dlSwj3(fb+QFplZZn+dl!O!FgzMR(l zkh!(B1Hq)OJ!#)QF1~{I=#7DL=&fU%_Ky2z`7?IUars)Ee{Y8CeTK0RC-57qVz;JH zQv3Up=d&1hdI{AjU$#5hs5D&Heq}pkZH%^1IC9afu9iwTky+0uFZ8cbuReH3eWIfB z$wifuWl$AgyKwW7X-oiz&e z^<_`gb?V}Y-kE|wQz5LUpdhLNFYKSbx-@tQq(k~beYt|UV+O4|KliAgH?hlR7x~KG zE>M~e!p}9?@dJ}NIy>j>%Q3O~&Ph?MJZ6&+2%KR`nNuBfk7<;ZjWIJfUo#!)2wBn5 ziJsNVdo_)y_eW0hZ2mOe@iyN-m5^qvh>S!zN!@LGW+1czlp{Rb~7;e&?@#m*`x!KmGZSW@l9LAb};LapmY9 zrygZCXhJEwB%y=IY!FV!xSPVIq~IL{`uLp~J&~vTXQrPqC2j8tx=mtySBw_3vb1Q6 zU*^?P?dTsxh{gYEsweXF5d1y$vaU9HR)6(92QUx%zwT$!3}q(wJi8>#L9)obS9Kfc z*4e)ByS1?q?&%q9{#hwL;+0ZJCo41Q^qg0`rI_l-_@+?lF_Xl{+Oqo!0*bDnFB773 zH^x}?#3kbk-i=us0ghIGRel-}S00w$vq=hlIw0R?`%&=5tUZrM~@#jdJ=nWsZY$AHoR-@8;>G!>#gfkMKgi9mqSTN9CMiY>~B0 zVepr8TT^0EPH)N&N)@45aE9A~gRj49Z^Apd$i-4JbZOOWv_a_g`v4=5M z(pxW1KPCD-UU@ar)1BvXa(TccF#wahN+IbDe!eybnJ=VppPuuJ1liB@eI4AfIYNS? z8k9a&f$uX>FSNarrS{ksMt|j|iZwR_OW4h|xUt?o{vz8Ly!7%Z)R^Ww$mA)$BYF$6 zx9WbbUuX8FxKF#psJ%S1(RQu{AN~ZlOp^Y0Mn#3%UI&ma=9@m+fi8oL&z=DjaTBAE z2}gNqRTPHS_@ASSWaH`?ojoN6ONx_5s=T>9oSQS9=AEb#?cXkwoItg;9}`l6Rx-hy z-;EPm>aP(6(SN{_^aDX4T$a>QHTHZEzAvVdCbQ*)NC_|hZku2!MkGQ9%g7kJ4z0#uC$3gJYQcOy?Vc+ zm#v@VmQy3Uw^CIf@m_A)ABhf~K^vX7nuLq@tC9d2am zKPRrPAE1t({F zk-Rpl1oeA|1BDjyfFQFH@!(z>$8U;|O<=PmvUo8CJ&`74@Nzq?qbWsIY z^PCtTtMI+|ZKFPc-%x@DCOwJ>6J;lOXQ^ultWqld;XK| zEO#*czaULkss<1fzAUpZZDQ~;6OR7{qPe5kWkJn{5OZ}|< z6^XQvA6I)Y{cBd$V$}P!<#cNOkE|C5!`DfNlm7mctG)TYrho6#st^9sRww9INnN+>K`xuZTi@m)U z)IPB3Sxaa0@Y&U3Lp*Ma-6*6kdX&yN1tjBYC9vgraeDgE*Jmt-!ISH4{>)%GL!)B% zGE|z6CTetSR@G(jMMcD2L3)z7SdOgO4Ar!Ejh<@0`6EY7_4hDxj6O4 zxt=OJ_*>V0IH#deBofWWNU{v?8(Pt-apzpjVl+6~&UJTjv%0*B6ymLq2~py&%B!MUe1nd-pyyL3de#m$wlZlbI_=&RetNcFwy1)s92@6HcJ8(U+P&jZt+;ZmAj{9jmGi{PTRl& zy?s5R*d~cZJbU~4HJcs)gEX@uy8Zww)fKUtnsqWd{qmr;>etL{hD>*bSc?t6epi?F z593VnBOp)pP>&jq=b^NF@9J?BR`sU^VB25)P!{_(!|Lm?$vt!HSN@22W4S+ArBNTDE_=fiLT9e08H62CH#eGXbf&gRJw>H}T4Yet!OvyDi5SVu7n&0O zWt;G-eacY1gIybr=2hz)G@9OPi-6vL+nWd8pXYK5&r(-+tl3Qz2jAn+Qb$)qf~j6e zp5PYf9DgkRd#aNH_ChK+BqSB`l8`D+S4Z6ST`XRkwPbB6ongUXi%rEA`M!sblqD6_ z7}D#><&>R0dvTp!)W7=mhS4WS1}Ac-T=_CV_vh}7Hakk)ws5}R%0A^b`thjjv$v?pMg34@bfT&{r>TY~<)lL$KJMM(_2@IO#RJ&PP7g?o zdg~9h>?bDupdQtzX2#>*`VMJn_UtMi=H_Ov$_h4r^fzxenyn3s!pcaH^L4x(*96L@ zD!vMy2x|{#HP=;E?tQ#lCZ3tPj^CYW(KRq+&XnvAE_zEBqRogQ$ovX~wM-KFiqTls=x-zteS4nQPS?6LD z@w*5qzbLbnsS-MSQu38v1E1^L;hS9)yJG==1OQOmaf)F#mTA|#?d&NMwd2%*I=n9? zXmTyFy4?dsy8Kpz%&8|c z)zfNs=cXvs2FVSjuS zNYkyqU!;qW$_!j@I|C4fP7f5mqeukh0FwtFbVd!JY|}?*sQrp=0noy(HFRvC;XRF7 zdhMIzi5V2XKy76I(_$Bb3oWjqxDZ0I{h`K$-+Z)~sq0X$J@r2-5b2<40R3Co-cCnG z3sXxi-+CdZrtlqdvHm+?yHaefa=+JdKUSqJ2%k*-eKb*{AY0kD1go^4p`qoH45?rJ zDh4d$yu^YR-4_nL)P04`^(prj&3VQ;P2cGpQ+xD=AY1B5xnl#vbSFr^%=obi*C%YVR*H+TgT4u#YRSQ zgyRF6fpyts`W{10o%Lb@2BmsvE{*o?n)Y|Vf`K(xMaHlR{m#+cq)Jzl)k}mTjs=#T-0nY2Uc%32Y<^asPfvd$r!beOrb!3VsoI4EkXwxnc^}ue zPkCQx86^Xiz(-ygOS&dq<1>-GsjQGqPf9H2itW6uUC^%6D*xv`h9>K&{VSu!*80?X zfMZ23mi?FF;YM?7FBU1s5c=G8Wk-<0ks7Em+S67J@eKK6QXdy|6VqP7;lKCTTKZC@ zBR~65Y+|C4u+i)G_ApuLaw1+rpqBHvB-_c26W*c7)`Rx0eH=fjN;bBdc9t&b!n?c* z1RX%|<7i7JTpc`@QH|wqb{Qu;o|Aje3c5N+OP*lo{7Y_!oV0>r(8={&JF`GFG{dYg zGy-OuKmftuNlG^2oD2~o+;nvwd$TkKIXOA6$JUBe{HW9l@yjopvIU&1D|qLTbC3G8 za~-2EYyZWLbdu6&qotsu==_r-LJO~yQ7Y4J$QE^U5UxW~h*hoI!JO|O)3S$|&311N z&gfR0k)hJGkFzq>HhsmrnGKsdy7rCc@djJX_2 zH0Z*5>2>#3wDdxsW2C{wgMZ;-YCud=b1cQT`=*ACq0UW>;nAQQfeqLIoEI|u-cF+S zSD1A_Pt=2e;-WZ1>?SAHeOwvqH~3y=e{&s2@2zHA%Qj=n-Zq=-tP|FD)itYW^Rnb@ zYT5l2GuI#PkE@{jeB7Pjrkglp=%q>H9fGe1r&L2?eG}qO(-HpJ!AKcc>Br?T9 zL&M3btVR=>4ut9(k3knhhw>2-eaPnYDGiyI#uGaa__%LA1S6cRr7_%Rl)O3UwRFWZ zKe6-ka7qg6eY^TgPd{E>^45JWpg>$z=u4LHeK=gw89sZ)gNUg$W0B5V_>uHB znuH{pcsj7t-@j>mv_smhXc95jsWLd|=Y6V^JIX*u+kY_WjQV4C-ov;+sLQ)NEYgx2 z+!D-sDKpE`x}_J|zK|h~GMis!Je~~N*ibD~AK^58xF~RHSmSzexdW63)*}7Lrou>K z8-2lN)bN~>L|vLf*xH}wW6NmiLpEHqLRS$zlxa~YH`k|i%NT40Eh8Ib#?!#LF=WmP zj&nOX@ij){O7%}B7?_EgJI2D#t~zd#w;s{Hl8Q#TE;>4PDY8;$NGcEmax|c6b=l}s z9>wl;6(lk`L%N^wGTONL5^xjBan*5u=1u+Ss=lKzH#QeNSka4Y4lDV<&b~JdXnicL zBK5a!xsVW#)PS({nZ>ze>2}ey(eI|vAL0GGHC3uyrE^`?B77^h zugUs7D>XP{xqh_yJjwBZxWr1hOn)M8U-=du(Ocd4MmN#sNUC@p-Qi~*)wBR<0=741 z^XK$&i%amH4K1x#!8h|4-n1fgx*>NbL0ffqPwp<+J9drDJ)b#5#CQ+7O9-;Ld=T5P zE*H1~IGZLZpCn%COJZA35x^$QIEB||dB3gt7dOo+7bOFv{O4l7&LgHJ z^1nSbwtp+QE*tRbYxx~h6+*!1O8(K7F5Xj9KeS8pvv^H>RrZb@7fJ>-FDr%Gyg4o5 zAS#E|eToKs)Zo`weJRk}ik^YuwG4=d-P)@HFF4xgpNu1DN{TGauuXy}i__B^n;U!X zo)$pva*UNWZSp=$UO*98*Xuo^M=4jy-DbV8jWRDQdf*_!)A`j6X%J!Ro0&JPu(Q;~ ziXT9~xU0Ajuy3?CYsAy}(0RsFqO^sUg|sJ0D4a4>(b8(fu<_Fp!h}6G@ud`h@~>_% zi0Ix}xf3!EZc|thr}&zbLPJ_jio18W#PuodU+TH8Ys1Bqvud8e>@nr9u63DQ?|4lL(9DSU{+L`Ml&b7%rWWn{CI z%x4{W5F|p!pj%4k0n3}$0NT>JnBF9Jm`W3``LX*7$6#A;xWp72QIvgYBhfqzAMxF0 z%Sx8auh^(NqP3RYGp$0p=T7ud0r`tiNBCOQTMw&ek6nURq>hUFn3g@YmX605!hZ?& z<@1XA{V_Q@KG;nJpu!f$#le+Lt3z)K-1PP%Ly**q+a zJ>mkNP_6|Su}*6}Sc{AUXFM>%{!MX5lOsj)^Cz8<6+l+Q?jNzJfE-Yw- zspx7GfG4b(Z=NKd8yi7ndqP9C?=!O)U0!|^ab#X~9}Vavb}zk#BBVgwhmZ$T_Rsr~ zsNIR)p?s>dCf6)_v9CfYBmfH8wldJkcLiQzpy|^j{i`_qQ`8YlqbTRj_wKvi=Wb}$ zkkk+?OQpR(5vpci@P>$9N*<(6;TfY40uBLQC<|KVy%8B%7ULk4XeZmp3TWEd4zgV@ z$p=z%Uy%Jvb&ebq*iB5p`Bm*tFjP_^T!5afN(EHq`v%Ps`-io~yL>Ky&_GmG9pf!? zpX;HXo>#@(f8}=OE6IKUYHD(Pypt?(lvuL~1pw{DjIoW9+8f{!NpbReKJNS_AUJ783H$TQPj&*QfG4_t9g#_ZlOM8<{;x& zg@77ar<3mDzmSL)F+FS*NcO=Hk zeJ%Mshfe@H@f)bWlJV<$NhYDo^}y=tm=P{DWLS|hn(j;+4?C@2N6C79k{2TitS$Vs zFX_atX_BSncG4sJNKZ#;ulK>n@b>2<*jk3b)iwuwe^WzUq#M1%3tVObAc#MWtVsC$!$4eeoK*e>fBqe|U3Pcb-Jde(yXV@u< zybg0fn^m~^TH5z`k%>kto1P?|Na1=eZmesM%>QZ&p1-<*9erS6;_b0S+=k9sf*otQ zAkMF4hmk*aVd(=qo#Y8JP?`|Cp0ZxiHIalScr|b%^F`L zK&Cz7?%6TA$B>#?49tgiZhpGxHbwNIBsTH&7UQx0SVKj{Zp^{t@c!&j(9YEtkU~$P z$;DnBW*@Y^iu>zeptQ|N;20ZY=2pn5UaQUAJ_OAcr};fYoaDco1x#-OHnC?r!)FR= zAtD1GnFC_d~721P19N@ zW8`%MF}z|Ff|q2MdA#Yix`qc>OWNojQqps@()S*Q@q&8DOp_u&#)+JPgmTVvv(Qrr z*>QP^GEGj49z~4CdCSm50#fMP$3Qngn0cBJ` zUX+#HPu76HkNDd-(KQ|JvuFTs{oM9^H|4OxYv?&$B~D27r4rOWg>jQmh)R~#y3RrP zyt{i6etekyG|Ec<2>|w+LPE%+RJ#)+XD7BiOSHPzvpJ)@)1G8)@sv%02=G>Phu)e4 zeGGiOsx7{44eJ;${&ajeQn5&}UuqC~N$-O~dV3Q+Wq3S|%)}z5#6{EsWIjZx$xBKO zr>r?t6ctg6xqfW%KUX3mCf#A8+X6dIDcAkT{FOE?FDdb; zav6rtjz&)EY;7c`cKPyV{UwjOy6L|d zEU02lIh5V=%om{eUUsT!5sBPlHEvXcGhnk+wl9oVrj=L`bO1LnFw*}}-5YJ!Yu<2L zG&fI5GiC&}>5m?mgBuH^PM^}K^Do+g8%Nj0@5M`&>JgA+WY?Jg!@+Hc`gzA#MWj9> zQ9r%=z0S%0dqO@mR;SwRj3=50o--&G0s6cyCHUgATUyvC-pHWwy#2=LO7@sHy$lmc zw#Ll>&T8V$ws;D9*+G4$Goov?9Eesc7l2?5#G~to1We~&2nn3NMzGff9G`lB+Qy-+ zcqk%APJWN2_Dqetaq89YSJ@3}M+AKcyFK*1BOs094d42)%toDJ!rJ^r=&uc(6L>%0 zxrjv?8-9=BZAvP0u!KVLOCcpd>|}?R)qM&UcROeYnEJJk|3Akd)7Lwf&7!xOeUD>x zeeL+2#b%L!x^kYfj{gl0(9hU6n_*c=NZ;E!!})$wqa#+fm5cB`rDSut+=(innBuyF z^uH3oAr8^+O#e%Nh(2f5g&$s}wXw_2rR$T~F_rUgotDYuF+OvcEINjNM0UF*ji+0l z%9P`I@UkCjOdNk_$MsQ4A8Y8lJX$rf&)?OG0a@s81p(*{f5 zGT(o%fs_9yprLlz~VfU^x2tfX6ylqh;TIN`4{9XXL2DVQP zYG5+g2D0bIF#3D}sSqjz?A-`&e*_4$H~RbkNP}>~hI=y3cRF>Ao_ur5XMTMI*(o1b zZh9Qa*?d|ch)T=$&BA^NQ~cP8;}sdf(Yox3x4L@IIqu!RYXMT9gh4LIf^`zfO~Jf4 z|Cc~@l;`9S-FhH|lmweUj=z!x=n{R8H@g=$Jyl2lI6U|6v&DyAfnxFz2^7`VITQzR51a`!0$Q-U{buJ(DRK?EV4q zVW`wBEa1!^qdvVbh$W6+>c9K8T-%@5t18#01sH#M#7hStC01&q>+!?`%kaSe@=2>_ zQYYlB=TG}X7TGI>2yX7iew%&MISVWIgo&LCZj#GBrbWkSba9Pkk}@4ur?gQmcl9*B zN(xfpheR0nAk&&A;1|6dA6ZwzKejKax;VCON0*WM*cswl-Is;aIi!{L;%p zu*vbqlh(7l`Qti5Zf=b}&e5PcpO)2ckA7*BVB4<(EoM#!^DHC=J8ZJYP8v;f*G^oT zczLTXXLi0#I}Z3?^uhL(jt<8T2A%H*6c9M85XMrO&WfUYIVkJT;8!M>i1Xqr=Vgx= z`CSEO+oR_BUg3T3HHBAp5=TUfYt|V7Ou&MQ`J2R%k?osPzPSA;fY35x=pEPZ@xBxO z042fdVB`+V)J#kz4Avkt(9L1oUQx0i6KG{{ui4LsLz^T{_wBL!#S)dS(!r6-NzB#l z#^KuHca?4!voyMb<-)X3==X4Usg`BG{m47OyXg#(Xt+q;lmopqW-Py@y5wgblbNN91ZF@r7;cb*(~A$&pJh0TC5CS>g=398biL> zz7d&JU7lP1Axy_DGE%kWP=wg@oT2FlZ=-Cj-JJKbDeP0!>Qtx>k5ju*@84f& z86SztnwmJdMLs!Rk_82#2zM%<2z-L(r*`bGnL!r&p_bOxcITq=k87Q*RdU{WpQyBW_M+0w0NHQbdRZ>xVGtFim6TLz zq@}x2I;BIpLApaqTBJc*K)R&6Bo@7BSfq4!!*8IUeZKpgz4xi>yx04OT#F@Z&bh`M zQX@u=P->L&c)~i% z*b3uz_gvrh^?s>4#IPd|gDt-t8R;6S(1xq8V5JI{@N~eo_J9S1CxGzqf~?tVgR}Mh zJtA?{Moy_;{Hh3ZR zy8g?U=(Lj;i8#lP{8M7LW7?=-1$tS-yl}IsOs=Dx?Br1rJ`ppc*p{k+xXo3Q&TR76 zn-PQ*tlW@!ja9ZRQLGjjY5ZI*WK- zwOCuLC_@;#D5FedC>@JWiA`e%2``FBJR35jsTme>IgbupPB?e zO-#Jxis;*(wH@)OQmVU7Y2o$mTl!h)i}<$6ZI>;_u1z>YR%=>L`K#yLiRsSJx}s&J z<>B2uYj306oJW2ECgvj$@a}$sm-pQNya86~pEn*o-t)(p23c`d(A&_8KJDE*W-=N9;`T&a~=nnf$F z)~j{2vI^6}0uG{gpw3W5uao}j3hAOYtf0Q4!ajw!ye-Ao-xQB3vGSPWU97)p;&ga@ zQh<-4nn`7Y*R?8;!12CMFPCa1fb)Xy-;v3@zsde)7O%C-2!4-+^@-cE;Y?WAC`*TZ z;@R}7TJqO#pCtsdNC!_EcAsvoeSf&6xY}Rte2o7t*YDo|*n(qj_?V#A{p`u)q*@6r zix%a{_V^ z01n`aQn_W*nQi%xcHBmnGLnf#Dyvku5fvLeXv4$wpFJ${PMs+4WS{YwDC!j}?K-yG z#o)A!pYq)vT>`lHP&9pex(t*NWHWGcB(h;Voix zIDmIgudhYCR>`jrr{%XDCGt(b4!vPb%{|=xytbKhUQx~=^kRjtroC(6HnNE18qUp7 zWw^aU`{(Ut@gqlvApm%3H88B$xDVnR%R*b2dD#oOxi6yL#Clbk0{DLF-OW%WPlHv- zQxpw}jE{dl<$~q)jd;{3GrDg9rEr^($E7e145c1r_!8Rvw^CA!j_b^|5dRzsO)LGS zs%K3H)Gex^*(p{mfxbc%zT9}jpKj6vm|Tc zx{RWVquAtv3tYv7XR}WEowef*Puuy3 zxY6!iRl{o#-BLwoEi7E-X?=e&=Nwg&p2Xs@{$imn=GpU!k*=d=II&zmqw+`@3MN#9 z)E{ntzM(ikQk%}iH?q>(XYpGzJn<_5>m1-FnVP*MV&Jz?ds8Te?TdFnhcNuEFHH`RnY^AZ*m}(v`LxKkSC$6v*R^olFN23n;TvcDkq8js@!hP>NKG4!CgcG3?g08l1<8CRT7aVTkU*7QZPB=qoMLg zzRasH6w>dTAXy64zUhcbxoTG8DT$1g4iKA*jm;rgQJAGvO;f+Wf@yi0h1EcvffLtb zkxbff7{$o2keLyV7Yj|82#AU|pPpqQrJ%$bMkb`R$ZM1@=>xu+MrAM-agjtZxo)a? zynKEfE&w;Wpu0Rqm;1|uHcIM?mdt{+#l#hPnyG5maLR$hU(mmSPBgQ(+b>*=7J)cw zq;F_tb@{xp>|TVa1${)OZIRT=(i>kvwuJWO)7{!Fhd}hQvXbVFt9uCsU*znh@Pk3r z^AHTMBw0Rdzv)fcD4Kptc_gm{jKnb@oP^Zn3hzN6Fh8J96d0;;fx7Hq>Ezr ztf#PCkw)9Z^;3FXySm4xN4kfujLZqPa8*`Ze$HjlwrXV%YG%o|iyjDPFOTozvqdOy zyYnePU;k2|mcuHc{mP9JzfF&uo-LMLbbN2VuuxE5=^#Jfd2_QzQjrEJk~A=Z7_U8h z(@#UAl#>YW{2-Wfb6|PTRZu>jq(SsYCPJoCJ2RaoJbx`j^gHLzqvq#e1@0U(a@$Ag z@yT9C!ep5aM@JWs?&Dqldg>U5xtn}`@z=e5Pi@4Ea?O7WmmOqQ1-&S4@->fxKKgsy zcW;5wPc>d%a-e~p?;x!*i^XQ&=;R6bN=Wtg-!gvO#} zY-IDfCaKNbvS6tzz;2QV4ZoHv;TfL6dxIb9#42fDv(G1&LUnGU7tr}>s*D3=rjc`O zoO#{y@&Yo6LOrh@qr$Y7`@;Z;l%i9q(Chp_t8mGpAdV#QVyw@!XTMpo{xCNBg!>Yy zV^rC`iDcAj?$AYY9VJT>$C&E=#@Op}T1ZC84?n|rpnI^yDbi0Yty~-vQL?^*T8$7{ zP#GproR6|3!rr0wI6-J*s0_-!I}$kN92OB2VGo-EufdLe#=1t8X{Ow_cu2-Nm*eV+ zqiWAGH8wKGE7BXc3a+N+E^*v-&tnS*wf^LbF&~8KY?p1rNpWy5+A~>sDrrd1BDnh& z1nf9=xJi;a!?qR)=_3$pMJ)gaf)L#c6IfcnzQX=ml|!5JlTeUlPa= zQInHLDoH*|JONh#-{^d%-gJ4X@K}iFeu_sO- zTo9k#J={(vR!S9f_udvJGosEv{&Po!(8B*+Q<^ z-o8&j&|DS|GS5PY${JGB$3`pCSJswv$;Ja)C9Asc@gD|*es@n=WGG7&kqbL20;Fuv z!tJ-I5W-lgv*}gGd`H&+%!n_q%-te1B3RzxEF8#wXU?XZGLKK2e2WVU@keB@E>L5w zQDS`YL|b4Z?UDSKP&2;2Co4XN=_dG=bC>ij;b^&QIX)SK6HsZrE4GlTpC)S??NnIL z+%Gj*-&PmK{M;#$4#YWhSyXXT)6)F*xi5A)B5&C70;}ccPcsbOe0k@c8$1T@TjnDo2DkI?7GYYHt%t9iNnPu!S4qRp0Z}8z7#u zLDZ{bCg^xMKF}citLwcPdZkJPL7n|GEdNTy0m-DlX2qVwI*XX6W#UJ}cr2B7CBa!6 z=FnZQ=8bkT#HQQ8HF*9ki&!BuLw&yde6ownpqqZlGxW;LsqN@@P>#$exxaEg<(WF0 znRStUS)Q}RryEGUuX_+;9*yK*IQ^bpTu<+#$*TNSq^nX=m;zhiYpjCog!(Z_FgQRf z_<;D4jDi9WToP=#RT1;yb0*KE{a2|^Lpw}1*2U~Ns0^5^ea(|DOT0lMzyD@JDJi*uDp1EL6Mr4K+6Sk|Wv$!+w!8lpm! zNzEg!d*tc~_^v83By>Kl67!V&aN|3W8 zh1oL)nc|&&TLKBL9?9oGi&WjAxW?c}2=iW4)8Yw%^_S%27>OrNAXWh?z<-d?V}HkV z#rSGfuT^?hslP$tu*x_kY6x3YmpqbR)hY4CRTWunBTFgo`w4xE>egDuHZ%RnLZ`5Q z;M@84M4&xNLTryjCHSKu-l^1LUUBB8I6v9Yw0K5U+$R4uftgH7+IfY_vX4;dTl$z* zA{5>UslJ0xb-!f-=C8kHg8bo9MjGjh8!p^nL)iJcq}PN;qhn5#087u}gryK2-8Unn zeeulUH-7>Q@uL6!okn1tBs1z@mdgEh+m7-r8Vd8t1;ZmkKB@UZ!CVpfn70(&^f{j# zEmQQ1%mDxYk>j2W&rgMfRvedDIYtKsY!b|?m>SMjy1rAi#aFNP;(x9jaL}ywkUg!w zcXJRDD+=g9Q(!JDU<1roH-QEH44>apn7U_+-_e1)WTb8BAM^nJq45Y(jAk{J5WOlD zopJj*rL?qY2p}bqY7$R(37TtwZ8@wJaqB1BYt2;H zrBcW$%@I4cPt3wWCh}>=HrE@o@+K&bsq`>j=oIEp* zsg||Q)WJcR^3Kd1(p-AmE+2?)$|b3oEj~HlM{KK;(CIOtGAuJeBXTpex8o&E&8e;p z&M*a2$&&rqQ`zdxai^~%1yXhLPQU$uzf_e0A; zAV+g}T$QctxfB+|M}?r1&dV zvmzCfCTm;YDzCkj{!B)HjK#Ohj{`YfR}2A=gW-v_u)jli@cQrcgAN4N!5Ayg(*m$R>JV^!vNRjDuF;+c6d0Qoz!HXpizAL5eQ$BS-hx% zb?+J7^gjg+H0dsew0(U3~;q zoTzcv8i#Wvtp%0AaZ7{c!osY>tpuVILRzUdfuSFT(;0Wo0+mUH3IASbm@YnU`hP+L z$_Az9^+O1IKo5Ly#=Mo;Vpr<*dVqcY($dk*YExQte;VRFmWBi7mhf;};z>f^+vA+k z9}C8=CRZ!$Y{}+}VeD#+Od?EC@Xgpk7;eYd^`-HepRNVuO}5r409JKq=!2=RhZT&{ z=wVf8`J-ZZ0#>m;2uS&r8AEE6{uxA9|@9a=4ZrxYQ>U!H5Q)4WbmZtm}v*2Ru!*{+wWgan<; zC?FLmHIA^ZMgYlkm|o=O>g-*3sX(00?+nwbMF|#~&Rxyw1C!u0oDPrkKSOiK4{786 zv`dd(o%2wQ+as(a05FT#F9T^K?^{-1p24xB6JN z7<#?RU%#mZzi%MViYhCOnjlhk^`{)LT)E%DD&gN6+Zkia`4kgVggas}VkC3QBD#$8 zYq~CL->(X70NFvw9O2v;WK2vg6IqZodR&lI!UgS5hw7Y1>(YuT%CaZ63BHJGAwnsr zSkmw8B753?i3kR~Xor6DsBk-+dvsbH(1$*tNbEVs6V!Hh1>&xAxjl4S?r(QA1f}CG z8A{0Oa&!1b!&1W3*|I3ozX0dcVh>FsG^o50EeYaJjIwBDG7;TBJta|{Q+H)YE4Hg6 z!{~L6R9aq~_MPUYrz_{((a9@ON*% zJ>9dqWu1=@$!p>AsYsjYS_KuKtmR)p8*kk8op2%h9Nj@+v&z^GkbmEpL?h(LDo1FM z%pja?Y{KQw#G0EA@$Q7B2DzvB4{5ZAAIm|f)cZ*Zvv0%L69{VGLihE#X{ii{F(iSsEYSViGA^?_U^|5oCzn1Jss#3oK1M~Y`&Uu*Ry&s zHd<(YK$9Z($)33;zg~#0FFsq8M%#1G+0YRUwmoZ8B>*OuvqJZy8~q+-9r5u9&o>!W z5hB(M<6al+BVTz056z2b58n%l$}=LLR^5Bc>IM0NTp9uL;hB~}-@XyrswcA2cu7z- zk4Y7sh#f~|E*!BeEtt-#P;O->L&YucFY|vcC*0{33knX&Q>!uxmosm_Aozwh+#ufz zM91W?w}BkRzW*F9yEM1Bn+FRgPkP2%!Q4$Yxr{Yb)MwYLEZs&Ea}r(rdLB` zI#z3lAK2+@R$j`?E?&JnD_KDkgY7J(Z|6c7V`upEu_nA%1!kf&#s2PlGilg@FG|zx zi~K`mgD-DEDK9eMC74!#PVSLiTzt5Hk7LtJe(PjoGtp}9bAQ(|BlEN1N!r)DUINrn zFAf$}*bv28S-%B{cl8bU=6y6>)MOtO+~UvDjNP&g?p65WJO|hKhSw*cFLUH?MFS+P z2eimJb4{6cpb1Y%I!sDy^FgzQhHu9AUn(m5W8*Qq52^o&bda2VylU&*kVe%TG5d0A z$VZ+=F9D~>o8+$CK&bDkive2}I(3aFs(s6pnsd_?sWl!^T!g0bym^8$uB7g(^KS3> zT@naVLG-pi6k^nrIk-;@wvG!tp9NYx5rNC;Nty%~A+{yUhadLPZ-~MNS4{Dab9A1w z^0dzD#KrZ6x*vDY#r0xcdgoOxOv;=vQ$J>VZ@B z3YZaH5&U?wAJqSjV9>E+FxzrUB%n|sxiUHroS%c)Buk&pX}@*AxDd7e-;fIIAeAO+ zIAB3Rew1)0C+)mZo_6><>M2Sa`nA{x=RlbY2^DRcaXxdg&zQ|K1w zCELA-BpFi{BVNltlnN$hJf)&Wln8ZLJ{ncIq`p1)n-@UR|L%Z*s00eiE6<>!!qEcJ z=)}!}i^Xf5`L3h~%Uumh_ArFb#RUc!2k?%N(t;zrw(qNU!7jbc?pTX1*Pm6VG1#GQc1L-&r0hrqw|%bkXAX*s!1K;e0I z#VD5p-dmaG$?hg)sti!>ku&v=n$B?`iu--re~KrCg&yFT+L{JyFyv@kmzvLagGdY_ zA7=mbu+VCRXG!J{qjc}QO$Afgps5qbk71~~YEs&p%VW#0?EQ5Fb*ds`sw!jq%Z(i; zK&vAvf+)0dR;~{i1ns6HouG^*w8(vxr^O_YvJw>i%Zi(-S9b=1JGn*^?=MC&*EVo8 zc-Ciak>Kdy{&n{D3O9QA*^z*Ewu&~z#L$bFvmbI;_-?gtL+6b4$M8_KlJWds`dX)I znDc|0xucUM*N^xC%%&;G#^OtV3mjTxQDswZk6>MWeb!GY#CQgt23VP@B2IN6D+-h= zFcsF8RQzC6H21u&9wi2j-|;}LHKtB>d>}bT?pY0q>uJNrhoOEnv+3#lWM7bj<8=Nk zML|$$mff0MBG?hIP%+9>{dG~}^ofvDYP;oRG^#GPI7ddtWKDW2u2Pe<0 z({wg#g#&{5YtLoHkEM9hyf70qM1QG=PK+gEVi827QExDINzzJl>T2r>pjDOJJ4)6h zfRM1Ha)h@ny|-GUASKcMwz>6eD~20$I!3L0x|BPacA&TD!C7pFt-C8kCj75EMg~d> zI?qN&XnY1?&KeqzPwnHp!aLTkA&*xWbS4kdHA^G-@2G>BVd1sN%NTE`%VKyhG6mGJ z#|00T#rOt5@rjO7R!Mx3psr3eyz{M$RCGisZT7_l$eRf{Jn~$(F2FvUDmsX&%Z}bS@S=mSNp%Ng7mGhL5rW{dV6OdFv{YLJ<(~vYvvPELTE1 z3yS;DCl;(f%Z@TMZ_(7^Iq1oC*pDui!c=xE85lLDfm#BVrWRbx+ph7?B?Tu~o?PC4 zuecDDPK9J7_>S6T>Nof!^3XwOBdXoZS|`Tn+Sc=n#hvPCh>$afcXo>H(pATjQyY&0 zVXOZF{o5O#Lk#$KaJ#HJ>mgTb$EF2(AH>hq0tC7E!pa-9X-=uX{1y!M*x4+s_!9$^ zX|}0#jGNJeKelaV-3+#%-tO@DZSFbCS+c!bS*b+-_kzJD{*ODgz#*dHX2!hy9uw)QLu5PL1&O+S@gFkbM2mqy)ONzXSKn zf2aq+o1m65)_^^FiN-8SdpgI<>XEtSc9abQuH&~3Hv|6 zbRy}hIo318lK)gzAiXmc$j1B&-5(%!F3$p^_&_}^Hdyz1b6*UZ@9zEY^zb*L|28Zv zmI9lkBxV#I_Y9Tv-P;lT3BC;Np2$1kYcjrlmD0!{!aw|x<2ND2np8@k?Bqh zay`q-LP+40Ze*lqmoL&@qx6DQr?gLf_Ag&T+2p8V+JCMLu(2RR8Rb|kH&67{;CpdfPk3r#ck0(AA z5-4`x)nuK`Lj#IrLWLrMQARn+Z(7}SYiG*dgejHMvm$P^LlkM!qUQQLjb)DP6AO!i zheJRU?2QQ3!wOlmVh@qi{>F+%zFo=swe>&eO!zWo?^UQm5wO{dj=nQ(p zrS@^Pi7_Ime~k9G0Hg}hUs4V5c{NXYvbB%u^c-m4^0c#` zob?GN=AirgH}HVr!( zLOmmmG%JzOrK4j1V;AIf$>4fAHv#Lg<&m+~%Ww}hWEvfh;KyHXURNB|A4fceYrNoU zzSpA|cqBo29Ip4js|?)boh5#?T+ePh7^t(~m0Ws|xm|+V=Yo~V_v})sDE|RIW5XjH zQ24CtRD4{Eq~N2631N}1K)lrS$YzG!Nmp5CQ^NIG?kPfCr~)tk!8?#woSPR+iS;sB zYG>zg^grnThl zYe5|+v49ppY>{o`(hgI^{8?%BOh(6pw{}{2T@??Gs^{8)9t%=?2#vcbxpd!_q4Y6{ z?kJu1Q&$HG<6?wZR^(sHmnW|X(=$7;gP|yclTbBS=ao>I`tRQ}|e- zI&>6s81wAaeVoWCR#BrmjC&X254holn@1VYV1A0 zdHoXgrO;6_>NMB1(aVL|saFn1NI1(jFZTbHQ{aPN+kLL^5_h}!E@TFai%Xx~Yl5Gi z=L1K#%eOTr8F?8~28e?>iT@;}SKm7hM2}2NFx=ZdgcAvO+%jWLh+UOob~(n!O!e|H zOoTJ}-)Hm;jQlFWc{Eh>pd$qA{q!r-lFHnn6(ZbX6~e3X6KB%%nEl z12%KRE%Cc(U~Jg#Rs3!h7w>%SC9F85kRDQ_ILBq9JIzwuFhghg4y1;pe+vDVKCsm` z`h2fX4D$b^tUqLbspabhmjo@{^sb+J(Z8&kkesPR#mLPf@&jz8e)triGqLznU?)tW z@s|pyNn)3`WW(p;&b8!!9`i?dy3^lWTHlAKxTw*9ziw|NkE-|&sJ_qfg_#ptIq$qIN>74(svR#k z*c5d`-HAb|V#eDf2iWuUxWdXSjUY|9uZZp&Yi%P?A680XBJb4H#2Mnr8rA`wo^oOW zF17U{m-AvcBx&U)M=T(raSvYWc3nKFr<^Y}$?NoV)Ls2{v}&KoV(vqN_02W^Jolj3 zk&`x!6aKq*)%#Um;+wl(h&mbsaG2Kh-3MVWf)i{5TmM%4mN->qtoJVYI?|j!lBm)L}reIO~s-&OBu8%s{ z_*P3;{^qund%UMlnMyu0qiMzezhOR=!}Rd~=CwRn{!6T#X8N~&2OM##_8H1Jyz_JZ z;p0paC8x?{&s&6dloXt2*Zd;jk$HyYSPHKQ4oqE_v6=Td_UGy)mv%iclg1IeUlYzM zyX?e9ym|^#PLTZVo(K;3f9{FDS~AXH#riP-g|z0!_#0$SQ*TRLRdoNZr{c&v6)cLH z^Yfgt!n+25>;S2kYB#X#p@XY#TV9V3VG8Lb%OA2d!-0%3)Sr1qdVJUOVS#2$r7HdU zU$`=}X97U#{*SIqRdfE%@W{7hpH9(%HAM@iHCAI4xAMPrP`m-gY#hGJuM-J#cYPLc zGt$xkBxK_+&oC3Usb%Y^iV89~lQ}jwOKLRNT6B-$XSv-i3w0CVoHf)3#~)}dPjt2x z+@BFE1y5(ht09z2!MCGKo25@pj&;cazAah9_QuI`r19J0Ci2(pf$TV$@iI|ccV1VO z$NW&mwGiKe z+R)jTjs<~@eBV&l>0Jy;J4hJ~4dt1euo5?HaoRT&@P!+;;jfcuA2 z&xQXnZ|2hdW!}_ri)X$VR(WT8t@C7SJ$vWZW6em|1Ez#CdzX!VuJN1G+svmU zRUg(Onr;Ok^|+u>WP1?q7cM5qTijsL$vMe}iQlJuJ&OKTvKg*5SG`kYAkYe=NkT9+ zbPM)xrmhsuHD_yUD^4==oP}NtcmFCnK&oJQJvuH+Lrgg`+}g86SUcUYVCD0dO7#s3 zT5z?O*139gh7OSdATD=b#$SytVR~G1 z+=Kt>P%-+PSOCgyWwiXrUqCtGcFQhb8J|R%|S*JZkg9cPN5CJ-se} zrsmFzcLKlMvm7#(&Q?6qPI|C}9^m8=YKZqM>MctSW}VYv1x?m#U^V_4BY(yKn$v>w z^QjU8V5@bjVj^N};vXCyfToqvqa@f=Q7>GURDc3S$jTsO*DV5Gl{zl-N7+Zcs-?Zl zUDrm+*5gfX8MAlUu?w1n(2zxgYiJXT^mW*)ytWfp?tHJinObmQ1uz~AiPq}yYG}pUc!yVYnk z9^Dm-6x#IjnLpl&9JR8buJW>(mbuJKH2(ND@0!@=Q`t6T-M1`a$4PSGc6ZrqRRl@X z9=EHuT7eD@~Ay)X#5ZmM~{h!%v{x9^fiP8jgJ_FE4@a<*0u;CwN%*pWv`;Kd4 z%C5}wXU7Hx*o=+775xN5+zx2D+Is{_3Rn+v#)TPirfqAwfo8VY;4wnP^=9j=6gXlg zv&c1D=6U1nejftX{iqS+GW0*Cr(j>H_a4#1At5LYZaxb147dt_ginxyy5Nj2E973A zKuu@j3fRdTJLLPt8269Ftl_D|wQQHuuoT+-uVYqm!E=b9O!JtH&D)R(5C)C7;z4ZB z2Oco7ce7LKTmvbSruv{BfF(U`i}~u`yJRabFEsz7WF`R-X0JJEy0MNYM-bbs6 zS5+7a&+|F#@Pw!ozmJgsQTkJZD~`1!r!)nz@zL!yW$>$f&{8^^P4Q*CSl_PB^w!ra zkQ+bu;WZR@*7ycGz-Y?}>R3PHydvaQ#BF8t{ z_>}70yoTLEyo7Q?n`M)|0!4KA`-+o0o**%uc9V;F-bC16uOUu&@JRqix>PK1J$cti z#sOW#H~jXDB)-PxuElqG@bp$3j$$C05?C*{D$1)#<)1b*+*us#Z<{)yBUHG0By#`J zb+dWb}T<77++~TSDEPBa6z7 zOH_8GK+Uhus&mJ_jOTS74%ko}vcFEp&G~zwoZdDnwJbs8F@H{v1s4~_OeDln7ZrRC zk6V?;{X-&u&RyfPc1A2&nx=2hd7_V6O_yARXGC)&{fDk#IM6A5sKS@EzC1@d0|QxD zz)x#W1Y_xV1M6Z#`~Ca0nE~I1ZDU zJ<&xBGdf;H6hP2r(zq?OvV1^`W3NRgei}xg^dVCnwLwz`{|3HQkIlCq560S1I}Xdv zFCvC{TqMiq_-Y$&=evgCh!x^!M~PPwd|R&^>)4Fa3YN)f6Q=T$ps-ulZXLv6iP7=R zsCm{pD0lRu(r0_es!BuPIR>~o`Mt;b!E`o5dTc{@!D)Z<0%*h>&EEPt#{V;#g}Pp;-A== zndLLz(9#dL_#l{h!CL5*>huIB5MiKMz}SuR4XVSdoCP zdwp9+X4bfJhcZXXJ@eaz{dr8{mFi+wXYYQ4jGRK$+4L8_ul5^|)yi|5c&r*6a_efF zE9m&>a7+xC9|bI^FX3YPZ?wLon&wW6?Z)`Z5o3=9ZrO8;2dRvnB@gaB!dx7(t%bAj zfwQoFp)N_r_+4pFR?_Z*kCu-_NS-#)ljpsI*CXTyVPb(r`N;L3Y`&=P@%{WhDnP0t z0HYJOb9rzEf3t_OVV&i;beL`uDU>g#>dQupV{^ZMd$y!n^AY8P%=KVaZQs>R{cU~; za_1Y*u^Zvc7rGKC>NG{Z-9~NPv?x;H1X5h)Kfg9Q)a*OMg$0}>PCp6_^Y1qC?y!I# zSA=7WLw?_7@Iy%xF$wY@Y1L;eNttoYelL}|WBFv^^4w>0J!q;tOZNlk{PDy4n137d z>L*{4{c8~dd&s^w|SCx9_gao!juAa5v@w-If zB0|AIM#W_~rCA_6^Yeen$k)H{v5%huAIbDp5eYv$+#`&R+GuGC7S6|Eam8s1i{w52 zp)Ce4i)4+G1mtP)EZDw{7iSjcOB!g&_pL?-4Pa$!d=T?a%_x@YB;z zbjEq&*C}E#_+o_*3Da%ng$uqm7mN!RufZqx-3u9@9J>s9W0-X~P(HpLu+#3NfK&dA zD+>t)CrY75YlmNq^vDc#jg~@{Fud(Xzf?6%B^Rv5=0%%kU2Z<5zHjW!X`|jPM!ut+ zRjC_?>O`Rrc{v(zZWl5vcAESwmq5RAz3D%dNn}kE*7u7wI z`|-p(x~|m`XMFZ-1cW*y!)fS`>~Up+vxe5^ADxjFS$}sMbiFmfMrNyT|xs??Cd~jYm;|AJy6YdkeQXt`+!}%F1c4 z6CMSJ_*xs>H~l?{dF15ixv1@kUla=dUhH6A&lu0tyF^`_)|5T9{X%y)(eDnp_goOm z{r8Z9-xlEYRQb-5*cn<9ScXCXPqq~HY$cp8uLG+uS*DcU# zBECG?7S_<1M!0n|?v3}M_OPvti;G*Dsxa4A7_W8M+~QkZUT)E4{4B#BP%>%Jo5D*l zUaUg`y^+d+M#jbl930qEVFka3B*w=-($Ue0m!hzDbj+x)pDBt!QKn#J#b#w?HLo=^Lid5@zz(0<$=!g z`HXVKpsLv(M7TFuwslREb`EIjFA`uC)wpn9q9b_HmZ5S#%6t(ysjyk$K@&9{PuR( zWVtc=t8G(fXO64u>lUN(^zqFXQQdNiiU^9kE{`)&eOtUEBG7AVYkwYdhW$Dh6BEnG z&Nk}ZNE~wW^x9l%< zb{p{a+tg@+%Om8ne+2hDZh8Qco}T_GC}{n(VQNYp+!RPw=_!Pv(75>ig9k6z*>R8F zXB(p-BMU1iJgu*`7p2zY;yOC(QZWwoR)=+ZE0S(O%t*vp;Q-NR#HX9vw+$min1 zm5~32)Ya8hvnZrP?*$pzeX-1;g?r}L7q=gCawr|QFQ)xWz~;J-3e+o}%F9Q7hAdY* z?>{{_Y$6yUq{KohHR{V`#6y{=J5`KpN=-}*R?dItJ+QiTaV+6}GI@)(sfNNG60VTj zmL(ak()rrRZ)uf(pg9QhB(^(`rW3Py*R$%LEFUK#rHs;%;+~~L$z+@C+3U|dDn%@x z)!>^0PzjosZRO?V;n4zpJ3?^p?(9U9S5w=8WE{Pyx;j41t5@4t8(_Iv%v3*daB%o6 zk@ZGaR$NYQ(EGHM`poGS^%C_!1DM(u+Aw_}_JzO1_ zjxo2e5D^zgG&48vMj&2^zZeqd#&~MmX~SYZ29|Z{M9F(n9-f)~8M~&n(}va5TWoF5 zb8IkQhO#c$EM18=#|k@U8&VgeWN-M=rvqAabyxd|_D&k@f?bS^C=?VF_NM0=xwbk+ z5J~DIsyUuSOJqTiZoZHwOjK@f^9`8ctn?@Q8mG97E$=(@4jY)Uxb|G|;cbp(y`&W9 znb%v;?rAf6ojy+BM#4ix5#{2IO@m-%Rq#T~f5L)7mG+@ho=+9M3lG|e8Bwnhq}t(N zxA78fdwV-NMvNlr{r+M{^k^l+YvXqx<}D=RB&)+@Eyn->HjGCY?D9Gr@pI{jg> zII+w94<9~M+bjt2*WjW2{A?vai-Rnd`2+{jb-@$WO$Z)Y+M+A>4j(^^Ese*Xi^!hK zW3cwCmPnYA5npP=Iy5Kq+cyKbWlyRE&W6CiZi6yG%DnU*u#&!V+C4<)J;M0%<;(YE z>ehCV;&H{X)5!h@q=qZK3E%VbTAOaKtv3=gPfIE*ai*)Roi;p(k?_MF;)JmNvVLEx z>Km?)@(@9a!tPBKnTE$h-}miHFdLV{zBy7d1LpqJChrKosUWZ`c6+-BH(@!O#RSL) z*x0PxamtPQT4U)n_Ku(fp+TBfRu=aW5i>J0^_y20wzpBiRj;fV1rGRj8wfqe@CHxn zXlQ^L;wnXT`=Rq+k*poNYS!3dzki=TQL4|4LmMCQHtOTh$gkRzy^GBzVa=~5s+K}I z(EUvv>wpfu7LwaDG`n)z&vOgg+rw$Q7(QdOn(;MGO-84P zVxqVG)bV1X=8*dA5_xc9!aoFe^K%rbm~qb2ri;-gn{9PWxaSA= zqE7t?4<7oarmU=v9Xlr{b`uu$Civ#LIg-iA$%!g!l3^%x`=Qc9U!U*asR;Q!zv6f0 z<>jS8*D(kPHs#X8BO(G7az(VXV1A!hJzr2zJe-}K?SeoyR~PLVX#I$Yh%k`+J#cb) z3RFu!Dm)rBW0jDU`~c!_-Q(B1yhQKczt?5ND^aBd(A#-B=Or7X*5Jxo-H@}kva+Hu zunnG7F+z_XCe{}E>@@~1E+ShJYL-G!Ss8O&fg1=Bsy|Gg%)nqnP^j<8iKAa&pm65U ze0vb)$j6`--RI=w4~B+@beR)AcN=_uL*`gSWx+-O@bD2jIwD*kH+ALA`y3Y9utM!sdE?EXFnbvU~FW5jl>sb3w89{|%6cZNw`il+o=%h}8qBrc;vwq-DPPV!C zCXFw@`!|o|Dwdj$%XBRx(dt+cC3WaAIc`q{fw3ykzGk$wv)i9CZ_2vpc|E`rmXqRI zR;BZHe=XB|Yh$YJG!@*K!FBtIicQQqt(RV2URR)Yf&SW9U0*_p2HSCWR$s;FbVnA0 zghaOQ60AfLuXbj^;KvmM>UMaOK7WDXK_~b=-9WA?6hsb1n-e&a26+6XiL&y@jV^Z*d-> z9Bt{7v!-XZZQPq*YV(y_^U0C~*qWLWayQ|x_8cuHzxdBxQ6Lhq@2poC(Ckf_>i}{9 z9$8WE|{zI zv2CFl2D>?02IJN`Fo53O-Ob^&LwF2plqi>Wp`)f2R8hgr$jJB{8R^p&h|Wzk(z9R# znA_*LxWKbBXRun9z@r!5NJt3T+A@a|aX)OXWyQzFeh?(`F(rjqDwzvgGM2W5ea;=B zLLIevgMSDG_sIQ+HMma>h3!Q&VN> z&MY7L)u}bQb7o{_?%#nueVqQrNbcV0Y1ON!s3>1B97}`g!gr8%v<@wG+*ZF@BDq}~ z*NGtHyaz}aIAeiU-5o;YTzJUJqBmZjXmlAcGbTs@R<3)zFlFn#ArVW|l3O_6imBTLk6F%}Yl!rY%{XX3WdOt(gofrJ+sAzeX zY&FD1Mc+@cFag$p0N^M%7-^>ZLtB>H{f7@1{c$VP$_^ghoK(pMWX%Wq`N6@_&=d)S z3>MJ2V>G{jfa)UgQ030WL(XdF4;ra)qo3N_$8!e&*=TKRL-o47>~gGBPP4 z!4}j(6BY|tEf%G$c4r8~?4;w5ZMca?k1*15#6!TR*LL1zmX;!`6tl^EYVqp+LLSfK z!VH$YrIQDMb{ji8VL`zb?=igTG#)_X?xvlxvNBj;XoyDE*4BV1&I7`YDU=e#2G4)~ zD{90TV6VsJiF~SuhX=Ljv-PfkvbR`%oa zNelM35@b&cG^)_}`1njLEWR1A;-LUs01*!DdehRETNjn4u?{7$&F2l7(1#C9*zK-2 zS1w=xUotZflN(Ozl?u?uWmZ&x&7q%wkAWk!|1gEZsUqNrzI*~8joRD=toX44HDACm zzVf=^fPfzv8F|N>K4Qg4kc+$=itj>!yXT|ZTp-!swI#*Pt+BF`OcW-D;3FWwvi2C9 zx`n{H{=G4W^W}Dx7QUN!2!J^dR%kV=F#t&sQd4^-lfv`1rU4{tN-8Rf*O2}CeqUAE zpXtJZU=|vhjlUo#@92LX2@*jBL^L@j_09;Az&B(cRcR{;>Hr1)&U}1)JW=cLYIu0~ z`@D8kw}DgqAi&&+|F5Sr4~O#q`uM1jY@rm{;#Wu_WNcp|%_PbsGz!_WH>7MemMqCm zlD$cjWg@$gWXm#NOO`Y!G?s+1@5JyN&-MJCxvu$Vu9^F~@B4htIq&!D%yoZ~EFZIh zB4l_`p|dyp6}v3U&QDIdzu*3rr9U!09v&w0_#i?GX#h8Vbd=Z5!NIyCT@^Cn8K_aj z%a_=prHF`#8?aH$vAcMtJ$1~jB_!~jb(cH9=LFb@jV;e?oA&Y^Ida5^;qygm<4hu#jOjKNr&F!SQ(Wx3%=>w< zIlxLObzTpB>rDB-9sY%}Mm7TOqZRk)=hI}ZY{$ zn0>!=#>S3OSPh?ibm=qfFLjXc|2t(Oq~XJ}wN7YSogSN4DDhded6$!8ySu~iy5j>n znElwfMsJ!Q(kiFI!a@ExC=TfEC>Ix*WP|Jmt9B^4j^#hKOQQ$%eNXd(p1NI5W6Xi{ zfh7oK)hL00k!l2|$s;U$AGsOD57Ou$J9}`GjW&ofh`duy^0%o+jm^#CDk{fuIGpj; zTKN&n8oU5lW^AcFCvNCd=m{i_!;w?Owd8Hj)bp4a6c(GD@y(`Gg_AW++@%#Sig_hUMMwo^R0~mRikabUsX1A9%HF8>O7n_A8 zCRe8uHs0K-M}#S42dpclne)!)*vw{CG4V>#~Lo$h#SV4#E; zhV}ZrfT-)ipN4e%@>2Tz{$~9lKhaxIMuX?(aNzmIy3wNbZF02-NX8Lw#V5YCXJ==N z%gS<2blycoqtJGC|H%q~t1>n*QO#OOGeEUoOd$}sQw&h>-vhk||JWa@`QfU4Ghu8v zzrEdOV`E{VRi@=5f8@Q3k)#&8pdYT}VJqqAV1#kIq zqT}+FD+dcKs-A%Y0Nl|kSzQ=K!!L6}H|9BdG!%eDW=RPzZ+N3qpLp8RP;ak&#T6GE zPQ>dh9M@}7@z<>>?=tr?>A`g)dt&!NVzN#zYrp$To-o5L7NliAY3@&9QrT*%N6}TZmBQrAgtk<)MFj4>Gd!kBO`l-R` z{e(`mj?S0l@~@Q-Cvu0h5ih??pCw~U!JOo@4g|SR)9G8YN3Q4bb7Jr z7%zS?^gvsZVFRmZKE+VWf-=>UxW0{q%#x6iVb|JT2yPI21EIphcfIp8+ zidgDF-T-;q(AlNeYVLG z*{R9Ni-v}=mymLRW^x>9H=h3l%YK-jU-`{Q_J{CNHsOOty;Dg2Rq(gHm_4(ozwx0uKIW;9pl zI^l}!s)481bH=ig$l}UghrH&t`_K7-XiFIqf&>hQ6?pJ*wFn_#$IY=^EobiGSTiSp z)_3pT)%Eo7c~lC)DnlUp3MzLrV>GV1XO12@#QfXCS4%$(x^i%R(TQzv z9dRis;UE~5kVpyOps)~Vx;)^bgX=F=CP!6hte*g>1v)sFMACL}kQ^Nws~>TnDd`>5 z2Zt9F9DKvVg0uYgth;zjL`0AgDa7D(=?Rs2^2XZA3IHN8Wo7$@oxY(>_B(!?LonsP|d7vy3vcvsv`?^e`l2C^>tr|D*Z2a;up;s66Uh2)>3$ zAj$%5{XS#Ru(KhoHdLT zxEQ_Avk&v}?FWDA%vuVEgJ{4>)sHqsW*U(&ckj|VH!?9&m>V~qrl$+hg%#4iF08Iz z26tZ@Jm9)}4^VwWLqo^mER57)0fBHJq1u@kgAJd8j*jbi#%jPqzwLC<{ee3^lbKZT zU;uB!jL$TkaNGWswKYEO7b!`=@7wnjar*;*&h-p06%3-wmmn`S%oTb=4A zXeTG>*0#3uo^<1xp(<7oVaZwnnh2{-cy_Pz7F5~a6B8rbo9xh&K^pUdZ(>47UhkK) z)D`Cu@$VN*E3$4-^opp|6))E35#r}Z0g9!Z=J;G!$1<!c^h&XN{za!Z5PG-W~;R)p5w^y8c62?bNALSAgxKP|t?krnd&r zH~-L`UEBx2iwWPBfaO*80E~x?&D$RT?2zy_$LQ)yKr4VSg^jl*-JFlMETljI5^9E~ zcNC!3v#V)lSnQRoc8y1qhk+J8D|hMpIh0p;`GTArH?SW{#8H{tvIw;du07*9s0xDL z#@2R`o>yqpx;;S%z#$9#SE;vrR=Ux}p3|oE)E};|yI4QZ_Am0RJpQK_943fkBL!}X zNjZ@Sj|(B7fFvjK%;I81cX{+**@?q^D)N}z+y%OtsI6l$cA@!1EropF?d19MJOK2o zNlLrDa&=zsHnz4J>=dlK--dc!U89K3>zMsett#Q<HkKY@7jGEr{t=4{$72qtL?MYVmN0ec}*|8(Sg8#jSCn| zhnhB6^jKIP2X(@OJHEfIwiuDeBwx?5Ub1s5!g7>Vtgj@bP{!&yW4VdlA!KFdN1s?J z-X>y)0aXYCNM@YIi^PT{-Srpn_4N(7ZIzBd?DyBx)i!>qPw{DeSg3dHT4+TD(tl$l z>;|^%Q*hh3P9}!qlOr1kM3K$`+nluM;+6PVM! zKC7xyf25XiN!0Lk0-H!IRJ-A#f^PB>mDy!6S65|V`F=+ z39t&M$^$itWk+dem^TDrq;&oLH5cne(R(2e3`9WsuD_-Vh={~E5d8S(!HN{&i{WT6 zT(DLWJekxMreWSXFmU4bhhFPe^tmKI~QCnIHvMfYHZLC!QdkC-x z)IVhX{u*_1Ln54V!hO zWPpu@Gz>8!JyN}s9R22v*utj9_daBl-uvE~hwqY-j(vB&keZsxlAfj_#1*1rUAru9 z-juQh)IdjUm{?HoYs!J@{QzH(Z@0mZ&+OXIe(biaYlT|{kxaHV5d39Ic`5yr@6YA< ztK)H3S-F$t(`0iCMOCCA=3KtKbBqX`%;Q})$Dx@-HYuF`Vfjp=(+Aj6F22H}65n4! zf>_Abk=0n*j+{@;HGZ$XAAO&%P;qkgul6ws6!F2oR`uOF#BL7K=gCK(PO^@4zxY88 zwk>_D+PXOt)lO8@l+$&0myZ!r)p@za78n-B3hBgz{qVuov$gy~OkiLj&k#DNM>V5# z&`v>B18kF~oBVMUJ~mt>M@2n*jkDh~^r(!-p1dr=8i2vtp3tIkINg|=nGt%h>jLoT zpr@0hryA)d0}N;kw$gh|v!hk%c!SZxojc?5DeXQZ%q}bmjIC3)*~0^LME(*=g-)Z{ZP<{O*6*-@7(ItB;&~a_=Pe6Nv_AmPn z2qV~H#Q*!PxreT5|0t@Zqw@+(UQDY0{{UW75y2mV9W zmlhWV-9P;K+3-6Sc;%Umgql4Fgt`0h7s0mQCmwha#X(Z`6Uq_-%1b<+XOQ+3;3Xo5 zPpS?g)|Qq=Rt_K$J0m>@Bg0qDW)7yW#3f}vtN36NfIzQ6lA^+jE;D=c&Mq&<;ho2q z7jIIcFkfeie5e)v!22B4_xpFG7cajE59j~VrdKIO>Q%DPEEZ}G{5ACg@5|rd4}TOW zpDQ3ed4llKZ59qQU?CoT{ftnkcyoC;?RsFE&BT}!HgCTXrgwFQbo*s#p-@ot@yZrc z<#pf!U3tJ2`))he`qqzQ4s2F0-pb=*KU{fM#<4z2+2ewt-N7}UT!`Moy38*i&iTbn z>#qeS^PbB7^N>J@fZ&LI+DiyO1gyS^$o#~mUa$;JlMo%bGA8)plE7dkL^mXW9J$<) z8FzT=c%7{C%zNXoKQ0o@7WX%L(_s~EmNtmKWal0l068ru^t9#TZ|tGArtynZ3!Tuz zEXZ=g|3{`*iJMrsK`zn|jQ&;nCLHX+#NH}`-Xp0)2R57YfWjm0L^?@a>~}ts`%lpc z`;FOsm)~R%6nFYHo3`k(!}lZ8CRbZ@ne~~I9jtJsW6>xuy2m(zzm}x4{2OJpPcrg~rd|Qh;ssux2Cp3D2|A6++to9jt{G@v6)ImW~%?< zRIs`3*+o*U<#iserd}Cqx}VNpebi_9(O1j-Df{BCp2++%Y?JmBUHp`D$a+mMEvAZ%w7P<1*T(TtX%cs{@ zPOAfU?xQ`jBlX%?g73Mgp@X~QYP6JKwF6)NnaT)SURK||RLxX5TC&;dT%AL!v;EOXbWcViZ0414O~-!`T?)ZTgQ^dj@o|*)8cx_E zdro zN1fIm?&&rJ5vCs%ys4E4>s@bIy}m1-vp8FJ+DM#bBu^}7-pFt7J*th2zq@Q%rEFU< zm@@L4sxe^_+Mg0P^-K74$wY^dG3ht1jUxToR8L<;0jBOgPb^oI2VaC z6e$A!M)_RKC*PadEnjw?ZIUlfprp5eWWWd5$(-K}7JWoKs*S0tO1^vMCMIRq2#~+k z+mfDkhztlG!(@^N2etQ34rMy>kEVltF)ar(ZSN!o_J@2RKO_>0GzXjeJnF4zwbVI& ziln%3T1?BGq7CF&DZT-TFdfKm`ePPI&ve{m!JhXQC)mG!7L;}c$H zx$>T)f-?U(ymqjdBu*LcN%!pbC|x26%e3V#8zJPn)ejS+(qUBX!J}fLe$QGjL9~7_ zTBamJ$QLs_sVO%pM|QOs_6zz7z6mWZzp86%89e1Z*LaB;LZ7eYB`K(ocj35}&E~L# zq}pekP^9X|@Fw-PBCh2QW3WCfKk>}?dgoB>w3QLv7%i))wxaDqcBVE(LtIOF zr%Fs3By!_GS)|>j=DtUd&|Gl8OlxOnPsF&K8|7dkGF;cSw$Puch^N%W+AUcZhi`;A-o-!IMd z2E8C7YR3J#*sQq=re(3)Opw z_qFC!KFRjISD+=-I6A{N%!HygmRyT3Iw@XO+Y8|i=CIlGnt)|v0Of60&oZ!tE;})i zgWe%dZ^_dHd!`@F-j{2Cg0ed0b=gynxxx1ZhcQDwx^?sS&P}^U-L;l@<^&?t;@XfC zd+z)C2+(bbBVU5IZdt^w0tAf6wDrkJj?wmfcNi*NX9MDDA-Hn)c;5*b*uQu&zJoD(QdZiUcsX(`uO|e<|n-+Vq0=w>#?5)h0 z7wrDseR|hEmXOY>B=Jk=VZpD{G1v1nOH=ZyXxoC z^F)=K`DtWu%!VBg-TC0YZ9bc8lpw@|!jxT6=m4@|pqfX-H*kngc+-`r}ua}`!E9&cp;Jknr`so5VoFq`E z`$7wPG;-#Q8WzTY!v+CU9HcQPxYLFitz40GN%JXWI^|hdM|Q5iy`^&EURN!3uU|+$ ze@A8AkeHa;fY^jFsqHhU`{y$(p`vg$`ZAH*Nh~y;>K#1uS2%IUvLpVM{>y0f*3I)J z8{X-YQ$`y8BgN&DXIT90kwS-*&?cY3%gK7ro?Tm9({gNGnCHlEP)hiR6bx%>aoU8L zSYu9NTX%kLtpO=KQ)WVzjP|c@LxU9+J<4g=tw)0U_AC;LwFgNK`#VUnL#ep@it#;r z9aXT5B<>rh{BAEfJ@5LRr#@2Jt{J|3*GWE)Dy`eq4U4{Q)ToT`j*^H03!r~U)8!+t zjx`RygT6hOE026qjU}CC%4u~S;*V)*x{!?xPK~V2X02Zj^OE3kBK7VHw$a}a(_~>XoTS*!u?{uarlbM>NS|nYb18f z$VBnVt8OHgJ+`!jwopMWqdRQca4h2`_I0nj@eVsaxcYh4a9B6KsIH4v-vlU33^&8G z!5_PoalI@QhOlH;qgro)H*plmZO`^R!``>qn~jz%tHxA+*`fA`sOd5YMTr<)jJ3QR z0Tm0a9Uxzu0m01o*#t>y)9VRe(&aZ8b&s9%T{mCvS?TM2`ihyIcr+t$k4Of8WQ45*1qp|k_+xsVsV@<^dXNE1 zB=$lHBgpmo8lHEL5jWeIV@X1Y<2pgXm(eM!`UkwTQE`TjF4lkWma;O4VMxxQ6`z_$ z4a!g)HgZ5jXD!z8Rq{q^^%%m!UGBPmEuN>tlT%9@rkR~HQV#x(+L3Y0)yM7?PFY6>8e zWj2%yWZugv=*y}2e5zGmXlm>AiOm+q1|!Ma#Elq{62ckoM9tr%_=Xd?A zss(^~@Pa_?Q0Y9+n+mh$TnguxMHD=H3dm>MU#@QM1~^$|1yJC;(KT4t*xAOXeXcU$d_P2yJMuF;DZ%GF1%T6piWC}D9x zmNGCW(}&>nQ9l{4<2I?m%Ai2l@%eR<*vS8qqS=e`2vcuOcMN-}po)~^z1)vh5E-TUXzMvJrI}Z(gH%_m? zU)TxJ;b1W;HD3GfU>wS zN&H8n2McPXOU1dykZ37SxNynz={zA>kwUZ@$W3eagBkY=E(pa~^C2{pU=*72aKCq* zU0Rjt3>skLajZJ2{bpV=9=mD&uyiW8_{<@pRAt%m z#pA<=w5{i~rt#CtopPe01I9)d*&)Fa+fP1bjiZi_SLvPqb<`l^!^Y;BO+pa#yuh#Y ze@v^ph@4=fhX#slk3R*c3d8}oJzQ^r1o+{Kju}f15z*8pz|YEQIqtq_UYG!kXk?#MDgx*TdYfXLy- z5esE0AMCvco!6u;)v|cMnjVXuE^5N9v42jlpIOlza}hq9yC(;UDp$Scd_&wM83@$ zveon!r0-_{;dhb9S*&hsAdjuP&p$d(gwpGdmQwr}+h+V|(F|$}GDppM-1KdKOvUnX zDtbQ!II5FbjxJJuJdMeQLI#$=MHRBlD{k=ZQC6L@j5P?>YHde z*U`FprW~6yL*mgNeW(^y@9!wi85QFm#xBmw85@q=T-ry(8tW$Ph;Rj^STtN{45%N! z2eYF`(9OY{IU-{nZpHD@?_S0>D?<1?<9pFk;GG2S&=u1KTm5~OFjGgo%XD`XV}oPO zxqT-#k1<(`nOnr-hLa~E$XH|JuO5_VhyRup>0XH9ip!GEX?13|<7EDO8!J~q(Nqs= z*;9S4*=Nj1Q7)zoLcjOvIXIheyyq=C^^a)joupqll{Xw^G=FBpfVW)^o{VhVQ_Utx zPyUcYkEG~Wq9MSxC?&vzKAnm!jcgP7)@{E|Fb)4bmLbVsfuAv`0->Lc7r7r~GU?g4 zAK}i%Hkb`-u_`^isx_(cy!ND?Flg;~gU@&qXwVsP+LK?rEA`^RUU;NmZtEwV9?KZl zvxO#=3$E;5hjRZ`;;#;ehU|)IiE8BL|i&|tub%L=$m0laUW>#o4{rW zwHWP9>0j(iAM#iE_I*J$#xXN=VE!IVQ6G@+);5qAyJ?xBXz(sLPq3rYzL?PM1c}@ zc!3qqpBk&MQnkFS=N}Z5hyCfnGzTu;V7y{rNV_!l?9*UdFgR?yW49WM5=Mkgn(i$o zEw9Pi33?cH@HEJQc96tIE46>?@4`CH-(*^$aQu1jh%Oxt$=MQHHtYIx#tcuENZ%(Y zcZ^%DJ zg+4rMEccD&R!KDdofFUN=H{>(IIk567`IS8f&aC*V|)7d5B4CR4Ny!u=0q~$C}0wx zMKY;u_dCX%j%gWXM)R@AYZ!jH?mbQ*d)}1dO|q}f!w%P&qO#9-ro)^4keS`aY(Dz> z_0E;l>dTJ@VBC}dZzk6~T6a4xmnw#)h8cuC>LGoPyR98vyCritN2 zgrlO;uB>HKtq=c40E(>S505vAN+v%XWN>g?Qvpp>vf@(dAMCZbr$02!p8}?Pt?^m@ z-`~glQxX>7#L;ngI_MHf(K%8{7ueK9oUZ{d zP`hGfMbk-9uV`{Q{y7>^*#Gwznda`q@~s!*1$YiYZ1r6l77`L>ZUdat`EFHNh9;KL z4OsR|42?mhhKCx6hyy;{rEEHuxg4=ui0YhEHf6s1gtJKvbJ}=7q3M) zSM)lTap!Z+I^&dE)L_iTs*QVZc>f^NQebJd9y@W8(BW704lX8`w5vq$Jv1|uV^)<0 z7rC71J*#FBYiXq#Muo_y0TjRHCORgePQb0NT`o2~j)s5NUOj+_WO8a+V8&$FV8`We z`_(^e%0Vi>8=B#3h%#P#I_os1ppGsHbbPdqWH~^d+Yf)2u7Ns?>$e*^QE~BOKxqn} zmp-oooI;{7(|yEzs4OigDIMZRCeA9+&(LJfYHQVsN^@b|iFAA(+FpFUPKZ`TW;oxN zm2J2Yo3Pnr%sKq3Zy@#fE>ERXsI+A&iH;{VUIn6cxN+`cDDg4R(Z!^nv5KPq4M!cQ zdQ#eB{xi)PPtdo)Z^~~j@{lCo!0qDF-od*m#rm%>Q`RrFzvvZg6+=RNEax*m-{Y5a}<% z0xoS=+6jL|KPM2-D3*cBDJlY8m;HHDXGvp5AdeA@yQ z^^e1&Evr@#ZeEfck;^tGrXwiRbwIdVgl<3v^R>Sftp^+L427;=7Ab}3vKm-TVjed8 zDLD!H2Lzl9-Cf!oW(Oq+Y@%5a3VfshA_q`k`a@fYOno4?yi0-gf()lg>6z@-1bwoj zd!&7LerQsuu}bL`ExyG@PL5D?+F|auSks47Y~Clvu_y=L|Cx}$ki(!l`5AMH#9Ug- zpg3z!L92-*>{(SZ(OeaR-%MNSodh!1fM$1}V_j$)aw`3#{s~yGN`qlap0Qk`{|B7? zxb>2R({@ebc~(&!epade?*z-GPyX;js8a0D^|dHY7t|W2hOj(agD2Jya=&@Gfv@th zp3ePqb!!5)z7?U!F_#+}Ka#mRosM74LHDRvYrm|?ra1M4{I)+QuJ4RUge;`R`R@iX ze8Q>d8GF6CZPMkr4!*K0D|I-KX&P72BVk81C?PW#%RBFJ!z6uWIrNJ3bVF}@25+|A z?`ZE1>2kqS$xSefoP#u_zh}<%^c;ce!D$2CMh3iTnRaC&H{c5)p0s5|%#N=w`_($B z_lfZ@BJjM-I7rm)mxfq-VbExSTb(L0#vQqvpC27vHcl7=9H+pPDfSs~-@HXDBbn-} zmLFhDQ!K<8Hn^WspE@*%OYD-8k^avZ>m(TZ%)7C{UHMkUcS^OOwA82r;=K0?*v5An zk-WV2*zkM)AopF)o%%CMAFc=-VAj`z@SvZ^ zt8x?PPqztlWCm7apFwWz!q)c=Gm_*w+#_>!&1arY=E`z6YKa{Q9U^974@Ogc>fw*) zCA3hcR4MvX>*TUyR-Z@v=XB>Dwccv2yRW}B#D`#2PKvkibUD&@->KH&pba?9ZNCSX zH^(^7PSca661m#^v~LS(@JOgQ3>(XoG(_FxD(oG|O2SJf&1UaLxfPjC=K7|PbAGxv zFTGH){RzGfX00=gnv`ztqyRkQ;_@j*lj)nI4iW^LB2~2pjO}xUqjpUh4B{6Ky@#y= z;a{Sk&N~taCWiVlK?@v8b7{iw_HC|EL9U~@v9LXR2&eT0bU#z%M+#T|1h3+Ox2p}k zYH*$z?h|pi9_p`d|2#Q~c&Jt=e!rx&G%X#U2E_v^KX}GI{ZLIbSYo=WmB^$3bJ*$r zbB$&YTFSJOl+nwVSHHKaRsUYpC{?`{K~1@qi#9sgpXC`YzdXDi9?o(Z9c|uS7tLt= zgw{29*-6Ty(7>ZE_5B`RoG$QP;Lr42v(Rd;duP#CiILy50OPDy{p z7`H$=n=(q)@V(K@Cf_;h$B(`U5|TVpE-tF!6N{S;PjrJLI!0>jOV1M)8cI`VOWk&t z?5^)S)%m+QrV41wrB&7=Wfk9a{E^^?#oBsRlWH>hc0^_^?Os2AnVDAKbd+0{_^qMN+p1+^)lEpp}-r-%1hPONTtRuAWNjLZjUaFRXcp zG<&b`L3{FoA9g1S%_}g%SIII$$de=stQSi|k!?$#A|r3LJ}?gR0GvZf`?`5ej$7p9HfP=D7HT8>&swEm0zry;PdnpI zug$h}Wi=mVT;Qvl*6wdb#Ih5(xG#M*$*2RMg=9-%eHA7pYNZ#4LGGub`>nq>7R@!+x6(L1tsU7-0bScp@IrG*(8P zfVtnDbW(q>T|7~k-+&o!hO&#~X?r*qV$j@TE^uIGwrwFd^YKu7{#*}GDrKcuh=3Uh zCrzo%Fc+x)WM>zhh`(g)$zQrE&bqrSrLY5hdCfm*OjUa(!&N%llkXD*8T94q_Russ z%5-;y$y4imFm18-xMP~*pUFy?61w&nX^-(H0GPABAU|oZ02b;A2Y+C*84NIngbRU7=(q39&g zS<-9>^v*BlU4NbUpcGjF9{}PDJnt^&kiy7iFYd5(t!D8>mJZ8(l~u^zk771Q3Va9F zPcpEkJW9|gaoF=7{NfF3@_8??VLjl271`NZsiKo7Zx3*}ym?7=U_ZeT5i3{6V|L)o zqN1spzU7OPE<sWn@J3{i$FsvGcz%WfV|$9||vlX1d<0Yv^1`n1dOPr@G`!0E0nI zjfu2z!E`$rMIl0yIp;{FzujToCPi3{*^_&(TOv4Yq#RI6?v)E=0zD-xGpPICkn@@X ztNy(<5 zC#7(->+;`)7+oIB4x%wNeyppqBy{;A-V^meR@}^8m-b=LL}u> z+|pw`^QH;Whsw0yI(_S|X4m%URw~e}3?#CKQ&1I=DI03}bSh9;?#oKI+AqCA&POt^ zpzkOtqh&p=(R6V-XxVf!h_Lo)wi4P`9ZVN+DYs{?HjP+Dmo@*A=J>~k@AignA?{@f zlCJsK3j@=wcMUq;+n3MeNik_f0=4^Z4!KGVn+eXg7L+nJ9x-_ieg?~tC&eKrS6(K% zdGLeKXBd<;?0?jII8(wZLc{-@*tPl<_;bC7iXY5Z>txA8U(dP0M4vpd&}2D_A-2{r z5PjMa{Jq+z890+7%lM9DAX9J%94y}{ct85r1JbvTj=%{7mffF^xmOJPHLM4GsWm5b z7hI}G1xl5Mwl{e4O+Asz<39|+g?P<;-~andK;zoF;nTv|WbfR>Sjp=z8Zi!^A7lW8 zh#Ocx)@Mm0J8ia@f+ST`I3QyHeWx(kX2vnOP(;qRNlGk3Muv$RV+O#M+g8p1w)X2sBc zt$T>l<-ub)F{Wb1_!cGq>IkOevb}Rkv{B>@%vQWh|LF;1Kq?n%9;@fcWHXM_T714Y8$cgLrpE9xcs1@_~@PrpQ(*d&7&pOm#Oj9w-z!&1AWwc z56q6OKdif&(VirH#xGN1tC&~k)#jW!MUCsP!T%*IkpKN?Fr)ZC^1su)_;n$6ZXu{Q zNykFqu&E)uUfzsov3^gK=1trE;?R(SfrlJzlC$j7S4ZD7z3;WlZk-p~=c*W%?Tr&_ zEia(t9W~}0!dYVYYUCHq^fq-86t-ju{!e`=euy@k=tK#9!|ymcT6s2;I@ncz&e>O} zevMo4esFG`p(Goj1+`lbYrTUewCSnl47fZ2SFgE#3xx$urN=3RBeE7$%j3lA_a zR|Kztql@DV2|zBI);i%zR)8+U!rF98b?K?1yMYu7AO&((OF~BW>DO3|_56F);Y?wt zx!E?q#F!>h&J3M81<;%78?nH(E{AxI5F&# z2@JaB*n;=@z*rRCJl8!V(f^sB4t3K5PJhCcn_fX&VX>)~OM9T(8I7)Bk5-Z_lRC@n z8*(>mE9`NnfBemhr^DA0MXmbq?YpGw>~)@!Bw2RUayAnW@Xq+Lycmnqh7_jBI-b>J zovQ#jQOsv^vIVvB(0y?>$$rMB{c{{0uDEBO1Isg~$#@~2=E683P*NikDH>3gu}4t< z32Y7jYw@<&?vIki*f+(A+t&3Wr^d2LHoN_J=D1%i=mWP*-x^6|8+3C4=_#HJ&5_IP zp*@hNVhOg*B=J>Ytj0dlMNyH+DkRFny2Xd&fa`3VdehivV z8PBoqC5+g7S>j>xJkdg@?>_KyDVLlfv`wf~mo233t0@JE@FdN!KeD>5OZ^^Sx~xOj z;bkC_T0cNm__Zm3LW!thRAC=4kWXY3$^P0NsX~y847wJ&>j`I5i8(xB&eibHvuNtJ zW1s=Y=vAwisCgZFa1ez$wRu7HaK$6CkG80=blH^)VkbC{C}qM);voZxDbf>#)q1Fy z9bT7OYy9UGLf79TC@ltl6Ve;o)ZIPPl$4C0mO0XL_AOkyuqo6Cdi@sF-d8B|d~e#X zM(r!>VLi#>vB4+(`8p?IZE_+iF?{(F!P}oDLB+pd$}z)Pfu}=M^32W)9@LM`2C0#3 z8Ua@|6F2r^w5CSfY4jhpQLv4I)fguXH22Qu(5;m40gZvE91UsugSbK~-Xto!q2n z{pngxqoiD2qIRfPXZj&;SBr#eg^iHqN9g^6uHYu=L-3mU%r9wwI3Kjxgdl^la$6i5 z^YbUD_j(f-KF=&neHwTJ>%L>?Vj2o~Xj}m%J`ksyB9v#Fo=UhH zmWnEN?`e_~HkTh26{QPOP>7h2NJHE3*pM$k#&~brkmJcGBTKw`wX?g@t)u%&j{@v} z-h$Sh?80=-w%7_J3LEwVdm>Hc@)th?Qd1iri_`!)AS&S+zyG`rVTPH-8?ats{qEh( zAGt}A8<5*#p!NGkwD`pPt{dfVLqCvBx&oL+NF>?R6e<;7?r;!xd1cFw@_n*fw;bnU zq3*{Z4~mX5K7t5x#<9H~EJBMtIFy>1`2#`V13ieTSge`1DkAnmlwIK~(Or&VMf+1U z1x3riTF_nvBdbE~_2u&mKIDf0Uu(ELO6r|9bzrA9fljvGu-)-;K9K!pfANJ=Z^kt_ zK<}P(E~xJ8g@Y0>;VABPT-J;=clLHi>8WqtfG6ky?vh?={~Bm7^h;`N&F9xKq5~&} zO>Oqca_UE0%cn(~y(wF5*Yop75oY!-B9)I@Ml!BUTeSnB4Pl>Vy>fK&Rc|VSdG^bg zEQoq1TFv;moe?Rlt^&k2Eh)E`LPp=8{+fWV9Rn%P>A;Lu-$F%oo1I3bg>cY?WfE-N zqpYm#FP9{2%OLDa^whnrc#`b~CRvkill{@;%CqEI*$sx(BV$8d=&qaxQIcU8aX6FE zeOM~Ne`g&ufW$y!73cSo^>`W5@bDK>p>*|w;+t_ijTD|aZSu6u4C8rS?@Th1~@78RrG7cSGb z?1+h-;vVz70l2ZLMQ8l9Fokj!!k~gb%M};Pa&*(W&3HCrj;%-!v>WYnhR=aw^+S^O$&KAK0eX~?1 z{VeUc3QX)7i82|8p<3%m7uM^wBPqC8Om~3@nn6Vk+-t*4_SmzA1^Ypnff5`hq1>cwC`s+Av$oLNq>+ zgxrw2{>!z19nkM0_PoJZo|vV@d7-=dJu@-&!kq*Wk$IvR%jeT<-xvftT_niXavs@= z3&>?lF&Ll(^hdSPgd)2yPFhZ(LYsYt?_+)<=$5W%mg2}5`yOT}u3IL@ru0@)SsCuv zV#s*i>ik3Qv(MhhuEpUF{cxkd+WE@Z;K3a!564Q>vSY~|^;jye)DikseWz$q1LVrT zVmOs*`DZ^N_IOBKSbt1Umg)K$n_pPic;klMt5USmEQx!)Dd4=b_TWx{KXI6S*yY|s zz$X?*p1kq29IhwIPhg@v7o9PqHLaZPT2>i;#i-22`~4sPXUjA>bvv^F>Je>0Qf(x6Qy z^B`xm`OhG=NgLmZ--|CU_T_UIkvxM#@_NhGLo@~S(FX-ipE@tFG%{aY9V;?6?wLP7 zL4&NENCy{Eh$uT@3fYR9_qkYG%R`Xjp_X!oCD0RsOY5??h_&tcyhn?|!4Z_PVC=K59K_=`l)AM(FJq#}Xud@I80KKIbK$F5GV=zn4kK2lj ztIEa|$SW`9>Jbxr+c$MQlpy@9lux|njllyzJ7!$H8kj&h9dw8Lv@WleGi}5ZY5bNW zyE)rw9?t6Q;tMAicszGt>}@((U^QK&)jdxt^}(FM#}4^ z=J&-qCG(ixqucbFRc`%mHyHI{t~yM=z9pVQjsthM77Ed5vLsOyAB-?Nc?)3aIA4p& z@k0aV-7oMps$m58WgX$f_!^Vf^-lpQo4`755!=>%OUJYs>m$8Er>h&|gy@0q^A{I9 zVF2VF>xT)9;r@9*RD-8CK!_PZs zN8@A~vrnH+L5_SEEaorR=u`^dX&xNAJhbIOkEOkRL_|c$=D{oxdx;QqZ(iKZO)S2$ zcyM!d-`NCVs8}bXWxa-{CWj{S(WMjwr1pTuI!j1EXd}WyN12Ju6^PR_TY0qP#zrgV zMMQ#ubqDp~Q#UMq*L%`9DuqT@9IfBalpDBq4FuEJ5}OIIewxL0s9GvlSxomhFBWD? zu8IMT|1@RTg4bpP(DX=!OsO_roqPitp;Az^o1xz-ku5(E?wEA1dz8Xz^+;Dc=Out7 z*45QDpuy@Np+x?x`ljf1xub220K`F(%5SrQ#f?|~vxoJGzwb|l~ROlL%l$7M}@Be}fPtwp(bou7z&<) zKkwX0mjc`r=oJGKsSg)GPakgtN)sV*20#to}Q`WF83i-zJNUW~(f-b8@27 z(!$&IU-R-N(`I-)$lo&68iaLvF>8I=iY27Wnq<0A&(j$}Mt1=p6aH8BR9lTI3%&kijta}!1b_dhtgNh4fQTfV7P>2KA)0pG)a_Mf(`D4a zo~}Fuad2^yI4z0Q)YQVr_^Kr4!id-aennvhtW~H{O~cJi2w>`RYtS<+ET%ANNy+5_ zzQZ?kbfE|Hb-3i@ataDKL0A-w7Yhgo2tapY<=kz$L=Q1DGt=lm=hN=vB-KM6@qV=< zJWH$22}H_c=MO}Wlb!tHkp}lm$_82RK_#((8@oYAnBHh^SSy2hRsfCEu6G5iH8<x^1g98J`-7(a^&jf+@0uRC@<^G(v z?#z637wACKf&R#FsX=GUkAXD4 z*tod1weDCVUWd1OdU|V{n~B^?UPfUgToDvrS3cdbwD#BQiMSLLk)zvdTf)CZ8Fw`@GH@M()&z1F~Sg`ir;U4?3^`RAP} zGBn8?-*WwEAGAOI5jW^jCBdnmfOFs|0zd?CA%$XX0glI%H8_TyXLP38S{`<>KmNi_ z`RZylavT-7(@uHVM*Puu{+p|=W&5FEu{s~j*->aSJS+e>UN$nlR&AV$j%WO8XJr0= z&`rWoN=9a_n>OPO8{60Le}PPNprZ2IWSM^Z^HjOt#3B#Y^`DW~uDntEk?ao3h5Do# z`^}itR2gMueB<(@t1FkHnhjzc9Gvg%gQ+~Rcy!89Nl78S>%YA2ZqP}&qk!$}F%y1o zvz#sq0EBzO?0RBIk1xG^Q+L>y(h*KV@D2t*^V(FY0XHe;%E}7lq=#;MvN!|tU-*uN zMlxtrl^FLE&s14TfBg7_h=|AnkODXYX4@4}#+tPl{n-kOY2fXs06h%}36V+R3^wk)Y!k+d3*O@ z*iQR12sUe7*fesfUHQe0QkMtwK$i73jD*ctCVF2ilDr?lTvdpTUi2HwwqWcqLKfe5 zUvoF%%YVvv+uGY7P$(ug*70$`Y%T1Id~zy>dZjs1Q==#BJN;t0 z304Le&co+?@P>&gva3r%;p$^C^IV#-yZ31E5hkwpTOw{o}Q*xE6eT^?cF@h`|?o8USkr!%ZmmZ$&kM-rk^#gL&x; zLC^1J9gj^FAnsp3qyie&4M^V2x<6HlwPljo^CbGlRdi&<#E{Vl7!6O>x<5?>`1=EK z^?Q6uR8&(=qIS>i^{MN2PU0i6&eK60sjCwstl?Ck7HX7~LA1A$N7sBgdwO-vqM1rc+Yi+=dvjhXQB zbXF!4kHx`8hvNsJYQ_B73@;r4Gdp8Ik8w$f{E20Ocbub zZu<{rbg797m?^CCIv>dB>b^SP8UJeVpA%y%^n_hhSTAb-!%-BTU^u^gM2r8Ow^9T6 zajn7Ya`Xz=#@$^9;$*q_an zO#u zWZ_Fox;Y-fp`jwKt~|-f$*-a%biaKIO=2^7pR+s&t8zUx4)6rXC&`orZ`!lh{x3eh zm7vO|08BS3CgzE}y!_S8&9Bl@Y#=X?keBZSLKm~=jYG~M5P+N7+Mco|p##DF3ZOZ| zb8}(T(h1DF0EzMLU1x269@OenRmFK)6BuaMYHM%LR^QO@I$kO|I@$*Sbda|~RK>SR zMu0&*sjaO&y0}0F5*0w~=V)k2RzW8qOMr*`DJlxg$e@ssm3_FYdch41;L%N~JoYb7 zUbC=Z9~>Om*xUQ5(u2r819Kl&5^Q%mdXNFd-@9f0&l z0I{V47%dGAO`lt7Y3cJt4T#NaziE1^s)UC~s}g=gAB32ax|p1HWX>YZ-%4^2)UI=Q=F>~1_#(+X{VzrKe>m8$7da#$cPBc_?+gB4&!8)j^yZad^?nzS-Aqfd0=;;%m76==#IbcCy;@9Tw z>vjkqGLg1#&Uanz&L^9V%UP`ExR0(qDypg|d3anukG!I&riqsLVWeLQQCWqqEpERr zi2>`-;|tmGoq*(NV!vcQ3ZMBZJgRdOAf}+7%o96D%dLRTH32^^udKAtTbr2)13HWJ zN%-qaDlIKN*XT)NX=yn$KECfHfQyS;GHs!#to+`SM#tF9OcEveLHqt^!5!JruZ3x1 zSqz}JcXvtaPkeq3Hi!H8Ab1aid{BJN&i;}z8a2b?L^9GR1F-7b+uP3y3cjhSq{cOh z@7ss7#1TNk!om(Ek1iw;`G-23pL4d_T3l50XuS+XOJ50t5+Z( z+nTB{!~ON^m#(4VQ=bn_6}65)tKx+a{E7-79|2oirZ(K9adC040Mp6jD5^FdOb-tw zU|Q)*Vtf1k{YO<*LLhZ((`V`F>3Km;J`l#EQS0yyz{BGS4QsuFxf*6Rw$=bNf~b^~ z(4(UxuRFm~d){r@+9q9_p!A~G5(WE7FTDxo4owiJnsvNN+PLYItEMyPC(B%6$k?7btB zy*Kyc?D~Aazx#Lp@jdS2xI3=nI*vFw-{T$>`5_$hFa9wc`z$03m-t$L8vq8 zN!d*`4UIv369tb?K)@>|#xSt3sj>0VpFejAMY#RN16JgDX*V+%LPA1lcWkR~YVr&T zp__X&Dw*`*gS!-)drHc^K%$ z=G@tcXTYlqrd_M6tKtQ1$&&5nXD|3OG$h%sz0&)4BNJW;|2?qypU?igmyX;$Kub%2 zo`*kGRR6vN6*Mp+J6i|^@8pqxOVKE)t^Lve{2XQDzh7hbvhn^Z`cGlp)2Ec6J#50l z%TJh@!a_p4OG;!uWoMUmcQfnh=?O#}{jX{!mIIt^HQg^QeD0jMwRGp&RAC@+rA=g! z`C04)0YO1xLBe6EFFr~`1msC0;h$$?HwH_&DW}V z3%@}#sBdT>VPaycc_p}OU|>M^ohYciwY4?Cuaub?mxCA!Zkh+c;r6p<&(=6yzI<8O zsvmacwffdpataCxfE)Ax&6BL)8^r5-_l`sU=-&3u&YQzEFBMFe=L*mVZW#8V3y`0U z=t(nb=n|M;)co}6Q$J*wZxBV;_EQc_Oi;VIxiL$+GS}ADhKo6#rsvg5wi@3?N>Z>k zDSGtSv6zAa7Gq=MZ~@c3fYs3;oIySxKb{`PXBV>UmBW>O(c!@%Hm-f;4MC)F=ilYW z_83heZnn88wDMIxA=<&|1UB|Dy zc27;^!B$20XlQNq!QOk<6(c64opb+KVp7t;$Owh1sw#e}KP9`j%v%Hd$ zS8lE-elHun-*apg^mM5c`)^jgIGNv@?ZNEV`z|KC;%!b2aqmHUrGSX`B>4E%!|2<% zWvD>HfS%^}?#+y6){zoAJ3FUQk=}m!!T_GOWB2Y078X2!^nO)U3TVO}D--4A7m~HJ zcgw`kLPzp^`I1pfTU+J&bszLak(|E=3N3~#!@T1(8Zrq1mrh(P=k=Ki{5GE_Pj+VN znVTnez-Qm_g>d$b_od<=wt3VIgWv*nz?qTf_)MbIf<%+ z@cp63GC_7TKhG1v3Qi*mvi(4KG$^z)N+bLhYkrl&7WvZ(WW?c>J+x- zAoK>MpwDhwuKN!hFoKBp*x!E}q4L)0yp`dI$AN(tm6WKONMC_v6W2t)kOk=1X73)< zo}q;Q8APNtw(i5RLDPSmm#1UssH4M7Fn}N_PJRbK4mWCBR9(e`I`Il_Aprq7ybX#9 zD1nobXMFrIF)=Y^6%~@_^7EPj=k*)wBa93U3sz*pa$7v$nnG}I%j-afim6)FQ;lqxLfh^}; zT_vy+4DQ~2+S#c^kb3wqSK)o6kf7jsJv|l{F$aRUxHv)T(xsi;r%ut8zp;(~{FxrD zeR^g_g~4(=;mVaO*wzHQvi5ccsS)86--yp$B--PNc7B5D>p{$Dke1SFjhUGMZ@W7Rfqb9Kxh zvx7m%f*D5_oACMT*J3A3g95dK<&V~!&d}^mn$e$~vZ2%bUdT)D$C`7h5QT{>3dyLTziMo>f95;SX_7dd5NVF65C ziY;ipzHB|-lY4+k$l_a@zT4$fcZtj4__HBg;Pg=}qVafGVWDKE4jb+Kxi8Nt$kf%< zIWKA7xpPA9@E(bkp2PvWyDVNH9s%! ztMhqx+?NZ_Fm9f0!7eD6RcG4R&`>;ACmE;`dm$z<@u_3rznvx6U#qdwDAQ%l4h*xX zriSX$rAzXPik=_|fPCS$q@<)l=%0}B**Q5WFI>0)G@z8EtwBmgHZvF~d9y857x`&& z-%@F8A{kj(IxTl6v&Hd_FAg(@AtHLdeR~*p!lQ0PmYY5{IhhKTswO-$UNP*js3?;afDnM@M6@LjfFj z(Q_NLio2`|@aTMTkB(;3ym3QXQE}J#^XDsTYP{m(ZwKzn{fk4DgDzAac`SF>d7(vv zAX0IVs7~O3QjXQU5k2ti*)w6=Kl{(#>x`RziN4V27vS;3TIv4|oS7&FpkD8bJ0~pM z5AnPd>h=tTH@-sg2g6Ev^NosE=V$)ZW&9=rAVaJZQ3oKR4M00@_ccRM#}lVcoxgCw zr{w;D0|%0Ib3?z1?EPDFinQ_!Bg~hW{7xK~FN}{HSuovz_%P64>FNV+rzA8O(B680!Iv*@gq?y4K?Ue|=gu8>B@VKv zIuqmA@jzdbK<5`95(Y=Th$s4gMtz)9o8{moH@`jBOX+HxmpDm$m+GRT=<1ypSjul^ z7+6@OHikLwBOXesgppHKq%x5#5Jt}5xY2Q>ZLO_v)4RT2xr&eYeOh8Gf9?JVj}I#; zd30K~3_ZdVRO(`?iKXS$m*?{5L*9I{rFmoKVdb@N|ano>3X_w~2}MkO$Gw5$hd z#^?S6K~YV}ZvflF_+j5qdGgHPU-7@a$!cJ4@P%d`7*>#G{t5>X5nyWr=r^Z_J#01MnY%?M$21_~$v}(b%Fvf zm;xdqB8W&2waee1%Dm&_cEWzrtp5Rwcu!#e4M1f)Z*9$wdPK|2?BCt3L#(7=C+*Pl z+-E)h-W=jtkG}EQoG~vik3eW?Y0=Cyrk2=THiBSV`t6(74c@b7uMa$gQYitxpNmgB*PS1JfJPbBDc6s1$IjlfyntmWuL6+?7 z{|Qj|Uo+FQocs5>)&=W{B_1`(aC3bHNDWwT+*+39OuYkk2 z8;s9-upAXXHY%HO`1tKG zh{egZPQO%&AAJA~DbanU%h%%YwTGuNOW^KYbpNL zmDx6FqFBTh{GApeP;}^D#VKIMxT5cT*fYc8rL8?>rkzX?lNku*7@U4oXTQy90Al zR#Wq;sZr9@(vs2FkGf&s@$q)$k00fQTN{>2uZ8!pu&{u(%ig=#z($p&^*GrAXAQ4P{%@iM?f)TK zP*nZ9sTy^I&Ylh`*P>leP*8ZQ6z$BgA3nfUHK(;RG1M_1WpDddk%dsmL`rmwJ|TR_ zJwj|=XdF~0aj^dGN|{O{H5lXY!+#&tB2g|5|D>Bs&v{BabC@me+Dk3FJF7OVa&it_ z+@iLe2i{-#l_>t8!<_nYS)+7UL13K~1LNpn{u%m^A&cL?(=+ZgXpVP0(vyLj`WKaJPN$Oe6He(-E|X2v5dEUe_?SX}R?uldNdTkwFf3I9 z!toEKCwcmG5(i`Vod_+hSA2Z^k7`@xR8^TeyUuf78moD^38nVn`o=|rmPAYTy_X_a zgTLfbSfZv%MA4vP!heZ@qOZ}_)Y4K0XVGe`^;Fiow#v#Kzzae0N3l1giEPNVuDW^` z^o$RQ*Cqt3+S|h~S=!p#W{=*^(5$cjJ^l%dpZfUmxK_ zdgy=q{n%JC$I-n{I;(bvReM9o1Rew9YIeFBGaC(op9cHKAVc%Z!zCV28{hdXuM@p> zU?~O$2JWw39fWXd>&U~%zyO6`=ES&^kx{T#UG_=U!!SN?4vicTQ;~j!;|^NSPVc9z zsp*BfO*K0IQIi{-lUTq}AwHy8I;=|{B*eWByV>(~>gz^#jS@{5Z(+;Ch+^2J^!@v7 zo(~+w4l#=@@9pkM!A2PQ;*=#fFc*A0(`xos>r~cu66@apo@$=`}hgtGy6W5f{Od+njzcuTe__=B_}7R+fV7e z2nn%$aAlfy|Nd>j+u;!rM*znP*Ju4oY7PE0Cs^6rn^^q5mardCT~#wpbarY3DWSWU z(z3DyiJJ1gd$rvA{L197Tq%bLtoBbf^m?KO@pVH-U!Qert#GSrVg#D25eIF0Mn*A& z2tzZoq@Z3Xeh4j{a1jFGfw@Z{K>UI3Vo^lXwzWPYiC+Bv(^dHFTpFq0AbU)shjKa( zkB?K)`{7xq7emNRiQsE~9`#oznOCO70ls1Sh&>_;Nw&N{;+;ew@!xj;Ds-_x%|~$GJ6VEhM7Rf zB1!Iv9=>_H*L6)&p*rH#E80E$S$$oHP}h4OPU*IHb&(N{?%jz*5q*@);>sXRqtn=M zt?2SX+iM;kr!3v$0Vy8XhTH7z&9dV8Po*$C7$LtoR5_qaZra=X`HOwp4R$Fq_Y`fn z;`vbtM2>cGUZ(Ml^#<>g;tVmI`N^xI<)m-=X4shIv!&#ALbzB56Ml~PLWk+-L)*6t z>Rwo5I;(WieEfj;=I5*|eu*vTomyJkmh0SH%Tu&l_|6Wq3pCTUv|M%(DHjxcMzh^! z^^U1&MQci-6bG3P^%+d_dbDSm#mEFXOzEA;)YqM0BD@GW8F~oy3HRP4JYo#D@%a~< z1VTl?ZLvLB)phRJ|f!%tGV_Ez(Xtc*d`eW9|C)~^hHlZK*GS63JK#hwEPd_e-xya@X^l%(Krz{Y4Y5>ZQm zLs*?EyEN6OA)kg8dF8dR2P$T9eLXE~;c&#RIk~tfQnufZz7i!t4W0Ao@#9H0xzI?b zXJ<>0JlblcG0#6KN<(oRFdyp82_BvcxX`D5el7o80oj1?rc>a|Xg}3Utch?=`JCqO z5TSv-^Ne@r*KZQFx8Do>GQ{;;+1i%D(pJ&Yq2dYiOHbzm_Kf}baWDK)&&VsXva;{) zUtx)Zxl@eVEoD;ow0E&&nd`|DN0zjLujF)eA?;rm#Je#`3zJRm!WE4Vzxnw1+_5Ql zvxs^9{CV^DpFbsieXI=)4+ehy@`mzoGAnxNynLl55hNjP)u0-jO~WFFpEwJ9#RGCO zN&`{&)%-etzWb}~t}q_m$)(R|3M2&I-7dc6<&u$#q9P)z`ImvM#@jOvL5xS(<~aCS zhC#6yw;r<}B2+&|q5{PkrL|NV#GPl#%Jk9-T{i@_k#`P;>3`hAc<$OMQ>DwNND0fU zowkghG_{!qm#u&88Yw;Hl(?UXiBwyAU*6o0Cp=^V1nel2*_IzasIWJMZBEb$3hGi^ zo0vGfZTq>Km_oJC<|FCvFcFW5EiGSNsG!sO@*`^NUQvZ68|}~cQK|fHtm018sB#Cd zCx=&VGqts)+5fpE!euP^$qU25a#m}U>12ILI-rN>SXr?+USb|JJu_1lYH_6F@JpUH z>-+a*AY5XLSuIZ+3JBQryl{o|sFQ0)4cciG+LQBuBI$a{qlgIYK&!NgR&a1(HBp+ zVMWDb{wS@5%Xp^iSabU#OL*RN$*vE{$=^`k+a?_O+h^zIUbappX{6mO_h&>V$txnF z>xg#4le<;2&dy>G{r0D}Kte;$Y3m;thzVt@#W8^5gW#Dv_N12#JGA!t_Vyh|jvRq* zaxG}_IKHfzGs_A$xWbQ@$4IL*Rd0I$%Y+m25_ahGmmQ8%;`fJ+Lq2B#td+U*TNtv{ z^V19LII^1QQq~22jg6gGJ5n0QcXylJsM1UR#x$kH>~{?tTbc< z3=>(+|K77cVX0`fzWl^h!_#Bd$fNdlTJ!GZBl8RA?(S-w?6ZUkO1-`@5ff8< zEu3)d1mZ4Mqm8T{wQgIYH1za7HGQL_@$Fe$ddEx~>J5k}_vF?Mq-00R-iqTz zUecvfFX;8_*JzerP#95aY#8U^EfGa|Y{+5{G72`@9bbF&E?)F4xvbB!<5K*!VbYdE zV4--`;&RZtKl66y^-AMYQa0qe)qL(d!Cc-219W(5cJ}^jNw@q%JU%`SVQ4qu@slSWn(G&D%j@X8mO0nl(sCX}3_Lj|Hr5lT z?C`N;@zv3-KYr|niO+D>oB=LEOj6P=Fb7yr%wi5fjwhZ51o*&^LCQ1QYTpn-of0l0 z7QJ~-&~T|<8pR|eJOTfWhH8NVsBYF)axuqojsyCp;k|n)R-3-QzNIjY&yHV_*qF==mK_U+qIguQk_nuaZcPmAr}-=fjR`6;!ds|ydHvV}nLjYiUD zw?5RAlg=(K{5R!HJWE1PZBfKb2WD(c6>jOH197eoSB3C;){S_U4n(751A{S`{5T<* zc94;ApzOewkM*ET(clb-sy{M&?B`bgDRJ`f@CZM5hH`On5zgl$+$4*>f@h_^fou|+ z$xpe&?}o2cN>z*2cjT+6mj_Nc*Ufiix>4C9HBo`mO-o0&9b2qB&oR@@x=7i%pP#S) z=2+{9^|+>SSju(JQ|HdH`uOO?TZ9^w3(InboZNPn)MLlJdongQkNNMuAbb6KnN!gh z)M$EzT-c6VH9qrp)veRc%Z6fN;`gS?KhmZ6Z%=V!ncu;O-7I81_aQ1$vmlA zT=?B!MoU=+kYH9R(fvS}84F0nbCut}kJ^bN*2BQS(BBxZ=(5{kh$jEw!GnR!(~|=O zBnfJr_soL6HX>B6NnrSxa;+*vVUS=T|5*xK9=bHqChh7vLsPChFyo;4@zze9I%ORl zfAsk7FVwlaghr6|%Sa`Qoo4B7B=O(9a(slmS!9N6nqQ zKZd`gil!8KS83ut*UcqIa5_Qt{b4aNol~~~M=6NPEG!9pK&y#PL3{>;0C(0Qepa{` zQZZNzG6mvI)Zl}I_eDOOJl9?5#tLgui<>OxJydU@7fHy29(^yyal zGS#uz3(sWG{I995>608~^<1{%gZ3BzU8cCHN!73iQIg`ix&vrd8at-9lai?Bu+x5$ zh2paH^^c6{ojZ@m$C=~azc+$X?yxqoWiUHr5<>b({9%EN&AbjGKDevZ&>9$e-Q1X9 zZN5)Wjf&!hA@LZ8AC@wPvM9$VCNfN$x}cJ%nMU$zrn~R7peR9Sfn5W&k5e%sGjreF zyLXW~q5+qFcY1wwfk~;%&MhJ9Snc;GfrVQxk4ipjXD2>?5!e34bYs;t=({@@#lFB_ z+wvS{==Gef#U6RS+0W6ogYv%Il^^#61lDJQrdzcoH#TYCK1=3q*_|F@pObe#UjEmb z!{)kWWpxsa~f7&yY?w0nB~lgemK2+qvVPYt7*r`QFNY^P#cy@U{}XVj%ILsc-OxT-h?#@1SH`U) z0s;bRk`Ay4+VLZZ0fjMK(2O26%6k5HgN5f?B-@K&R~Bxray5M{8=OhE8NCK?i;b7} z>S!(!E$AAP{M1lKiB|;i5cMZTd5$|oA*5VVV)EmW!W#g0H*3QA0VAdGK@i-t8YtNw zA|X5TXD zwL*$Yeh4&IoM{wccGPLO0G@J(i+!k>fagy~&*kv8{o|X~qN1WgOX-ajRjalhnQ?$j zu{@F8hpOid9SqHnKa(KQeZL{fXb+@nG>AUdpl2Cy$T48_A)af*AR{XaBKdds@_#Oe z*LSSWTJ2K2KYI)#7B|P+`6-tqBqb$LZ#e2m`XelN6NKFn0Ne*%v2?h0YpHUs|BZd` z?W*~7L*UHcY>*72>;J$?RRoO-i5uX`0VhF~8&{zYJ!3rc4sJB{_3NR_$Rkj6%b1{@>+0%$tBAUw-g1Z-f2e#OMJ_NF z5BGqQQY6Y+$kLSwETu@1H^0W_wvdp}!_$uhG&Hz`ML0hTVD3ZEo2|COMEGqiS%yW= zk~le;=gA|nRJFCF{)fi=5h!05Urv9NlOwS>RT%f#23vrPV9}S#`eMRu_7bVxR6yD9 zh5%Z>+=6!V_Gz+Ios;E?yxxX5YYwCFR?dqZj^jNM&A0u+^2J$0LoZNPzlzwdApNR9 z0(R{c3g6i6qM~O2x!=abP-}mFtu`tV8q#u@ub#xo>2<~RNT;v$YuE`8(Vd2()ls8h z=UV0H4E_2g%}sBFS_Tz&JE8zFvc6v>iaX3}q9j>y0EC^F`5FwJ4M>_ zjoxlwGDuiBU_cuN_|eMBO69R+OIX(mr!Iq@>q=-^=jdo^Y8C;{0+bLFtMl{o&1WyC zV<~}!V;k>PdCr+cI+EYMeVdb!x9ny{|HQ;I+&ts*GlV<$?h*Oc9<)+)t+;oKF4w#N z6OSq_^?Z3Fxa?*z@Km;C-*q!Hb<3)NoSbuT0Dv~E0Sq3O-4qrPIShi2>scO-uxt+1 zd-(7ntVjHZ5LIz)Ej3&j-5+u*>97j`5=yG8{TNFH!f%@lRX&FdmRdrVh>(qwlL!Qj z{{H^@DYna3*l69$Zeo%Y7AFNR@Q{^#X@QR7P}?6Qql+6G_x8DMoz-n|Kl zCE6Jr`{&R3m6ZJXK}r3Y4m`@s6N`&mx9(BAoc{>&nn8)z4Dz>i1e?!aR#pS#EGBJ3 zdbPpsz|Jp2bvs=ReG`6}9Mn(Z9%d1-c(x3wXr|7sAYgjh%6V&JsmEdo*@7>w8|0W9 zf({v9zdbVz*mR)3pMVe^vRlim0f;9YCl|aevkTxpyM zhD?euOaFLBX5jiEY{1G}A1ypT6soEl4mc^zWvi8y>k^eD0p1p^bYyl-<2 zcRozCmtgl}kboLC-Zzsdmv^C`5HZf|I=Q^2*n9aDlE|Qh7gSXC5OuNj_04Rnv9U2C z)PP>6&=r*3IlZzm#=%4HHv`)Vz<>rDnP?QCIbm;MW3@O*EWI1Y=_RJq=IXY>LswAY zHS-+!k;Q~M67zXU&)eIZf2*&d!DD{jhN$k@+JE}^QASyr78Y#LfK@%hVVKYzK6r$t zkvwUN~6Rkf}mljC3&#YhH$Lu@2|b9w=;4QW&gswGq!WJ1XTD`bt0LmO?S z^m6+9`F=j{x@T^_yy5hIS4{erQ6Vx5XxRi{86xCO(kOhCqTN@9RK~T!?3L*+{1_(S zV3^9!rrX+FD&%{(*ewz0vfr-^%}dF#9mAMJak1%rT~S?K*tyiJazTqHSXj8A>?=S-yEp$5;^;g-2QBBnq(UTKnJZ)$%-Z3*JKt+%&U1R9?Yn|%|I+n) z_jZ+(^lofClH8M|arJ^hv}XzTKZ!+yrd&h6lnrm+Mr{{G)Dx0mS)C!TY7U z1No>rm>=jTGC9atRmO$~sen8{3?ISeMQ(5Bg9mdw?{(ewqD>I(Hh8BOF>r!F7*XB> zCubJ1kvCL!voyVXchi;nx!p_q*@NAiRe;q9<`9E_#ID`Z!AM}~I6VJlHakg6-SOFH z?C{P8huf=5e$39!ZaHdeh4r1Nj;iWD%!D-wUemq(9_}pu=w9diFnE5@ z6Omwc4i0h%eDd=0zgwtc5~&P^JSd2xrQ6VynZ3^=dxOQ}Ffg6CR@+~Mcf z!g^90`wj>Swh#d_%A(VTa-nIHarc+!zNr3)vfHiY2Ol5gGb*RTk)?y&wx`^GlsWU^ z%KfKRa~E8@e^p$U;85niZn5!C_G8n#5+de7QxS;)@0Cw{Tk3P8v9}kJlT-46C~YyEWGBZ24g z{trf8n9WdA=M=UT?PxjO#8!XKQT}1>j)8&6${KFZz`ALcJ`awm)%Wr72_FS2`&doP z%=}AAiX9c#zX#uNSZLW%6UEw`w6L9&J%IUAMd9XcG82<(R|->_H7Lc8{h6P%=?8u* z^Al1R(JcrL-kzAm)X>XwP~rTcy-(t*FYyo2q=0_KC>kO96I#ILc4)nG+*~Qg`bm?ldOn*aBv-cd|I01QBQ#t&-y5e z5&L-DUG&&xOhFJ;RU!dHkQhR9Sfgy{`qU(83KU%gQ{#Cq62ilZ7Dq_*CgDXBXFmc1 zX>gqT|NMChSvcub`=}p^4(0_=?Yt+ikui2P>O&z56iXvgNSNE=4;N4Ps}EWUBmJSS z*zsKcD44hjr#rbU=pj^T)@rpmh!8|k7?^^td^y20FGNXi^V`h~nXyo88Pc3$G5T5O zDYG#(ORfgBL(9e{CP$DYBOxLA0{F~JmV40KIN}+}AgF+#pp2UFSY7?L zc(T;r|KL$2-kNw;q+#jT59y=KU0c&iQGYnn+7VJxxe?ULDA!{WDYzP&p1uSBdV>ez z4uAe^I{40>sdFGBJbk(@LEV_{<ZeRi(IHQs zWZie%>E`S$URYdCTT zlzfpm?1R|~B(hDR-vaz*aCQkDIil;}*l}1q-=k{H$k%!DP>}%8{8=Sg5a7=L^Us(gXNW~Se|WFqui(!N2JJNHe<%Tk|?-OKjgGVI7+yG4B)3shi$ z`HGA#8G%Rl>%8VHu|`27BDg0eit4&<_H7je;vebUK3MKOKiYeUfq@YH;I*FHXRavg z*$$H^^$(Q?F|s__S11{MBe8?6N5nsbb{nDb8ly|LW zZq{udTAOOZl;%EwIZfv4DNgh}JfZUFQ@3xE+1Rx2*lkx*;%%g?-r}Cj{p*!~s`2!A z@})0rgNo^OZ`P>KpSRC&c4Tl}W>4vIneanzB0AmruHRjMR^Pvb zSW}CMyD~ddL`;KiZs-qU3K+2_+>QY#4Te z@_5I>0?9jmc+;>qJb;F698(vJbKhT$DtUW;*9<0#rDZ+65xGaMk2qO~2Ur}cU%pP+ z=mb6E@~3P+gS&~oX@yr@hgMmg>sKZ-jwzOJxDEs#K0abs=qW&0o~`zSuSL?D`lfS2 zQ;dv^r!`gm^`h&_YorjPK`O~LQeay#_q}YfP<+Rwi&@E3_&&|y}6#woe5+4`m4viE# z=IEhIq#jZ+VpP{V5>isi@MwxBP+3bVDk$)j4d)NDX}_QBZX9a6>$p91Ha|!Dl(d$m zRwMbGCWe>>!I8toAAXi8dkY#3@hrMfEl*NnefkLC7W>&nx9Lu^wA0mNfBukn-&2SB zu(fHKZaqkjhoLNJJ_(6*rIy@sraEIH;p?_JXG?gK(k^7t{aZsy9dR=Cr0avvgf_!o zj>{e0J1{+Pq&;iwNY+pabcIMa39Xp%o!&Cc;;Rp`O<|E;ba5dAi_Ue)_sq!PKYZl9 z(T~?1HK(7;d!afEmeSZSJj|7e*^!pNc(4}_ib&R4pv>$^E9flEBYj_}=_h0!JmG4z z=K^`aR2%v1Twm$;;Ew!P4VLL09WU7>T*W0eg-KXlY!)-(q){2JT%k9}6jau7V!d~- zgv4^L$m17-U=_(&tBY^&@s=)501iI$@SQca3#cx{$jV(aR)Z5wt;YEHg+>SkXnvVK zbL-X%arF=iByuqK26aXGQqpBqF(N@Bw(8%y>|~dCBWiu1c-Yw3ATH^F<4SX9IL-I? zGY7Ivt*+7>~xKT$hnBjNTv}3naT#sc~-J8zSRyT~wfD;f9@5 zun(18$MeCtl^Vx}h)J)&%yIlUlw}5F12N_Tsbu2*daFU$At-Ca9O2>TXe^gbrD-Z_ zIQ~wma5=@8Y{brMf1wyft02^mSiOoHTcQUQj!!l7F0h3*dGpYKfz~% z7H`mdqgA);8_K5PojU{%Dfh<4j~9p4EkAROK=pv01ui`8AXzDe70zOapLRo!&|ntS%?+;4UCxhPIO%rwi)EMcPG!89l@#(`}` zclvS8evXTii(iTUhwUCckS4M@#s_I4vj5t#n2vCU&051CPu0 zLQIc&`*yEo-VDd>8vSl}&rcIwAr%Id?_)K5laueSuV^g@e2#n()@JzQwJvflD(+U4pT_6q zLhjA~x*h147TVzPclwh3>f7uSyEY%$P^Y}`{3L2WcGBeEpgsE#{|V0JSdFGF7di^+ zD&iEy-+ZTt0D1x16XeS975$KIKSZDLUP$Nz(RU=l z4~(DDqUxwAUMl!Gs~7g27LVDkxH9VFfjz)((De<@I5CvfcG#U)%($crt7zr+BqIm-U zb)tQYEla=0`85Podxuu;k7=gVL3GYg)Cv)D6- zkCO2TRut@GoO^F!iF(a0hszE>1PQix_;%foRBO@}wc$ncp0iAu#pCjM)3T*qM~ne& zjepN>%E^KGw!NM26R@FFP~KKMaeL`d_c+#bcs(_{DyZcNy>Cu+-txT%+2Zg3Cb)+( z%zw=WS3czd?ksQW15Uuotz@|94lc!aEN2{?v=5MP2W*RLhQuD?b}2;o^F3F#hfoD( zTJW?!5DDM@?x^gD)o1wdMaP?@gz97lV;eDP&LXFxJ;07bTE?0e_#`u&^WVBv3r01` z(CK@_jX|%qw5)k$7IrbD)g}#Vw<#h7C{bfw}f>lyNWtQ6ihonzgZ2>7% zSVBg^WVwFm{107Pqm|GMOQCE^@47d1h;SZ_mIFc>Tj$4L`JsV8z{c2dj9!_?Nr;Q< zgDP*&@1G(jMhdd^!J%ojvcZMsPEqMcqn^X@5=R@pNYM);cWv6_(v%^-CZ`?mSXfv# z5g{3HBJ)XGQ<&Uyc}PDH@u76_eqq4ksSxn7ZNGZDK5?Dicgim+=si?X(fQcRl@uYm zm3>%n+WMwPI5z;-Ay3KKEY<7&JCe~rSyj7$hQ|BQP`U_F5|KVJDmp5IxdP0*($Ai^ z_@8hh6}{@wsuplxV4Nq`pCIr!72(4CH%^P{%kf&rg7}eso!%;GYx|Gcknqy75|oSO zDVU65brloWQt_3LdEHkFiA6qPiif6EsJ#SmzEz@M?Q5XUm) z*qO;=TX+OGZGI%`k0>ooRku5Ja2T6Q%AWBb891|Xw|TPR@BvM%*+#$Dk(1ygT8A|VRgiDG@01GW@nSv))w5U zDRuky5?V$F{cGzS=>w$t(uIW^FKp}D2W(&UOi;);1x;lEgfr9A|ZW9>Y0 znWqL7nirQ&@0@lWxQ56wpy6rfQP&CvAQ>K+S%t~`wy-hZnLvEndUwj|3Ug*UmL8;A zb&8=3`f8d}`kAc^J$gXOMiv9YMXS8%WjiayGmhT}4}#A0Erj1z7BCCin*Y!a4}Wg) zF6`Or+Ho3|Kog9h0z%rR${E2Pd#w8p!%t^;VPa$yof=P9O0?XDO27KqZ2{-StMvdv z-t_?fK?uF7nLH8Wh$tjOD~2?qxYQp%P&h5S+UQQnIF59l*;1r=cadMre>&dR5?z(L z`s@ab;9#OB$*ZagNYW7ST2Rt~=@=hB zK0cn&a{g*Yw_`lZMkt3NQbr~|K?QCfq)qf4E0P9d1lxWBckk=+x?6jPo1L%2r>w+O z6vPW}zL&G4A!(OZ_rnZYtg8UrSwdJbaaqY9w&_TQ5#WKIQ4zw{Z;k8gy@?eoTUD)b zC|m81MM?%;ZU_Wif;u|9^qm)6GljCERSN@24`_lRHRVNrxB?hac&|xf#Xx-2gsY>H zRnhRg7araLn&e7L9oBX8oQ0PG{#{5uompQ#eW64nMuSBT72UF2qb?1TRhc@x#)=e` zCPy`?!_J%fw825tn~eGa^8E2hy#fO+h#ACJWR61+joGg2D$3&oB!3QXP0f{u%&(R# zIGZnBAA)-ZlExzX*}W80&{creuJ*3EoVa+oGNfzK1tVn0|WP)$K{@L(f4Saw(`=`XjKck^&M;Ns_5_+tsff(ueWj1 zLM5Ira;N%ThQN-}xgl`BIMo)vubu9?J_uDPTzoTYAb*{n zz=rz`O-ivb_**dIbMP1|w>(;)<22~JuVlesDs9UgE-VxLT^pj5*BQ^6x`=F%%|iCe z-0p3S3F#J25^Mg1TE~X)RH3GWNlQu-OFKvy=g1=m&Q~n?2s;#M&aC9z&qjY>al`9- zQMlgzdHYu>;zsFKflAWXD+^(!{dFtq1%7DfzfInAjH|*vhv-zw9U2NJeDQ2tzJmOHPA@DuIwybT?~ z={t`D`rpn<4}H@y6Z6ZmjNtw6L))kC0b?TI&if60iCv9+%~c&fw6Gv^*!tgy(1X>5 zhO^kBZTdD!#2^zI{dAHvIT>}4j4D65whQC+%Ju#gl@;|w=bK{dtK|%6j*HbaEL5xF zhd+@&9E>$J+`J94%>6qUAlMqs#Rt@r*@6Sj9U+=*Z+pNFpsYL4`_BDK=G4MJ@Q>Ad zW5vUhL2rO0%#X5n5gz$Bp0V`5RjE1Ch-bijTho7^{VN66w_;`((?(xxjeuB_1%m~r z9Q^gUVwLB(1(|*|E9r%a01t!fGI*Rm zF%{@NDs#u8iyOIThJ2px=MP%*Xnil^$w#^R3Yv{hz&qqCVTi?ZWf;nug`)Do?hH%9 zDUZ2(ocpPH&qd|BI14#iWZEzV)8ADir@OW1m=I^@nEV45<$+$NH%So17qhjS_i~rN zruBLWm%FDvxE_O1eL1Ux=D%<^-$?s-+%z^Bf`@GeXs9amH#&N>9nRg0OGhdGxo4+a zdj(5uWZL1{(K;BB9;?!&u466VzP{#>LHn;!W`AT)?!8+*a9eQ^4X4sLsl9KqFPG=4 zqdW24-hT63_dKhIx;q)#3f^FYN}!@PJoa`oqRsh|Kj0LXm_MU=SmfqN`#yY{VcGEA zDLxg+2DR>C1t>HZ^M3q@zJ}P8k*z(D%a?@t2YHs;hcmWG95ep&?A*Rx!#m>VJ4^mm z2li;ps+~*`{;#SRXaw9BJXu<|*0rinWh?JMrDdv03JQI;zE3ZAt1=6N=(MlVAKgae z>b~wcj_AHW9jpO_fE`PBno63S29fLR>7R#%P66d`hXIYoSVLJHCxZ7d?)h3uZ#dYE zy7u_(x!${0z5{+T6!uwl3>z}OE03J}u54y8YRay?m85n|s%5Z7U|x;Wi2d2Lf>hPP z0+s5%_seM2eZH}&;cQFVVdpaLeIwHpYbHh5yo*%Uku2mVGERv~lfOw)R6k`@5X8|i zdy+f9U^ndls6t=Sk9u(tsgVVeQsU>$_tGwebkS{1gz{wuhK=*mw3rt@IK)hWfZD_F zXThN?m)Gs=JZ3`B_+ih@U74 zmt1$r)`w|G^)>6~CP8%aE_T6JZNUD@Nz8))TY%nykAI}ta`+c z=xO5n^7bV5zZ>!4D{dRlmuQ%h$FlWCg-C^ZW<$hRamLC(h-m0~>DG5nF{7~@nAw9? z@!OkB>gAampWq7CI{}kS#J3IS);GsD5*}Xo6w*eH4CoC;H$3CW%mAbMhD$|EL5gam z7Nh@`e~&cI>ThxKfH@1$41rhCYHe%oy#K(0xkQusLj<~c8G2Hs@bf{?(`p{>9$0q?*!0mo#3%Y?%-Q#pjzhHU;gma zX1bIXySd`8N6%}c15cE2IgfEU(hi<}^sp`>Z3kGy(u+V|dtu@Vh`QG?FD+=kz^v|Q>etGG&h&Q8Hz6m+BnX!Vd8&c+b@$E9`qUJM zHL?I%W*qpRx^r5tGfw&GM}3ZVQ4=_`_YYmsC%XsK^oGxEDGpsgT>=ma9JAXsbn)sH zUxgU8{1sRFR_!2{D*{)0dX#L!DZ4^_Nn(164LO|*p#$Gkh^SUZ^mnw6r5(2ttAFgi zAKKh;wXUxW98CDu4zRGU-2N%C?oHf1L1BPOT`69!#SIvfzL{QxvH+rZw$m3&N|Jd0 zvIxGo+%J(02LUhg#kyWAINpQ>$D+No;UuS`Ewz3r4%({&f<-2;r6o~CtzvhG)z|Qx z^KvH_73e0DL8Ocs{e`D!rS=`~a&sFvHfpk}wlP27P8xVelz%Jp{8=Kab;(8TRtbuK zhh5gb&*fwAVw*DsJ>QF~iFDSg5l+Ss)}ycE!jAY8t#y3mir^0SX`kLl^_ZE{=YzSG`?mmq2_|Su+Zn;AeMmf=T3)8Z%m79Y=B#g0 zrsKva{knZZ$!*DjT(9njJtkeQ&xAp4&RKGC7DnWC`fU~xaSmnbJZ}Z{%y9SNM+sWH zsL`4Sc}>+J-dnz3^e141I12Qu)baFWh;KvtSo7zC1oWB9{Wc&i1#~A5-qPhqW6l^B zijOXC=d-NHk;1a~?u3Ii^~IvuKi~N};>Lcl6ThA&p0ddz9KN4EbOxW_y5@?o`rM2> zZugS{0XG`MNL1vUvIhs1c9yVP7z!CQL z=Ick)q=GF;e#UyEkyK*;O!eyKr=dJMukYhc_f%ge#KVdnZ&OVrWMp>x533I9eCFeO z>0bEveytyZer;7vHg#gLz^fFIm@e^L&}(pQf`})l$NdI$3!97aLaq%#RoZBTf-Fd_ z&sSL1J99LxhYgj8!iNp=4+q?%E%!JmA045c%6D@eB=uILwKUMD&O)KT0r8aL(Npk_ zPHC$P1)uHnkq-{$pd5pYS;NM9RSE$f#jW`ar zx>CI?u9VbjNA#=>?G|bN{uWuX0my>QOykWtNO>xRh-a1vKbd@dU?tpLuAmaWu}6?_ zs=Cf)GZ~AJLj<$WqrhIR;;yQKdUn|uw~p}YL;9y^osYar{6*MhznF%+_x@io0&cE` zj?T*cJ?4V_96ILpif1}c1(H0&l|v@6-zU};BOwP(sc2_U-&6j~yn?2;o}#cfM=Uru z=n6OetsJ#8;whZw!ktQw1nz$GwV!E*Le*zhSqOQCV?hf>va+P^7G2+J85gaEdysQ; z7}i@vxk_=xQ+PwgKZRh-F>qPE<5RWVXVeU%HoRXh-;40@$JdRh^n1ZUfe+-Y-X#+x zAdD(khI6q5=NM9g%4X)&bVm_=?x@$bJ43_ix#UhHfb2&43$sTFyeX9R>i6f%sHxmAYGyLzy#eCr z@?=ZQ-1cg~^BlGA*eulnyuk)E6MY|_sLWt)=+o)!xShW>xbNL~rFg6aD({tr#4ud!_=5x5!yWOX2=2NiAaG?sW-LeJO&;+n_B z;-f=~a{9jNvSoV_SRJG2+%1N;$sGAz zW3NZfM(a!HVrNfH?L(@jXgw`gsUf21IQZyJcqV^n%itJ_D{9Wa&@cCDqcF4vx0=kTVo&)YloGgE4TH$h;tb=I{OBCecs9SNWl2=Q9TZINS7@u7qtGxwl0Q;%vU&? zHO9kPHqn}@5^D-(KT>9Fc8;CNXqmK`^WXF!)2GGFK)z_dW7-$Y@x|)G0MrDUDy^6o zv-}AiAR@POLJmGAsx5@Kp2IF15jN7`&J^V@h86DGC0PATF%L-+20L)W8pqBaXh%6s zn?+5hH}nqiIXvs-sufFi>a%DiOf%_r{*0;_Z~&k52QMkKsL=0WEm&ST!9SWF{|s_voQeC=ko#BRSX$VW4Vk&1?vbCd*L(

>!XRbnD6Z8fa_RJP?i^fxu?dv=o_&r)7t@YaG=7f#4Ti43Y6If}{yw!AI7DvDg z-&m*p?6!9?hhelj0V5b$g}-emW-278iZ!Gw->@D#C|z!mkmxr| zUgI8vM(geL#>;OK@LiOryTjO1dZX7Am30wNc&kd#=duU3>u)(ADO7JMp9@u;M1`t4 zkFJvXH&#U#K?AAjAr>s@(;kuAH4kNKKnK`;3h6TJ{$1S%(uX_XMvk(Z!R#37@D5(8 zKQ}K|4cG1kCXZ-)?lb~+E{SO7da;o0Y^1nusccsw3wSK}$^_rT@zl6wXBT&|*^+)? z?aVpTWAhD07V}H|&uEKDBPmD3>=TR5G>hw`egJbOCHVfi^=>%HmXw}dN_JQ5S9yw(+w*>K zGP`RKOAtUw8AXEj{?Wo1MbQ#m#1a2h&x}Izhe6eebu=Q?uTrD~QegZYjJET+;TnqVD=v^hSiQ1BN!m3noC=F0rs9(7{2H-Ct^ z3>7tO-6zjifAEt35KN-&+Ox29z0RlV`y9txShCcCC}&Lqgc(dT_S(AbqT~06cCioj zF8b9Pc5(!A2S-CB4O<5#sz03(0W_08mdW#b0+`ii9Cj?vu6o>81n$o=n$fD$KIKu( zdaWn%3q@6z1?D(&b3e;lELi0#?`NoVSYOw}?y4n(-MRc1BDu|IOL{Y7{l~3x@9x<- z!C5boJ?XtEHhqa^-3hF1J0G=5+m75yzh~30nzn3!pNZIP*M`C;UNW#}JQ3`i3m55i zxEyca<3x8{VR$~Dd9Hr1(daeA%r$yN&(FIw@4e0-?CAhs95n5QHYwY7&Mw*epkwKl z8zZ6RYLIlEh!JmoX=PT684`?`!nI_hM=MhWO!tj-)Efb9?@%Wg9U<_Cz>FT_L)}kq5g}2uDyu18JVWrth z*Tohu-tV_mGlkaQhs)2dPt1K8=9y$WI`-1wUobXgZMbKkj(1IxZYLIHCaI z^QHqU6%nB?^z@B2(dO(~|TTjH>o?Ac{~lIn;L>~By(|18#jQyiR;pBET9CV^4CMEG zUwu5!!T%0xFOyYQ0Vu`TwoR(K2iKR=a*Bpr*V_Q>22J3Z0wC&7^FkS&4#-PVa&XK` z_rbn1s=ARFbB5;9q8IeKf;Y{Mo8~x(iFS5lQjokIi>`{ito(Z_ovpz_6i{6sh@kSrbtt_li{mV+sUD-1`xRax*edg?8;MP$7zqYTkx3rmVQei zoajqI{y3@j7x4TCAZHDeJNX~ z94XS#&3*feTU5Wd$l^?#gdPL_+}G@*NY|1LZ_3QV?Q2>*cOPCL=3qGY^T#u#H+y-k zQcBLDzj;TueQ{LaOxzY;oq?SaB#Pqc%nW+!JNIh49y3#j?1gFOxU36>^hawyBY)my zg8*IC45E(Aq%s^!i@(?IwlE$`s4a zU8i+2KJRI*mZoA?e(3$v{8(&U6iLzJcRiIVqX(I?#oMZ%Kw9Oznzcl0Z;JJoN{n$H zw}Q|dDK(?2{Iyrld^(RS@5}{g0>%@1S%n^lx{4;U#5k}@&;6Hm#$P2{9iY+M%N*VIo7^ z!h%Um>hoXn46N}mI*}J;7lbJz1Xr5mSp4|#V6 zYB?eY2-CX*9?}Q4`UpS#J~7Z3?Ghgx**-e;_5(^l@oG~*!-WL`{}UHfTHvfkoLN=z zdqZB(>Dr6%(Z(Nj4R0A*2&Y*VB>2Fo6d7KqTL^rWnqUG`9~Una^XcvLYL|ET0~P`^ z*}?93^YOv}8ALX_?TuPO;x+-KAneDHDd-WN7&g4p_-#^)L!5uezjO={S5a&(<)UI- zvddF}SuEwXCqi^PeX{Jme1~yHoD-`I?kvcPjT$37&w@%7iS^Cb-KUA`bNek7 zt@Y^klD?pV;?l4#n=buE0OgBk>V`OaxhzCLDMJ?&iSF?_gTBt0EwueCW)nQ-XEJ}F zxzt~_i#a&Nwdb&MPy#X}Be^)@GdA*j>Tf}8P^y#rI7R*Q*{&dXYbBBlT{0X3U5HW~ zLu(uu(Qlc-duZFB8rO{+1AL0f`6=T|axqz`YHBunoazFyfsHf(=JQr!+R8cR9b5~) z6|iVHJ*>gp@bRMYvD85SFPzVH^@{VwrZQ0Fw8E%Eg1UQj%k*5m(mfXmNpV82no6U` zr+riT2>YQXgM_9QXQctd19PFw56{?tMIDwS>TaN(5_O3dj|mM~rU6yr2dsmb_u8_B zcfG17gZh>7iB0zg0kNg#X3Rxxi!@8(&n@$-0RsPAeMdx_1XN3ZWRXCl$JC*;G(iru z{lp3Mq^fN{=$F0;-v=Eyp$|@ZISg8-&)heoXm}dt!u~`D8~h)>Y@V}j=%6}C3`!JL zbakE{Oecx8se%;(wnEs7C{t}u$1OJdog zfXi`iF&o)`shiTnvdJ>Fr5q%m3w0d7Gh;)<_J5Issod-X^uo%)?pUV!u~G+6lR3?) z{z&TL0mIfV*=^HT6TJvk-mMAAg0Ggm=&fl{pg6&<+*!BO!%CgLD&I@fS0rHb^1J}0 z2ST840OB5iMjrJj5F$lC_-sEnk4%I;RZxzqd$Wp6bO=112(cVR(!MV}Z~_XFo9-4p z7iPwg@J^65F!2W{+WTD%RnQ98?XVvSb)+oK(KAz_lmjxs;p4|_T@Cc5vIAEW<(t2Z z=(hklQwh@&ch2mKEq1qJY*=@18U5Fn&iQFog?HbW>FX>}R17ZWFl$ezFt@HH)05dl zgy72Iq4WOtL?RSXBl~B7z7{lB9gI}1rL>jZ^$BX<;+hFLodm^i7$bWuh3lR&HJ02z zmhgrQC0N!$L?F}~u@7$Dd$e5A^CuHOZ~5;&@ONzKW@Go%m9@xs(vhJp6S zq9KPvSgGy^pAtv&Rifs~dlNUaZu$hzHWCxO1Xm3I_$d3GWc4c0w$^sTfX&(qr)-Xx zogC1w^K=!=gCXw>x8H$FsB1-amRM!BcIkbyFy||`N+<~QU*+5HQTFU!H>yBkN&#j0 zgt$AJiU$DL6^r>zM>#-QBF2ont=J=mYpKJ!vraq1tGw<-FC6N&zxqIqrgDn>b$1#4 zxcYbPol*3E@`s53bN;Xg9+|f;*sQ<@=<}m?SZ#Z4;)SyoBbSv9+XPP3K}9Q-UMGB9 z4^S`-zGMMXhUAo^t$gDlgmWvxq*L*CK&9ZTXSyxubUY6rT#Fpp|HRBJL-*P-gj6!L z0vgaSPBSc#!|*GN|Pl`Kw*&)~X4*uK;LP+btu>%s;k>j~k;IvZVTw@hs}x@8!X6m zdAI03|IyYDb)VSdD6ZQKltC>wT#gItx4S|G${lrO3vG2t<0AOI2+b0K&GiX*B<9`w zYfBv7U2uvZKtx&Tz1euajQH$m3wmovP0S0>2d~}B#u`9gui2?EwMY1aiTS!h0@PnK z6)hK~PSA((wg^TQdRm-A5f$erxQeMyC{eiw2{vie&HpWPxWQR{m;5hpASE@uf2#BjTd+qbn4%8KR?atkSv}E zgo`q&kFBxIPyb~KSmnD!Kf`lB?KzPZYZL= z&leq6!DzLnq&Ofs51atq1T-Ef z8KJrTK@GNOuv8+#opw46`3X1m*Vvo&}lFkML%-NYIfSI>Y5wGY7Mw zU$cdLN-c356*bcow$t~wv6;~-e+B!~gY(g5FFdY{?)9Y4A>UGq&xk4lUX3tAz7Pl1Aa{(F&=?xUxw zCJj>56?k$%(<35r6E^qH2dFf3N5i+J0?||GK*mSg{dN9(JvJe%i~*C@!-C)ww5Exz zH?yWPiM=2luo?j@0f^AIVOMegTKDA<+v*@b+8@fNPmZ!+k_Y7f*;782ZAbg-1k9~- zH+RP;^5*kbePA$#06ddzF=7Lp-3-d$-90dI!P?}#8E2tRBjA+Y9~ULhT=|K)&O7eM z%$shb z%Z#4#GfYSf#K!R*v88&gkpU=k@jjTfK9L~r-i`{}bb|DM0orm8Z6(spCqKrE`rT}O zdi?Uf^YE2Zg^oVW&PFjlEYQ`2`^PH{biFXqCB zhmNWRRmmx}JJVA!Q^4 zFw#h`IR|w`on`ZZ&fU(T-!^%wV_YiN6_QQ^1M{WT)C@Zv93GI{J5ctBqX{8CIJW1C z_p*P{)zYXo+g}+ipL~1bOMG)`cccvn@^6P!2bg+r$zsRmP_m}!l*NV z!s~0xvg;|$<1YOvrRG)`U(b+o^H8DTkpreF_4%lxDa6vZ3f0ZfjVNKPT_L&MHQr;} zV=_c|VNN5Uuo1P3UjZ+hApR#hsGj{!(g*}xU{y!o61w&I4okEMpQU0Yz`%A@>5`Fx z{I{)*(WC84C@=dvJ)z5(5u^WJAFp9*CExxL6DO-B0?MwXc7~v6qp;S2=Cu z(HhFBOh)oVX7LEX>Q;Fw6L)orC95-|D~N<#nR1p%EDJiVVF=Mlo+sYFBg}+ERFb`} z0|C~$JX~fD$bsSvEN9`GMMe*%ss>VyujR<)kxx*UzjW%`I@9+8V)Ve!jGdC=9D85J zF1Cq~L=6Qrlk$$#dQPQDK_Z+RjH&9z5J2M*h_)3!UR+%rK0SHlUeI%WPoAehC;N&% zu>U`l>o+_9Ka^|isZ79@qNhX+m1aCBqzbTw@9zZQFi@d_D^`LG1Gq62z%mtk5m3^y zk5tr-rUXDn8!{)nvLpNkAQ~CHJMzaHe3O~}q8bE8yiOou7&QSIcn}Gn@#0Dp-y_dP z_(IVcwjuwIt$aTF;bfX8{R}Pp#;@ka#Ok~B`=WMyl>h>*R8MtxfU$^L zcv7eo-89$CiYK^}UfdlW*w*;;U}X9}5W$=PCKs($2MDl!N5mtKV}ouM~LFfqdy-|3>0}{2Q+(P%o#|%k_ZO#>YR%V|C?ESXJ>= zgcqQvSO~zTBWaoKEj)}GC3?Rfr4#S$Gzf$FFL_dEw&y0J?_AsQ^-FXyPoDXN1YcX= zg4t2sIdH2-;~nrsD?;KiI?;KK&v(61b?I{5K;-@0)gFD(?7Xp?#Qnl?R4dF9`La|W z=sY)fM}mA0vKr_gYnQC#y%gfy z4deu&gauJK?ZWS0ew;R81RPu<`;NgniJb&Iy_G>w1UmKv_XxrNm|X$OgFHaI`+?=8 zA!hQ3_7-!6x3Lha-W!L}jnLfH*yVt;&{rH*j70b+rKry7h$9pU5pV#))|qN}n1zD> z6q3o>nOfq@^J>-EQ?nkn18!y?i8B*g!3Rzb>5HPO- z>t|XNG88xitWDW0)6y?2gU4y7yHhRQ&WAds>kFW9rE!W2{JApr(S>9_xa8NsAbgEzJ>}J&acp8hbj$}QN!atipFjkPl(@AdjjWUAnZ5BdixD&>=;Xeo zNKdQLmp8USbg`RPN^*5S4p*w&N~=>tY=YN%Yfat~mD| zA2XI%4i2xkDmr&VH!+)C&!e%jVtTwipSvh@@0qx(bfj8X(Z992tWE4LXMjPuk3%)4 z(|fjs%S!BfM(qpLStTbht2Mt>HT|tu>n&o9P~noMX*5*O);OA^06HyxQqIDSQq zfV`ipguXlIC#)mAq){A|sJWndAmBc@z35!=8NmN&L)M4$cmxT$qZ>#Ayr0paCqx{a z&c$?-rPpJjUO+OFHb)sU!oi2AG4PyO{)7Z@74=j&0y-U-O4kr zJyYVF;gMv02y`T4EC zry#y+BPe%)@jUDjkY2rK=S#*H1~(9Rx+cb-*%sv5-hOMQnn|fKA?w_DV(8Sq(|I)- zbI2eRlFNP?IwGEkeNQFCZPLI?vToG*$rz)KpfhBn{e|79=8PKx%NdF*X~?gFe2%#*Bx`U(%4 ze&5-@q2apBUYX23*mCEHXz*DvHCRqpU&GuOpEj*DmYd0;Gp;<^`{XN_qaFV`Q33kQ zK_ek+;>E`{N{sSBvEJn!be0>smQw+ZRXQZ}xE5cIQwCi;n z7Lmf5e;dSoY9u?%!pD7&Ix2;oq9-oRWdwe+=~VwGhVE($HAQZGpmf1#e&6f@dRq|f zpKM4XsUVMrE>ea2QziL`=8J%V=i14gy4Uk{pvPcVkVeCZ^{W#$|x4HZ1*x60*0*>WB)q2;PIF#O^Z|8{ymp@9FpAFXn&+Tp9FVm)b&-O7Z zYz)+7J&$=OFc0XTZ^pj6~qSPK>+^WybC^lg!(CQlx>?@ajo zpprms^89mKax))iH1R8!{We)Hp(wFIpBo_{L)p@yN=mB%av6BIA@-|%tO@q=S5U|B z>W;yeaHf;kzE!?c%T=c12Z2U|B7IU}3D5VkdAuEH`(HQ+HE~~VJHx{6$4+>`_UD{5 zlHof!;NQ^o=`04#?3MA@?<6UJIPzq9Dbg8{LLjr=gtP&TZJc>^_giZ5Ho zm^JUsnAsll*q4jp`hr+9C-U;_zPcMb_6+d9c?XAj}!|0Dp-FLf* zei~5ZjsGH5d#)b#@)qEGOijD*FB}7#4;k)KP>j6)418*YAy8lx%>5nlI2*1MRCa~) z;xKXaKnb*9RK%(&46%H%azzZEtyr zZVA>@=HSeC4Df^xw#$PFsRGS0EO00_s`)>*F&<5j1f? zMmI_+l)xS2M0>SL0q?ZNR!-#`jSoY1v8~K=t_Nd@eED!fCu?6IkUM<=&~)=i<^F&j z)qcKfZV6i19o@~MrY9Zv6LY$V6u7*?c?Rm7Y_sjuZhCcO1|kvaOxa<{8Iw)P7Z2QO zArkhh>G9_d(75k%M)92!at#j$U)?{rHY!|<)+yV1S>7SyT3h2iuk_U8aflcHL_bEm zhQI}^BBW#rdrmiZ&uXUQzo*Z)7!LW`bXs`P6O?V;EJwWt{H?9OxA944c}E?<Yq#HN>dLweQZ6gV}f%tfi{E3xXCdSsaMaQ7Vyq_4Mc30;VFf8nG z4R?6`I?iNa4i+~{O^1Pv;sS>N zy;aqLqMTq<)h_!LVj&m6%QC7{$bwe{Qb9dCMHgKP2&W1eaf79Aa1A<5C{ z+ocb4dW1=k(L}mZ6Z(47Z184lKI?j<>ELUlWng;CAg630{nWj4al3 zsBiJo0a|5O{L?RBEoOS>XZ-!8IJfE4w-tuXOUK{zh$P4u>=qVI-^)sU`c>s}wB5z)xn<7B zY|PZD)k6B*$dB@eo!j;NFryWs$!D6>r_l=L;2hk^>59-eo&x{H-4pt9o@ah1*;d<` zj7y}8Fw_n-Z@t=Sy6u@GBN6HppEPs#{P^9Q@AJze*KYh*`SI1$Mg2;AOB3=+2~=}V zyO>>G&lWtfzs5^JxWyW0+sS%YLQc$z-uPtkwRu-6AFxE>x`Sg|QOiR`<9=~*{_hq! z11EB%oBIz9KM^0)*Hdw5N~peOG^U44$X#=v+HG${GhUDAb?b*8KUh_-47DCHMbKet zl1g7|oys@dkR7>iO|BTGIT`D?5v`YgE?w7_+r0qgz${<<0oS(4Qm_ zO>S{Pf-^cg1fIG1pSfCxQXT?DS`0dD{-_nqQP6c5?0T7P_BzN|${GHC9Fob3Ey~$R z-UXb#aNmNcv9vb%jRI|VbrjC0WzR~NhPI%s-CMY`EM+7K*~+?LZ;CZ}O`?fhj{Ir0 z&-BXGs(K;H^M*zf0WZIzSRHu!PdMu|V%4i+bZh*Wk*8pM~+jTCX7|J_o zPC)P&e{a-ew1f&vhprqOyNO%|Z<;X@1_p>_d$et(vmr|b^uU%et<6(vE_|54_sMZ< z>eJJM8tz0>@*iwvoOvRRv3VR<%(sE+qyh2KF4BITMF6zI$sEMA^H4)vNZG z_vFqjEfrW7m~(rExxe3o3leC|aZn`Ssw>&JsWCP+B1{q+AD}Z#pmG0?7l3qi%CE7K z?%5}sh^=Ji$I(gS9{*~6prx)y;N!X{;kT->bYx#!M`!qj_KVLO9*5R8;8RN;dyWRK zNey#z2ykPY|A?;oh@+Skgd6w_na>UXefX090)>hQ!(IblE+U_dqI{@W*oV+$v1Kk`X`s%-aAO)Vb3?aBbI3U6~X2KJj z+pjh@c7{=$#>^5rH;S%#LG$lf@V*CYvohjpf{!o|F zj~{pIZJeXB5?$G)dvlA6?ny%|IWi^TqoIWWj?z!`B^&LQ*}k9WRH=gL7o>L@Xw!k zUVFCjo#5dRnz7=8Ys!Pr(3Bne@&cec%2TjOCsJmNkFKht9K9|~_2SgzjBydDPRGx$ zDR%O>3BCpMyz`ZZF|T2Ak!_G+z;S=BjlO(Y;l8YQb6NPmQ5M$S`*a{_V64UCNk7X{d3LCAbwUvxkW|Irdt)`J8L+wix?BDcxiwFo~ z2xtUp_+gvuKELr|V)zB;Z}|9>IRLT`vh6s18o$WAZ3FS1YT%*wHMe*;&?U*2*k%Z? zTF_-CvS}ESB!$hI-6yvev3^3&21PYeOR%zlT5gfx8@lm8@6 zipA~5UZZJVUMjdm)uHt{2{k#rH(5*#x!H=!c|b0^U3+>bbIgJ@wL4rE`-LqH-bjU& z0KZ%2U_$Q`2XADKQqk`$hN$dWw1IwGt>TC|uft4PwXYosOpq}xtwx9BRV~bv{+dD@ zsTP(QfR8S1ph>yxz5Z1U5hNvA{Js-Uy%EJrLK1!Nu515nVkr3+_ZgB6wV8GcJvz*d z7l^Tf+?Dx$Xdzt3hEoVZUGL~$UlasFLqe`XuyRR=i8qm&qY-_Kwn~z^? zE-x5y;UJ5OHdsQ&Gkba#Fh%6E^{*fXw@-9VG{8f8jOO(b$yqXArhDmp*>%gh<#qki z8ZlYli!qwBK>jo~eG^lryeBDjE{7G|DmiytQbpk7^C>~m5ezBB%v7B$vq+;F-z7ko zBxK8UD~1j001cT<+aawaBV%cTQHCxObIC;Q?;rE{FzNOL)n95sB)f|2o6H(l#znh? zj0|CA!CWrIf}>fkDq5`U#o>y8Icre}?|9VLF*^zI>$X3_BqSJ*Jrs=}5fQ`sX}Q3G z2^UV8kmJLzpS+%D_`lj70!kvAr(Gcq&hUjn2(QBl)e#ahs3UsUMpNl?&Dxjt zJYUMdlORl|Vda_}S9!;zHj#xXC_-vu16fe8+?VkYc3|dXoal;mMAMSl_Ou?~n=8-7egir3z6t5>q9Z$^*MGF>6i(>Qi zIFl`fk3J~`9YYos0I-5ijT?ve)S zzKi?a@452_KJX0VnZ4Kk)f)C($*Xj8zI8*ZPfTBKNQ~fBahYYV5(!VM z#|pmo^7LQ19uj0iy2b96@`Tb&jYiFE;Yaww3erKEl|8RnV#Z?_+(ngLB7G#^RysS+ z3m~)f z?UoPNi{nr+_tE)y3`uWP9$$*!zj=e*7Z;#fAiA~ib!e)HZZLOH_k*e6+T7=w3df4< zmoI~)V-9!>l9HN?#FbaYmxD*m5xuo1yn@D3bk7aLyA7XDw(F&+PJQYTn%+!N<;OB# znir+i&#e_{@Y;pSd&#%m+$89)%>SicuyL{PGPqOkjTOQ+^(C{L=prkzu2xbiT$Nt zYyEXz7TG#5+R@LZh=| zc=C=%TK5f&x0dqdxed>|zGE6lw7#r!m1P~lCu<%0{YmsS+UbCNt`XbMurPOep~iLU zh?I}iD8r+hl73o63G>p)a*qG-Y4d{rZs%>7s*Ia|-=DSMRAv0mIYG=&0fYj2oyOU$r zP1|2usE0V*C<{`9ySI?~ZL@@wA+kL&@nfG`5a^$bO`iWMdUcyt+K#9iz=L`oHd&#w z?{e4saJ`8B=a@9qsJ0V7zUu$N_ewhBe}DR|Ljdo;&&=>U|8xA46;9UMNp>-1>W-e_ zJJ(-myD*=u^n8802X&D>wc@(vj8*qbJh#ddx2dVgX{Yh-$=}qFO6!F-fr~Sl1g@}G z`fc};`;z#(t87=$7gZUQ$E|XEIy)QPuza8U?FVZx(LH_o6^oDw+r%|WB0ViL)An#f zwbWfqa*WALQ1IsG&!1Ho3D(xuxLgl&HAz=D<4Q}}B_t%WKJb28m0`18mI{xE@b2?{ zKtBIFMd%9GV%y=@*JV+Z>>dwN>T5w<(CZ{))6Kgm&YPp+)wn)W!m%F53tz%nQ#K1L!X*4FlGPuGml ze&%pEc*;un>zwY7GS&0iqOC?NGTAgeXjRH zK6{kI6OQ&($dl%l8{K) z+Onmmr;~Bp+&0TBb$<2g6%8HT!t%1nhYv5Dot^nfBK6yYTTRQxiG>{HlO}Z05C}wg zax%`R6a|UK*;ySQ9Lnh>$Kl~&jT(o%?j!n0f8A!Ely}d$#vJa+@VXvu+-R7YNe&DQ zL|SOUR~jxZqAHExP6NR_B?Wo;FNU8GCS3-sH*em|Dk!MP*{Xa|3I9zA+0 z!s22#a;sQEQW9pac(9sq*2>e2e~^lu{ku07A$8>8OQI*#)L+ulXne4VzclL1c11Ff z33yD()I$+PL`J@aQJpBau-iQRoRj*fJEd14R2!FofIt*;J!Q#QPg@XW z<@p&DZxo}dmqhw}leZx9@|fBEX8_W;zkfZR+j(Gj!)d-n%;UIO%3-_wNQ&SwHwBu< z>EK(we;xP^+sK*L1}je=jp-vVfvHa0Q@bjz)>{;=8?U}gJX`5Y7EO7_;(BNfGvAvb z9q(nv%F22V3oEs@mMmRf+`$1Mb&lhSX}(b1tZTBz%Gx|+#P za-10So%coGW`jAxaeeCkg9o%!R4D;^<|e$aN>g!B9UV{T>HU3t(0$v)&)80Rj>|_v zPETALt|pZ#Ki8YS6FTi zFli8f`ts$=Xq~HLg_ewQ{#cD8bBPg3uPu<+?PMwXYtwHFQ`7%yYilPyvpsq8iOslI zr%cT+B*Yj_P*hZ`G@lg}7A`a$rk{3M&wH~wU03Joou z!Nt2`z@D9*W*h|-6)|yfjO*2vo^HJ{`l@D~o}NA{FE72cRB_|neJH#B>VntHcgBt^ zJxz)lf$(XkP!EcZrV}{r$J6vUT8t1__9oO?Jl@4);O?-7PQj|~fY#c&=A$x345N+exG#-O;Mp$Cv=%&G(L)X*j8eP`1()YFa0j=XG?DsytzODr+ zf;Ls8TU)HyK3maDb#;%4f>0`X<-C77D$K_OrKN*r)8%r8dy@FocgAm;&-|L|wapw{ zjbSy!R4+GgG%CT+YHjw#!P1I?a#zYzd#j=nBeZP`({g!vnIPcl@vehS#pJ_>N449* zq3}qgq2b)tR_~uY_3WG+(s$0LrluX;-FV+e+>fS2H8jZDLnybYqoruq?YrV?RM zn3t=Y?O9q{RxB<>cN%<&7NHKaV+c{Ce@h>Ec+6-_Cs#pVB9X4w^VyOJWp(#ojNdJeU+D&Pn_-?9f_vPTW^kY_#VJV z@(BsS!SwzT7e^AUyz-I&%c}mu6Sks~Nk$3@x7?d{Hh#%wBxO8W$U@JcS%K$qSdxrO zNH~$n8YYo`zDOslTQYXM#5^;5yt`0)d72YQ&Km`Q7aFm*G|RG+NX5hBMW|itJDkJ0 zbG$HUZ5uK5h#MELS+!#8?DpNWcXD!let62Ay)tR}GJPSQ=Ts}b?{7XNSZ*iO=JT%i zT>s!bQebvGT<}YGqFlk@D4vt~Gv$Q|RH?2Q4KB8ik;iTTS-Bcx*;j9YeY+`Os z!^%34@`jv}G6XiGS{u&!DwtH`-JdGDu)QrSBO??2FnMB*DmO2$wXd(BnPxctZ2$oV zIv~C~4l{zJq@=%gJW#yf0t3^Eio&L99I^0dW2Hn) zP3iml`zx!eQj~H!`}!)d-iCEANhR^cc65lL`D)ExhF0lMaUu}%ii$&dbN%r>#Cs#)P<-qFd(CQ1a3x!&O+egDDX7!>= za=KYiK4?*<_Hv>E`m|#ung#)We!9y&v9%Y66ON^U3H@~!#LX< zmQCbo&DX3UlL;nZ){=5_q+4Isdy31sxH{L=bPib&;^qWL_Q+xY};Vn<#-=0KX-^s}f zsbpjx&MAM~j~`EZllU3D#_Ql>py*x#DWXSz-d=sN{o7i-?k^{Af*0qsO0UoTs#{xi zR`0i1?6R!xGG}{_kN~);pdYhsyQy}|!z0Pd3p==3nFbd_TwMHnRMf!G(9m0PA1^PEf?eNVD0HAu zkI~CBGHA^a@riTBC1Wf^*vF?l9u>#OPMBC&uKVVbGS29DfW4Zvr<`Z2L_#PcSgTM*P%*;$|G+`&<{y2-qllUN8vC9hsFNVjS9%P~*QD2K69tH-6?n-YW zuoXV$ous70%xVgxTMP^$NNjGG{j8mdU=iDz-MHxJ{|k>ILDXz)5f&5W+;!RSQ{Me} z_q?OClM0DUf)(`Zbd4cAJX}mh#t5$XdVSz;mRxE_U!Q2^;48~XuI1juZbEIh&qbR< zfA;t7eD0IBU0$5QtOt^Dhf^iW)NSI#P7uG+yRDqBv09#=7*HE!4RI&~hzi zV7Os{h<^N7IK|OB^nn+avS`jQz(AM(V+IxAqu*PJ*$R;oZ}s%3U5~cXD=Udg#sZ_G zN65!gS#_IkqXkp&FUw;)x_SaJH=35&Y)x{*W{{Vc9>Ss_|7B&xfX||i==8$KQ`77N zNsWO+Df8~#!;u2*mGQElh&s8IkF5Qpqr&n7!bvV`;_zWbVIXk*ytFU>+2i8kK4&iY zQCj*vFwic^d6`a8@w@G8!<#pRp#3p1gXfo*+n_3(mSZ!=>aRX^8}geDJ#Pzb?n6&sD7~1LEFE zaz9)y)<461%}Ygv{A(wzQLQK~^fk=i-=#Mpn+X5w>|~WKG184|yY49IE9?HUA6?RG z`pALbzja1#sHv%Oxu3FwqHH!`1xmVyjSZ+dF=zDH)6-K>P;j(Jzx_w3F$-06xAS6{ zs8=6BfOiP;RP2~+)It7dW+okYx!dUIaIpr?8Mi@0!&HILP6IGWV8Dp@UD!d`IKHKTan%`DCn9;zEsp^WcVy~M@NK) zHq15q%F#y(zkM49y4GBNX#Jq2t?j|L9yBBpIc212BVuISH8y6(Q?S8~K+G>Ju$i2_ zKl3WCt|kHR5=hK8T46;AUR)46A8s%^JKODGSQ*wF$tTSkhexEOl6rdo{q8N$sG@eF zhp+Q28eMutqvU+R*)u!mdwqBW1R`>BkL0uQ!TaiZOTtc+IpF^PxzE%kFuI^Iq|_PJa8gm4=q~W@z*XNtENxEY$;e3az9M*$h*3d^oYm z-!^-7yZG-A08U|No}g(=lc-YD%JUK%3bHRYz|-vHI|); zlTE9c(i&M;%^E=$7ZO?ivJ9a*OIv3~_tkVlP#v|qZKUqyulV@uy!;lw;vmJ8{%F|P z9!;zkywRN{#YQoz?eqFdS^>yqCdG9#6r3kU5v*H$`0L`1x3_$e@l{T5-C@?MZ%Dsv zJhYRBy!NL&h0~M77YZT^>+9kcc``9QXkI?2xcF&$Bl$^#6#3d$)z#G| z`E0?Ik6g#C?d^j=So-B*6EFI7;16pjEC{wn9Ve%z0GBsr>a~f^;ZUh{dIhNH@Kx3m zRNWh4;oFG#Aeg+vjWK=W7>RUwJv}`ztj*2MLOU(?`}j#b_WrrKj8V+my->ncPP=*) z)3ZtF4B3hjc6RJ=2?r5LQ5El3nWO$tku%jCmYJk&W;b%2-+Jg zX9XKBZ$3KDS4H~AN{Zhl(pkh!kA!hZ5rF`6z7F-yfbe&O_F%^T9_yc(5-@5jd(s8Zrip(;q`?`- zF(}gg`d?DVcY7Kt)GQR0c%~CLjAi(4+?cLCp1-N(W_<&#u`%!K(#+){4TEz2x}&o> z!Yec(iP!6KYX5FqtoDWJ9X6wue~ov~va{adK~+xIdOf&RD5aA3*H1F~XDsJ7C!=b3 z%RqXk`Sdw^JYs2YEhm0^)Mx_kiOpi19eK75XpG!cf0MbP$b1+4b-aBSLnAKyql~7nzlnq!AG8{o&U8<9l6B%EE zGSV4N%gI9Z-g;Oe$wCx>?9DN|!6LBR124&Bg~Bp;W8jo(j{g$FoPXT^+n6w}n5~HU zbz^I)W~|P&;vI4`qW_SUk(s$7C%HSCxhBDI1$3|B{@;Eu%3x3AvK7GuDjmB5cSE8; zfCg6Q+Nsa>1eMF z8I_E2)4SvhUM~ObPi$)4R~L*o-JEAABeJ_!zvl*Z2)81QKQlyV|{DO^H(dZY-k&;LA5U0kF=9?!b- zChj%f-Hu4)PxkSdJ`k0bwwOq>d)^+B42~ABNo73RfW*-tVzWp;o$q8()M4gg_AXm! zXIifMQ3x;3y^T?8A97x@mA<-_q}|%RUZy_PWA`g2pEa_)UEhC9A`9PtIei~%ihMuM#wOBH!Eg%>M*iWD@tk_?WSv; z@d*hXcR!wee|?iwgOM}r4;wtsJCl54BqXPM%x@TQZIwXyG~rfB(LR0ltTp8( z6C-0aM~8%EWo6)T zeRZ{9hIP3|hK6)X#vsoB>yX|U$+P)QidG*oK8K6)3Z6|X2)-<{x0hEa+w_W9Y!=0g zP;sq}Gwve`F}tgo@zn?H>Wl;ktLdhN5CNdki@Ru7$L*HB_IE}%->$uoR5kYZS5wXa zmv@_(PPW&?Hdt%zsLqJIB)bt+n9;ig2GSYa<2OIz=4$)x@pK%1o3sYY!2N@RrD@k_ zUDnv+#fbXpBWxtRZ$Rbx9Rc8UHiq1pv}(V#>QQlUJl59MW+%C?s%m?K%dz+1(n%Ei z+-!uT(gCFv_EL-Q5ho{cRTbgg#aPzZ@0FD-HQRA9G75T!nD;H9!h68`z_vp_U+0&gwMv4Xap4y#N>;owDMA z`S9iB(gHC6s|+PEPW6SNj@Tb^oWY+IPr*T-;Od!q&bP- z^|}Y*dAa9`|M)Sa%yy88XuFpkY_aTY-%d)hL0EZ=Z`Cw4(MK^_BVUe{^j${IoG;%v z+;Si5&Du7JDF6vZ5){T^wp{FTfZbSgKJ9wuw1={~5%cP)k2eFK}itIO>~qbp9?2MHjKb#*!E#9Zo<5)#OGt&Slkxb9|LTR-q)5JHUV zPL6^EdBMcA^=HAYW~pj|%kz>CG(4k5L%`m%vaxMW=PlG6W%jhT=EYBdC!hUIx*FJX5#;N87g7hA z_Sk9hTlJhc9yyO9n!nt^QYkeF3;j|6`5Ls4#to@q%XSc_zN#fg7$R>hE%S9|JrBo> zCq1t&_>LxR9r|q2j+nH6@jX`(xK71B3sX}uF$LejBDi1ko_x(K9q{JRUw8%GHAH6qo-4Kmg0!Ia zpRJbX#e1;YL>#7y>hvf9yD=TtoZENr>KCbij?PF7yV$B(fAO5l)X@TXr7*d3xVc`V zst;z`YG-ecj+@(WXJ-!3!sB9nW`EVrt`pj|>UL?I*$P{>r=kL9V|se}Lf}IMrprYj zac*s=6nG=i<(WE+nC^eTW|zfGABkJ{%P)>3uQ>=5yVqRvg|%a}oI>h4bxB3Vmmg0a zzkEqxIr)2FlFvzBnb)NS5HAH~_+>PF9*}pPpX{A1M#!SuJ^A>fUuX5IUKegZAp$nBF%j zDJcU*QiZ-|UOY)!US6iyW{a7gQl)c>HdDJh!`wR?>-*|C+XbdLZ?zi1{Aj08ceyf;5HH+_&_Vq6W3#`@-XeJwdlyWxv@%{!< zJp!Ua8LdZ~Gha~kC4&3DcVfvwBAr~9Ba&Jqiu3V|O#^k=uZ)c@6hbOGaaYTj1ztEI zBf`_hjKdxj6jVpTj6$L6=;toOtK9EoP6aGKUnyZ@Jqq!#)D)ipnet;yXMJhut&~*Y zto6pacpr$8>R7qBD3jE4Q93L^BeVw{`Ii!p^cWl@NB?7KQw-ACKlAH_jYj$y42qy=wzn} z7W7%bs{_l?JbQK{FWwa0^oNNj`s_zjRfa+ptG(8L(O*Pu}eL%;HW zkHsy0c<7^aQ}CNio*I2r;QxQ>LQ`xvJ>Hm*-K6weRNepovA`pFop;H|=l}0%4ESe& zcN0+u2vCt$smQ$b3lFUD{~T4_e!=@u=K`vGg9Jk8T+#5dCN%N4GQ#6BSV3A6fsC0LOlG5EBo9=E@N?N76OV}XYT~ZtA?rt{S{5Q|@yyv{< z{O@-zFE3%iUTfVk=Nx0qaT_EjBZl$}{}}=T0*d%YVFd&PL=FUmhn!E4z!lX4oI3Ci zl7Xa{Fv8vapR|VDNO0w;?MHP71Ozn9`@auJ6AxLyMPx^DX%Xc4XHQWcqusosIt7>T z97WU|g>0;>Al8luLiP}SM~KmD7c)oG*J9$*a;l$EaS#w*BZv!sP;#BxnRRh}HG0_$ z-;X(#aF_k~HvO$1@n?iDL$5k$#-F=>fAhxR1I7n>k?-WvKCeY~3?5{Ke0lOlM9_ws z=pkwHacSM;&CP>Czw%il#1C@=M+dw+ut^74y@N5c$yy(tf6KQ{DC&-Y)%z!;e&~Wv zNX=`09Z+^aQ?WRBIFL_M+4GdBcXll03zRUa{{97FXf_RfPFmWxr6mJH3Nd}gEIYkB zGvA76ENACR%2&^VqV8|whn_LUg8Smtt5+F==1-qTC{R9#lE`+n{OcKh=x49|1wSVz z(@A6>UasH3Jzi8+vQJD+e02QR&y!j+-aLCAB8RA!Kbno1)MWcTln_OP`-r(x-ZwFl z%}MABS7ZC&@~_7(?e&bx-b7cI$;|Jml=Rm{iy1mUx$PgbZk!A){XOu!^l)hjb8h@F zq)$;Jj`h5=`x?l(wL81JzhT`Xxx?S1l7b_uD3B04!^1S7oiNzg$~E=Q%tmlX`a~bz z<8PZ;{(lW2Z!;X_lW1m50a4Q79!$wub)7s~!at%a*RBDxm>e-}e7gipxJSbX7P6dJ z$y~j@F?IGc-w*hye@{S6R7}ig(ae^qsi}#Unv1*g@)z|DeEZ!e3i(Q|Zncq7NfMnE zv)k^toD%R_rpFS@c5NRZj6}~_*r>Sv!`}bh{siGt-EiDg@ZI=(D3lQ!0|I&iM|Y#6Ig$kzS-JbO^JeLB41m=-@|Tobar-D#;bXF?52YscT_tNz1%t@(8CUCNd;?sy@)Qyw+fglN%iy z{|!{;?_DNOFWJ!k@-+)h-8Bz~zLW2uY56N`Yy~AHKa~QJf>DLSae{eBrX00ARdn8{ zc_9>9Qc}_~Him6sK{KiF&*plTuSA`cMA)yRlJYDS1{O>Zs-kaU5h9aY$HZ1nd=mx> zQ^*(2SISVymoHGxQ!-bgj+gr+~5#Q1wI+xgaQuTkFzWIz4;`&-g1hkw@hTue^#iC=XD z8iF~Syu5r8pGVA>FJG>V8+e>|zN!>J#*4Ivo(PAPR#aqrU7bF9^eAoAyvF^6y3Tnw zBQI|wWtL%Yd^rGJkefsBYhCl`X!KftVwuSR@!`=?*gS{j1Ud~34Jr}0(RGp2_89N+ zAEBMux>)H%cIAS|mGyNo1qDp}u$-|jN^0uhl@%j1TDC&#*;>DVfS7Zb9^)^o=}L|H z%_?CJ70OpH2szPlaBxm{rvnx8-%(P2U0Pc5aBr&CjUW*G>f!rB^5qgqXGe!Wcp3h?MBb|@^V&st!gvJW3VRBQ6)1Jke&tLwcuEGqzFcXoJMIUGE0N@{QBi5*Fu?c&4d z*tCpP(6BHR5bwS%a;t|?5@giT&!8%r`rIHtMn_|5Kr1y?8N*_YD??o}p4wQL*Zeqr>RUZ@=qO zc&$IkB!9zT>{Kv`>>o18+=HbM$(@~@-+g^6T_ip$?LjY%M{6oKC(9^Kw#U0#S|0ee zytlV!CSbRGO-kBQ=xmyQ9ygRCZD+wR6!3y+t<1O|AC|;rSi+8tj{g4c_QuZMerc-0 z;@!J^si;(K@ZS|;f#>siy8Y9;~>>vwP8`a17UG3hlUfEY26+Fw}_UhWK= zZ1mzWfhBz?7+qRecn{(M!QbEC$i(C&78Zn8T%J05q(HT}XTbLQpbZUN9tJa%;&~{J zO{?}^R5U>RgbC6BDw4OicW14Jy1G2bPGw~*GBPqNYikLF>wB|x!{ug*?3NSFqXnvF zQ{_~${1@+fW9V3%A4%mqSiJ=EWNuEa>vO|K9UV-sUHhlW$7c5XU!RnUiVCCz zhutZ*BCQ&QH~kk!>tRB{uv7tAV`Jl>)$s7}_V)H)U`L@6^FGlcDZ2w}In?Oo;eEas zh`A}7%CEij^9Fi*zKq80vKI;>Qba@qBrH8ED=RG=ha?`C@Jz9YhqjwTWSyaSLfd0d zNxs+GI`=!Xu#k3HTss7tZeyx~25fSA7!0EErf|*`iO5uLz}z0x~frKnE9hCOcdiIWu2yQwNkT4>FB~j za2SSu=?ndcjO6mVWF_K`yR+XrK6V77Lu-x@xc&W0zwO}cjDnQ(1IRB=?t#u;3be^cTee`%t6J1?haAWBN)^zWiOEQ@)ZEbCftQ+3TjZ9XnsgZNHy_p&# z2xQc?t^qGrv-o(t^L*y%+8z&*?*o59`K`N{8SMXBcvUc+-;$H9t_w%a`}bzvQBbh> z+atXS+ za)Rh9{aF7332|&}j69bE(g0=@1j+g3B_=w0^M2Evj4u+39OWxAnL_H2qyB45Ow8ZC zy)3tTJCkK>V~Z0_J^~<;*}NN_G~fx2W!kg#KXo;i9Vp`J_GbvWjj5_+ zudg}1F3TrRX6sxSz^i0{a`p%bDdysRS&b4k*8R$ic14}^^!Jm2v4NClw>3hAO32x~ z(BdmDE6V_HOz&Ar`tjpSi{9(EZ`(M9b93&?S8puCc!IM##j>KjM`L8)tcx{FhN3Hs z$KjvIl7F6^osEHk@i{gY$Gw3#TfwZp;7_%kVQFb8s3BBLDBHV=r8;%aC=EKYQc^EK z0Jcv};YUk|fNZL+u71CrKsJwzjusIWMFKd6lbc)RlKi;zK5DYE-homo2ABa{9mQdC zdYa(QX=efh85!AmZ^rC!Us}5uq$q`A9X|fsb3+9Mh2Ot_zh-3o7Dm8BFC3U&AS z@5`bmpt}DR4(2u5b#*d`f`sqizq6W;{N{WiTrgVdab~@Y*u}BBwszRd z&;-(tSb+-k)3##m953Ds$juRar?`m7sE=MVFhqa|hwb3ySxT;pqHMCOZDz2=I+NEABuGv z=4Qp6+4ccev*Jv zbB5G$d(Ibd`JI$CZ1eRX`VxP(LcXnoL*}nvzea-6Kmr44{1pxkK`sY<1QbelpOn_u z339m_qkVl50n9ZrGE&;M7W(+{2_n)9Jy7__Qc4&Dlaq-~Pfu}QAQfrXsX!x%BnxBn z?J7%4TSrH+!bBAfIxBo?>o8J#rjC-0^1Z~u2}XIW0c9+1gY$5@ZGD-k!B|Q)?unv0 z>#q$b;!$C*M$gdb3JZEkAtC2?j{F@!jyuM?5rL5pA3hv!jfP}oya^}fGas8?@<%_T z;Iam=$i*79YyX=m0H?pIXW`=}2j&FB@4wvBNl_}YNB@A-|Kf7(H$ycwws6cu@ALdV zh`+x`;iWf`y80xW&e#nl4@F?3EGpxzX6a6(hOz)urS~^Zm+fU=4RdU+)pSe#U4ynh zaZVOS26+R&^&9vVn#5}Wl)q}E^*w#;yg%7BFkN{xc0OL9-Q0Y~-*t-^I8kqhg;r^! zX)xJ}7R$8gV{>%vAQZ=EXdV^=kr31CE|s0AxN-qt-gK5Z0HZCnV{EK*g;yr&+&8*l zJf^0vtu6Wc+km;rumY9gdi9qG{Ff`wuWvjHqIKoJ2lxht|H%>;+S%Rt867K=yBw5@ zWOwifBLMBjCf?w9PytCn)76)WQ!68(d(>V5b#T}kF~erKYJZcQXsx~c$Gk$=&p$?Ej5&wv9PiGDyqAb>nSDkafn3-OU{V+pi1)n&zU&RIyDm$qyEeCmpUY)~Vc2 zs&6}Yl4f`WavK|xiI%&<9_S6P(T&1v##0)Oca9itIV&R;(wfaz*(_jJAb~~@O|xSV zVW~SWmsg2FY;^4q*-53X47zdWY)aW`uAO!}Z=;>U1z2PT3--cpBt2Z_)YIiC&sQ@2 zuRRvzZh4N~P1h+t=-*E{RQ<^awfUB{KPnD;5K7P zk2_c6x5R_@C2JY~fEb!c`gV&`yl+J_{L-cSW7I zmxmetS-?PW+!-3i-PTN9&bMr^zFg|H0dj!RpHH_~W4-LnXqrAv=DGRwic7;#G2NVj z1e0q|GC1|(VE;L!OQy^~O({$Nn8s#dl_WgZ2A9cd`P^z;r)+AV|Hc5^yV@;q;U#)d zna6AJVEPJci!rM#FzA_jp@Y+i=4pX1#ZB5)W3`DxsM1yhG2ecq_AEf(=AWrQPr`Y; zRKZGEMMhf4SGBpnTT#g{j5br+a9D(StIpzGbA5I?wCN;u>&5L&BC=TWE^1>yv-Hw( z(y2(XD|%c9uOqlvRY6XU(ntM12i0Y@LqCbXoPWJ|h()}2@kl>Cop&l1*(ThpV@GOt zcSr5`mT-A}@vSa74^7wkWJ}9j_4VjDg)JRRS)V5=A+MBU!$%d*TMT??K)0gF!Tx6< zE@#s!c(5}B@&M==B}R+9f`VTLO|GA zC9Z$E8o7e7<_nGXc3jM+vM_X^=iv_qUQpL?$GNy1R1+F1Z}Gw2~F7SAEx> z_A%-ykC#f~4Rb0`MX=pCkjEd3Fy2~$14hT>HKyf$dVwQ`w%6L*JG^okhncEAocrsp znUq{6p>srh8Q%EaM_4$ndmF-=uG17mLAs4# zM?Swe_;>~*w<*rLe$~idk-(AABs`wRNm#fePW3Une`ur(L-*!_2+^BRrg_=B{McIO=ZtrRK#zrul+iQ;&j$P>$Hn#= zu=F##FllQYVz0|n3SQo*r^}rOdNXg&FIaDg0AzuE{T>Ns4*_8**8`Nt{nqk#k0EIsJv_#?kNHytP!EyaKT+J8>iKZ` zWkE!_)#oo}g=(WFpE8*V(Dmtc!)0BY+N4r~>*d$kZAuqgja9BIe)f6t@y@_cPnULT zo*%}Ffb5uul1u>dZwdi>T(t}uCNXwTdH&0wkZm~&YRxL^gOit41>(jxG6kcDgI%d* zPSdE&EG+QDdT%x^n!4jxBayi>>%RVy>hKF@eAcg(=iG!`&PhNVSMeR&+_$GO@wb*%R;GBvNr=d3iCF6;hN8n)aG#eTqspFogr*OmX4qvm?Q6Z$ zY7|sZVD@t7Gk`laH4r5ztaBI9A8#sq>^T#P^T?E>$6@8FmhN5CInnnm2*R@MebzS@ zW$Yz_uOau!pt;aGlHSBOlh8Y#%?^x;pQRjuOjtNLi6?dXZ*>fgWHs^F>^VtWKD;9% zD>K&KtYd!8uhzFyLvqlOE9ttj7-()pbj-m`4b|+7!o|cHhktm8cq5q&Hf!tP;PGU$ zrIqDx8k42ZTb*5)+p8N3p8#^m&o{f@84{j=;oye>wF~z+VB2k$T}w7w0iUNjr?cjR zuW2~pc^H;%@k%e&{9vsYPRh+b!rDN5IFLVc>*mB8Pq^ z5P7zGWB~ST4v@B`?JlMzZK~x>_HlW4t@9n4eIIdO#9q}!jj$%|HT%81$CZpF4i341 zp~%b0$z?+Bfg}Jm`a5xh%_p&eGS#~s5frkw-RD7y@oZjTDa#axhs33%a_llq%P2Iz zh8G12roU!o%^E`9dO*B8>nrAQ#t8LIA9nyLu-0);%{b}k=%|2@|6=Eik4Pi47BM_x%gX8&w4$KSE|pXzQ{4*4z9QYRB!STRcM@Vi6f@dTwY8;LQCR#f=dsOLy3Gkj`@PXqkPhrynCg>p3if5uc`D5 zE0<+pkSdLjT4eL)c-uyGacN0vy*m8w9Ce=SV%<~M#&qewK2ZuG=+^{k1o!BWGDpgN zI0NE!3J>Jvv3&LYnAGc4nC9a)UzNYsj@MpKXgomRVY4x@b zwxmm(Xx5t&8|tJsOBOfk6ZWjEZ2Vrl;R$0gh^HoF3)|x(5z8Icw=wDZA#!9kRDG?9!{|AL)m|VsLbEWh}4QJ3TwY@uZ1VapvN0iI1>pnfG{#8;`$C zT4k0xn|8C?JbjDL*P9-d`QjWNvkTevD?0(z*lyh|bdu|HtTPyF>zdK(??s>e_VIM1 zX!YHR)r(OF@BM~q1OCBD(^Le)nN)g`S4+yB9MyrR3_SPKX z*xEV&91D_xK(xi{C7e~*WneM@S*ctUeHJYsNKBbb}hQPwC% z!=_*DugW>N>0#KI$v38eQc@40pUE+3u-bH9a}5_HMNq-TB_%27_z-bSC_w(`-5pWI zZW|e4ErJ+hXmC`|#8#;QAtj0M#}9>Hr3%a2%f3*kbj?*iC`o@z%gAujK0wA0;5 zmBkR%ebu_<;&~LCe=+sWBwnXbty7XNg9qOl^W`Woz zBR)uFR@23n8h6Qd+?J%t1ozY?*Fo`Z+tAiXe%q8&;>2+qc6~kX#a3B1+t_4vBxIER zJI3N8r1SYmCs2NAbuS-8<~%~j#8fg|6&4XGT{?o4hnneZN(yz{jHA+}n?-hpy17OD z)u{eDWgD(O+%|L%%jQ>rcp>Q$cTRCB%@^YfUS@cerwQL5Ymv zSAFJ*^uWS|14C(m4};B8eOYdjpey4sZLdnCn3aHXgF6m97#$vgMC7b8H!i(&d6KxMY)_w}hwS}%;; zs+T<+6)(u57klFJDn2=f`bU#)gkb*9L8oFXt7V`RJ_pmyHdMpqw8a_{>!kpN^%wDZ zY)-E0UfWrcw0!tnVeDV$6ed(Kiiv@(VXodAw>A>;B3FA_g-u5VQhVl_R9I-~e&!xm zxI;+Z#Fu=7j0#^hk?h}iy?QvF&oVA+KajFKARs$fAm)N#Z-2Ds3i|ImKu&mpkjQS6 z@9Z68X;`*aKaj$AHBn-vSW~u}zq>0xn&A?+LM-svrI^@N=^*8`;?T@f4jycnG`@rL znwEWdD($Ep6Pa{0H^(;n((Ly41io)$rImKY#Hsf!`w-{Y>V?ya}`6L3@g7bU1>yg7YfQ zGrK~18+WQa{DI!w7m@m-JiP(QsP6b3AT)r| z?@h5_1L>lZd*+(M0`_qEu&=KWvloy_6=QyeM&6ijUpuQ^A1$tF$Fi4bPhNS{t3w{# zV`MosAuT@NgQs7?ebn<84W;opSe@S?eDQq?Pf!~1vlL9Yap>#9pH96hQ7fN2Kkua!W&3SwS}6Y%iXnoq2xw z^+%3)ci5MstI@zYrz}TSAB+WL+x2nrB*wUwmW<&xA-hC@2npl$BkVyO!ZstM+5tW-qn4ijazD zj|_^;EwMg%hJZ@MF9C?AR5EW$Umq>dizEm5@0K=Nv|VRS5px*H_;bojdd9YM*tR*~ z?aN-&UfsExmOL{C)5>u+=G z{*Mg*m~yf#^MwoCFCI~`ILUeKqR1$)hJPTHw2;lBn1i{mlP_W)qR++V6 zy?;b1{_g3a9((|JNye|VKZVOj)lVZY)}}}A=LX>AxVx4>-qZGBLcZ$fy1FD~rwkyI zE)_Xh+aDcn3|9#(E~ZQ2as(hR6SoX>C7K}dS*hd@kyu9P6eGW`^h%@-+pGitLL1f{ zSwnGdUXf6w%^kh56GhwGQ$5q&UD8?)=w6sm!x}6_G;yMckjMGU7}P+D`Z>rY!F#i{ zxB>(5TwwHXK|qU)TzjwkT}i z>)m0DitQZXM7+<5dqDzu9IH^~ZT%oCJBoOZnY=AH7a3VJq*q{hwu*xJKtT4vpA0va zEjG)Rna9uLFZN#j_qq`h5xQDW=q+*ceRNf$9L|w>N58kn`&FxJ)Sthhc64iD6iV>o z{nF^WGVk0`^NWq;fyr7coze5zeSPxQc-dS_vy#zj-NCuD0`k1nCL}^gjZ%J?{3U#h z6O;ckYB+L^)agh?!>WdVWxYL*=^a!j#A7{tglb)#^RJRJYJagjAU<7*sBKI>#!>0d zUbi)z9oTsL3?9MTdAea-9x@+fXjCn3))(f`b19vdoNTxKr?IoLBiShEru9{qOp=YI|--48Z!rnY66>?>$tCr1FoW- zU5R=V0s9ge`(1f7;6X1j359{sVlfuoNK>}p+tHVD^+4CN)=!MsJ5O}%gHLAWP%T6% z2pAlQNZv92X;N|rUM4~!=~S$qvCe>cdvfADYfst|op4mtiRsDj2?;8zxKo!`nAbDt zMV=QZT>*{nn-@Mmx?ZO$l-gjVva*zm!bmOj4HQ4$Q2e0+ZQ*B5N?Y^|>rL>yCMA6v zSWu3_fGNjIdBV#W^=;g`atbBk2mo3Y z;*wP2#G9RnG)=8g zuSA6=Co?z#>dBk#(0N_!u8t_yXmOwb=`VMpI~!`g!ogO)f<#Y>g=6v>e5@f#JRFL9 zx!y_>WJJU(HLEHb?<%N!Vx|8Rh<=rpH~u-&_>0TS+Hbp17-VJXbtLk};9S(ynErwv zu5W!i>pHlT!+nt>a08n_wEwjSFJRI@wgz@0$ylGxxLC38k%=KP1#E7n(ZvxsnPR9l z%MXISK3UJD6F36argWuz%CXHnU4fnq$obVpsn?ZE%HBMhfG$aCFRsdAOGftjrR#8| zG!1C-=wHvZ%@PR+q+*)Q>sBWLN9wW9q5M=u5K|p}#K~qis1E?~@ZwHmM@w{UvaD~6 zr1Sl>Cr+au03kBjU_zp!LrkktpA-^{iOcM&x5*h82p=A^>O4D`!3Pq6qB;SKNy8=o zSje4bc@*9-xiWWyi$71KFXCsR6jIyYELr;#-5}-GT<&gRQp~C&qd8a&@E11lR+qbS zJ&whEQj15@@cGDLu4}zyj#U%>v@*ILh|VhM43ve=3BePW7e7k8C~j}A^q04VN*)Ki z-wa`0<&U$Gz*k&Ks<(bi&7;X{?RLBC4E!QFYT%y55s4p{MNuZMrpNHPtR44eK3C=T z8;qW9n~vuw*AOKKpCZr}z#obiylqt;-(lNds0J#R3O%*##HQ@yT92T%z_jBc7pYLyE{$;e4Kov+`;7QY*+ z^@_!8BH}Wj1Q3b}o}8J92-Gk;N83Dw8*GMzuDEfEgP{T&+`w|s_p^7slUeTjm?I%B zo#`>qHMr>r5=4p7ATYepd&OFf{9e4S2KtYIfn>*I6mYPC5?7pib!ND><0h5LRi(jD z)DZ0vs5ZmX{-s53=_+G{b;*N2{6nq~#6+x3Z5X8sctClEo~qw5u}-=?T&uD z`>BbEN!e*KS6L2x=Rp2=&*P>jG%%2dghBJ8n&~W<|9f7H&C@jxOtW$V%vqh!fI06C z6P$W)&)U_)WA+4VZ2JTtY!vV5fj|&Q`1|7)HqwbW%uNeeV{rqTv*JNRP+3Iw3+?l9$FwOf_nMq^+XhHWQq)XLv z0|6k_OggORFK;QVF%l^*-)~n)0|0_i9Pc+z5`lN2a$`JpzuJ%3GT?3*6o6GtdxvFX`z8gmf z7dGAP*CQNyOgs$ODNz$ptl#H`qs1i70i2cRj=`oA)0E0^i;cTyUcr0#Mfn-k`Kp`1 z`+d5?BBb>lVaybE^knG?P5|E>ru=sdsSDkcy+?_qx~{I(%lfU3qtUxODJjH|MvnsW zzZi(02&`-`FuG7N7u90i%3v5x^WgAsrC=aN#Z|$pI7)e1Sk&(&uB{C$Doc)1t??L< zT>9zjckl8bSx>g&x|BND#wuv}!c z!t+;&%zGoC7yru5C%LoOM__~==3=R-L{3fp11psQ(1s%kU2K~BOkkcgJbL`=-d29b zffqv#(6S6Lx;(UhDu2JX1)snpUjHRR-Ah1@*0nfgT)*&|zs08$D;Q-y>|jdb zcPJ9=1@I+vib1zN^2E(+MtJFrBr1UN{tl!+$7}8rt>S#VIy5vSn0058K-T+13m+cP zCoCJaFZl5A#?1>~B+CF(B#Oao010K@A}6GiWML^IyCU~U?St2U+dg=Frr&zx-`~|Q z&wYJuB!8Gz=Ty9cd9K2QA-V2^yds#Pu#TiJ;fUYkbZ0qNdVaWCzMtQ;pz<$EhQ(Mz z#w9qx!GB>&n#|4j+b57;Rz;g%igZVVr#_CJMXVX zkI$`alq}b?pVB2ICG~jKIk_23buyVAJVD@g-V}FrWNFPzENSnn`#?pN|ALtNhdYYK z!VdA*SY1x*InqMyrhMT*$p7Z|jl%y?H1G^!ot&I}alP*V$o|qAQS0&@zMAoRUTLXR zK1(x>uD5ShEQK%9F@wYGOL|>W%0kkjln>{RgfWR{LxJ%}#Z71$>0Ez%-$X&9K}B+C ztK!{ksWpXYQ)lHLr0W%ocI;w3Z=XC4b!61&^V#_P;ch;7V8@nXZa7VG19LEFdu64i z$(^eFwFvU*m<;eFRyYbKz3$>-iYz#D(39!qey}#7+6Qw4u(?hNG^trDuNrTi*=*`hF}3U37Z>zoB_zy)NVN>-liC5fVs<&LW3?LkTCC#P^~ogQ z<*$y27^eOQoVEvygC}o5UsKGq9G_e^xuppib#)?rmU9Ld|D?xa8E^?6lG}ML&~Q3; zUOh|Vy)X&7j8llAazauo^9B~)x!f6?OSk{G-{1o{>S8fwLw{8Dz?+|UsKFs<9}w$B z?CGLbR~)k3ZwK8tTDRv8!AFNr>mbu6B>cgOH>PhWBE(|jJ0{f~Cg<06+lgoVD`F{X z>v@^aNS1ayp4(cCC!{S7ERAOJJd6I>2i6GfeNz|7Y6S;*>2561v1?Z1PRV_MptqL) zAFcMD@@3!MFrgBI=_*oA%3m7MJ9E&Gk|W!K-{VF6ju)!Aw!9l6Xo&1 zBS8&M1g-eLB(}XBv+|(bo)SX}(^={O&``Ud{-brN^?6x7eW@nP3lnAPf(-6?$J^`2 zTOy)Sbu<5&A?ri3JEw;;`{rwFYvZuy`_2)y02o`T*tQD|e`7F%>zT3>(gQ0J=B|71 zqXioB&17p2ysc(}9gg^4n@<^yprV$mU7@&{417Nr%q1i_q24O7!QeDPf8cxTTM_6H zUD*KYHRv-W@wt5XXNT##uXhf%d|nSgAuk&C^$YZ8w%QV2TH+p=>LK~heeIqb?b+HE zgm*g94;PN(9e|r(qsmerUfS~`F;+}pAA@*rRlp(R|3^QsqNBZT7BBzqPv7w1{V&Ey z^Yuee!17Kgkb#Zn*;)HZX`}MYB7j1FfR~ZGSc7{0f3u|P__B zlUXM(w|)ZjpzK}&!@l0k-`)KpxuZjA9~^@m_ZhTgIxD0u1uiIer`qF`0{}AlKZu8M zllG@3lAF5|&dB+c&tP*}X>>&4n;YksT5nycG2Ge=Di}*8`b9@8$8T5u8}TWVY~1b` z?+O`w8>m)9%E^h&xq|mFkXPt3Jo!Fi3g(OG2Wz^yUE?e~PW5+JTL34as*Za9XMv!s zXA>nU^@yK8;v{D>SoYV+QN_mw5UyDC^f1TpZAy=VziLl4Pci`mgKTF6LEDIn@!{f+ zRfPK6tqld>91%liE`ls=EXZZs|2vwWmtFq5qY6JcVd6^VD$7k*qQ*Puvwab;-8Kw- zo)RwtOO@0SWY6}y`X-O!_#NnN!h9M=4hQ&eznKJvy(zt&kHiG0!&@&$GX^Is6cS94H8vnIS)L9e% z8HYz}6b+hp!(jObi|&dm|FdTqX}|sSWVAqhVsaV-cOjxS)I@?-c?|h^?BSZG;<@)K zj^m!vX*-H4X7P{Y=&;DLmvcHPkh#^Gm#2;!0g5-28ODv$i=#$64{s#SX<P!zRmb zc{FBRi^Ko=0D5$a?)YCAuv&MPFq~|Uf%zJo^4klK%%%VRFYwnc;QMc#KqmkH-QgRd zW5_L}mRwbwErr1ny8Iu9&?>BtcS*nVzWZR7E%MM73JkIBn2); z&TYo$8Zcjx`c><4bJU&f7;f>f6eQRD?G7%nZbMtWWr6`wvqMC(KszS;FyC68iopBz zS%g4Aas)U8z{C_J=6+(hZsU=a{S1!gM8^_I;yop(qoWhFA3?~|*@%cN;aTM{Zq0vV z|2VPgR%_Sr$Ug*U(7fUNS{IoWYEbnl&EiKp)IFx+MVk)(e;$>tgyJpsu^4NW}ZISc?d&O%XDw#2gFO& zIN0B&RpwYMoyOJ_)Y(5-*@$|}>p~+ukq4X8U4@>sP+<0R-dl@35H(d8{)KWtVSf3l z-h)pen|nNexyPb7)Btq(q!PI?czC9%M=vIV1TGf4tgdaYX_R$r;>T3J*4-aRRzF%J z5a94h!{e)Sp(ztO+TGgc@?LtUk&FtIsgH8#nW8T^a^l%+_$Rr>_a|u8&eP%Nb7Vzt zcQvYziqJGQ%ke8mqNttOVa8(Fc-^9AyjcYZCu#8 zB(Pf>sqoK3Z&NZUWPbQ8FDZfd@^a@Fl+#Nir18&t!0c3;Hwf$hPc15yT_e~2OKD!X zD@jyO@H(w+h*hAbWgT;@x`*4K?IQ1v;1?!S)}(kiaaXDj9t?s@U%J3a~pGOXUKs(l(^KSZ*h+ZqV^Lsw9O*obHIXuJ&3^T)*Y3 zb!qGF#y|iY-7w)K(R-mez3q5+m;lX%!+GaAEStoqC(%%+V{&rSeVv>l5HS#Kn62m~ zK;hu{FF6?>t?qw$0Vr?;1m?GKY%Hzp?3~xyNqq2mIKY9CNS|BrGkR@We3DOW1nl+) z2l|(%nneNhZn)cKnszu{kBOI*&7seFB6t|a6EhV?80oC9h*)QmRQ^n9GRKP zss!RZmYW0k5sx>gQx6X0$U_O*eC;9@)|dNPd{fv#fc?`{iB<$1kICJAfiEW}GKL6Q30#%M#H*As=xscaGY{!c{$9t zfzR4$3mw;1Gi6a9x0>IR7Q`I55~Y?Bnb=#@&@^1v^|su6knmGl^ZOi`)E0)OTZz;Z zK8dVi608%K^_D6l^)Lc$wP*e>8Ka4X$6WVM-k z^(kex&TeT%9F>sUNM#)=RuN}_S04TXoqwgEEef)Y+ zKOMy7_v&g?W0Qn`!QoL4B+=sk+0!VZH80yw4&25vXQS^2ZjLV9(Vnj1w&azlS7qJ~ zREy8b*1go0>c;1ip1p7Q$|%YwpmvHtY992xEpW-Zm&LVUyog07iBWmHT837qG0DZa zFGGEpyxd~;nSict+2P0f1V5c(3f|*=Qd1RkHqs9tw4|ynRdo6j*&sJ)iClJv@4%PK z$MIz&c1|7By}cRm8v7G!B;U_B$Y^%Q?LnHi*sGCtyBZahA>cGoOUqjEUt|6A?@&9M z+37C~9X>KrZmzs{{x$aFjTueL+dxASLScCY=I(9wIS+Z2_i(G>xC9z9HWYB47|_Ip z{jQa&P6XTI36kqlL!3-q+=rj;tG=paXZJICCWn*;TYJa$(T>%9-?FKm9ava#1jn+D zS2SWEDu&O&>$3GG*+dmOx1~Y;9049TLj?r?jFoQ&{(?a*X&eRoz&kJ!5o|Dj>&t30 z7AkOi9daozuN({k=QK8o$GP5dKIzg72p0(mTvq9-n3^7xfQn|a-c;DyTfS;#?h+xT z9Rj_Me{eRG_XjWxPE<4bh2f#Rx6_xe&>2zxAKuOVkDpHP=_ZjQ7Ieb&9XB`eva+&< zs`b5&*o!p$>!wdgDL^AUPszszTW}^3oJ65cLh0-5OsIrv4pusWv+i6y>okoy9~@av zGPk0CBT_n$v)b4&fX*ilgLdm#r=3Fac%J7YycbBIco&{BS^&oD5>T4Mya#Z!N|BBxl#0vh#uxmG&V1)(X+1Je;)j>lrn9H@#qSX;B6%yF8bk-J8`O* zapTeKK>Sg>1MI9^Z`y{Z#_{4=Hx*UkO(s zqxG_1v)bj3ADW61(@g=+TD^}>u5rt>u>mgmuF!ag3)dwvte(t^GhGMc4W1o~L;J3$=rzt z!R+{#ZiY4#CRToVyHkPw_V6KyF^oYj~WT%ZoX zZw^yk7cXS6+{-_aU)E2BI3==|8Mld^9GOXlhT)yHTy1&X>0TYQW=r;+8(5d{`?X$) z-q{QqfYZJK0RhLmH`LZeMa&oAh!(aSArF4G`O3OFS-ek$9@9(yl+H^wfuY43XDk>6 z4}Hp)a>rvREt9?D`{FNWA>foR=Iym)`HL5Nw@G-)WBC_mn=9|gd>{60{eP6bby!v3 z+BJ+KU=S+OB_bi+4O>OJM5MdByHNy0N<=!gNO#AkyFDc=ft1`SOc_KbtQy@a&9IG{Oz&MPLC)HN0Z2S@M`#(HdAoSss%kQWYx z@z=PhY~f)=h4F%U-Tfho*rUNfRn=G(Gw3QDZV%zYGBBXZ%g>X-%D8{QipHTh{OpvY z|7znJu&aNXoW_95XNHA_BG`pxyXqD0@n`s#kwWag&3Pe8?W~^b zrBLE^ceIY8X=>3c0sL}DZ|(CS=h30j6fJH}nE_T_K5J1$j-#**Heokll}%Q=9pmBv zs~oPt`g&oxFl_326H80Y2HS3(NKd0Lv6eiCMIY)wZ<*`l#wXYNX)_H2b5|E^?CcrR9bp?7ZexyM zIRGAXC(3=wH%voUpn!(_# zN%H&wY3@=Y?>(>jml=s|Y7pRTiq#7QFsKy9oOvJYctyFhW)>V~TdIv4_ff%p9yq;Q z4R4o+uNC96OaJ3>V`}R_w{ds)TA*|&Zgq9_&>V`ioH}3tl4R-(Ng?;dga-sF)r?vz zijyUeD=)iS9uP1jg-fMBV`lz9ptI43L{(r{yt$M@?$ORwj^oEHqNcu;9+flWt|2KC zVT=BlH=<;E9b?Cw7U~CB2I$7dYn!@p2H<*0Mp04WuTyl_D<8P-z$z|HI`4v}j#0&Z z@X$Y%_l}Pju1@DuMT6l(M2lT$JXh$L1_&P$eG`*Fuyxg(BIQn15Vmw1pT~&iKf}6i zg2ZKM_8HF+16NW+9hq1~g%Lcxw~y+&$&Y>5^jmm=sk-tjW8?G#HdfZ(3D*h1@3Qr~ zkGh2%IT;udKGjIFFa3Fn6?AOU&)}~1emDL7-Z|}KKHD|cT_4^LfV+3FFcDr{R>pXI zZ@}n$KQjun8h=GT_& zY(rYfp&46_`^Gv;(KEBP<;^$3wE&!XpTJJ&_}8c?tp$E4pG$;IG-C`A%T4Emer0J% z=iOmom%en5{I`{7v zjWW|k++{Ph)vc!?q-16#y5v@2mvWn^0>#*vzqdZ0cFIbP(Nky*r{x7eWvN{^-7P=f z<{R~)BN{JIsp_xusOxPB!36SMS`Ou#nZ`Bs6OPMiUnXTR+pq6}4}j{@2>AcdC&HFiXUgS~^%$qife zb!Kf{o!NZ7SH!q_1?HRVcM_|g&Gq)iA&k9!ZWe1Vo1qrL;gtlBhG}uh=j1kJD2xq( zNY1^9~H#l_0=-8pe@b9ymsY?S5n@#KU}Jalwb4D>w^iKSMSbZt6jTi@gh zJ20GJgH%y#xg3X92II$vLZ00aLOKoK8zjTgc<=WI>bd@}g(Dpj5FLJr_cpeutd4sq z$JgQw0=2K>)MFD2jLoIq-HQn~TiV(ie7-#)Sr^g5nyV6%_^D=Q_DFhr(+KSeP>$0F zx272~GBK4SLTVBi6d~lOjq4l)6Q#RPQ|FikxhxG$=}L8OhM#|FWkqQ#uMhg2?rqNU z`$I*Ki8I(^v(RCx)x-SZME`xjE#lck}*x88)LUPfL)vqo*f$HQA$}ms4uFo~CZQ@~_nS5z5ho zeEdIDnl41OH)kVl@O|V#c&Fd|WunYP3t>u1Of!7ztL7#D#2qh~66llMHn&e%*|?2| zKH)GZu`n=5JK2Jq=JnkahWNztM?b6IueXI$1>$6kGt92fWfPwA3BGCmg zWT6pWndD(Q!FiWKS~|mvJ;j_#%z~lUtCkf!nXNAB9^5wtNWq~L^%|dHadEwWdRimA ze&|=3Gpoe+6|^vO*d6>|H@O5i=y!9v2nuZq4+d~su8A)nP~X8o8;i#(5D3J;o zlZUhdfjZu^avggosu~(fW=T1vj9OB%vIaYsXruz z3vx^B?d|y>F);XOmucy+c-cN0dz3l%MLb7EKG|n((Hb=d^1Nb zo*aiTB}7kKqi>@x9L2NdNPI?9%Ul+sVlvH@?O;@yv_bm7syiq2;?q>sE|X1|8KREy zO+L0Yvrc_8k94#$ebqf@?6g;86jN1#8s_oAr_2tFZ1GAgp7NClt+H!>x>L%S9UZja zQd3UMb2Yuv;RkMCV<=NKLWd{>mxorw4eYl0fx8Vny&$}OG3CVI5$O{NR0zdXnRi9^ z%@bjZk!5Ivk5}2k?;dZszc))_(XQ<+UK-3(-xmV>(ri43H2#jVjKR^FtbRUaHRFs7 z8Bz$kZ~nA*uF{oelGS)xf7GmVjss)ai(KvdvFZ$9EcQ*ZeO} z*DRGC)!v;HGG}#m_O$jkJ`oaav*vr3@{9pO!hxz7_sSgk&Nyq4yRSdY%|GUBtSk>?&_HGx5D<0YFPHuC?BO)m+1mRY zeTP)i&xom5eeJ`NK7}QLTgg5`*(tuC#Djh5Oz?Q#g?({f4ZksYI^3o9yxTJa$%Z;^ zL!#f%`QUn`DX*pi*2-Qu|5Ai~&BH^p@=ETCt zoBPOo^BsXU>hs&yGD*id_-hEmQT(6>`V-?rpX%K*I2o z2J&0hAw_%cD<@<%*@v2ChGr2gYuUplztsyVPx`gbc(9CT1AZQYHek>P5wW2kcG7uU zg0_ywO=De=p$V1vFLLL7#i!*~$u~-zcid~YTj%OUw03DGct_eMXK^U5eIK)s%oME? zyl&>#+guKcOkk0-uD`4`Pf~UoQ-W;j%Y>7Kx7X;QIFW7+W=)o2lCnScAYk#lf&y(5 zhkChmr0~e-%&$*0=b$2E-?*~ltv>(#dr|i7rY#@|X{PvQ)E570oB2fpJ%HyhBh-e^ zhhY}-#=VH1Q&CMpFeqD-l{%RQIqMaNCs{cAd*&hKURoQxV^o*tJ0R?785t<3RnMH7 zzcbO=(%_suH7~(ou@M+`B;PP^P?$Tlck0KYbWG_SKvAR#r#d+y(_TBFpVQW^ok?ZD zZLcp>3V;U~Gg?A0d~r_X7ro`ogVv4x<(5?SIl+{Ooil$XXN--)S!wh0P>jZq=HPI} zDdYjEY-aUBEJ=JsY}6Z$n)Yy?&@PGROtkqDPjOFKKWtrE9$OgT$Wa~~5W@q!#2y4@ zmmKSt}ztHc^-V zusACxXY0&Nh`)|-JcL4a(c8T1kXFB)fbB}GF`?Z1|Kac^+m2Y*=SS;W2&vxp!n$<#TS#Os*cVt zbSe@gDH@VdWFEvdM#oyc(N@=B)ba2T0MnwqX-?aCavs3xH)Dx{4~Lgw`9uJo^U%iR zQ%o$Z^^yI-%VI_^BzX0@RtB<>V#tZ9v8GaOt=rJS0`mB1G+-c`QNLxqvvn6NT03|z zpSqC7nfgUV%0{Up01w&SmvQVLm^3OM!R)PySpsjk(FlJ9 zZ8a2N>*u&Wqi}Va829Mx4CA5K0q20reev{nY;NXvSRB_|OLiNSGc5*d&HOhtC%wno zVqY)#`H`HR10qp#HCBSz%mlPQiJ#Ts#4~?NNtBwyqf|_+P8GlhQSM^9qhA{j z9JJLIo(byb^n;H&mhX<1^;b_-nsRhVO$S<3CvBLMo2##u)W_sHSl>-K{N3I>lUNym z{rnPE0Til_*u2;m7Pix3d=G{ecC(S?(j*2QAwN9|$M}ed|qu zW=jT>PaNdM>1UXm^r};DPtVfyZcM4esqi~RMLS2j%W`HWAu_t!WqQg5W+83&;`R}> zaf6(r(isaYAw-i9APUM>yLQF!r{-^YHZGj!+STs;#x zxix>`(yxWo$qED7-4N0Mle+5X&u5HUGIU0ES62zVUm@K}&1JJn3pz%A{d)M|LFxU< z+}b}|242*=V^baFvcjX|;}Hz;YT^f4eOR&wgN6ml{zGtiJ?KVcLV`-_X+Fr14NdJ( z-JNW9t0zvud#643QkdK>%X3rJn(rysv%affY?~@=9FSp%XE^#L%$C#XH{5UH-TY~# zmG5md?Xl#e(_IZ~?{S+}+mkJ{YNu_@ZGi!w`y3pKB_&c&2v~ubL?cj2KSZhfHaY@_ zRIZXhU>Lxg{4CO`g5>$+k#P}fp2#zvmnm<7h+Xp9+bg$r7YLABTlWnJ1&p7(Ju9k} zgnH(9HxvX4C`U(c&6LNCyPq<;^{c!}%5{*@*_n%qsd34-t?Awr0S~FQGF+5G0mwP2 z9df^_iW~S@dF>Y8NA#?5Dvha{3h?yy%5?SSO*p4?X^uKUVz_)zfO4nGjX`;dPv_lq zd44JP1zq|($)m1e$E{!5nFb0G+jV&rD$et{vPn)bn`}(0%Ha~Yp3}BZ&c%iFXJWFM zv#GP9r7>panVL_@uc5b%y!~Um3e3qiDwc++N>x3qaIF%dO!#Y6aW#8iTzJ|k;=f3x z|IGKWIKo3{O4ju|?4+zQ90jMvprJXk`gnRx>`yZTkK0el^}=Z9=Nl_LDh1ks0JIM! z;R0yQ8H>($V=#7#AeI9VN_XF-hwyMtK+yH<>t(GVG0#ay->AQeyQ9Hnn)o4Y9v1?E z(1f`stxoy>EG#S>u6J;7(%YT8svWH4DPZnTyWW0BkBSN=DsfYpJOo&?CXZj#eo6I1 z1@--ijIUg>2|_H=IchwxI-aDuyJNO5O89!CYvj7Wb#@jD;FmYZcx&48ITEhd zx%S;853E)L0JtXLyrT?xusL8i{+vPOoEo7oM<-aZ33kHxsnPub2=BXs`sa;<6p zn96W|(EJYk8TNb4bfDR$ehHqhijH$x*?ye;ZM!b<&)}OSS?m<3lYWfnl2q)>?1VS20+1MeHT5-JMcN z@C59`wT0tnsi)zoUE!2xEI9B`D?iCz8Tx2wT^*S(Bi-A$^TQJjy0;G}a)fTZ(i4sL znyoh=NYZvCimApbaynSfkJ#PcnQpoJ0Bg`qE#BVIQFrl-jZwYi3B@(a?T^i>btn5n zP4kS2;^8schDR7nrbEx}TxT@Z4%)B+qa*&^8l!oizN9MGOJX#Ag$^wFd1FFC!Y6Lr zO%rUXu>t}gDOB>FRXJe@dL<09FY`T9DabG3S6PjX7L9Z^8~XbOO#1Wg-iDgJI!*R6 zJCPx&xGV|XjbUt@&aGEoF=fY}>J4=f!xl;Z==-Bcf2lxHIKEd&>%;*zPtbxscklk!7zu=-LK1e zPrjAz>Vf~4!f@|@fh^HdK_VwGT#8sZJKAQlC)7xMd-L*qWaM}Ek(?H3= z!xO&AqhC}}!dUU$3gL$VJb(2~O?`t)IcR9wm6W5ofr=EkGIsR$lgXZRTfosWVi+ux?r)vz5OFzAHIwYKa%|cN`dEqhUJFDw8m#j zWJ#h-zI)I5jqH8vWjPmsiTuPrJUnh{(NfNT{Q7RzA{yEs5T?9d=fG#^9-za2P6Qsv ze-M6vs{DaaK0SOqGxBcooG-7gBB(6!$ee)#*l0%VhAniDZc+pFqz6R|!nXDt?uIf2qKJ*yGLa06b?3Pu2%Am>QxHPgVia13V(_vfbz$>|s7V-6r}W&W%i<=a__N?%XB z&O8}_l>fbh!;!oHfhMv&vD+y!S`a(Wji_Vdtj7{rg1+!KhvjdYjg>90JjUwn{l&5* z*>jqT?U3f+Vjua3v+=^G~h zJ?k8adUoi{$6EX0?6>=@EU4*HYFl-0$Ccll&JHll&2}>}$j7N?1(#*a~f zRcup9v?|k4qg^phMX=zcs}}=GHlNuU{7V*&BejZkf&TBm@CBWAA&|!o+^IAgOT5n3sqx^6@cqGBuYpKyKfg2sx!&D- zo1gL{X#sM;3p+nJ%yHT{6>!<^3Q;>8@tF?_?&yH{3SFF7aP7p8WeUcBSDH&4PZRD4 z6JMHT_2mycHXYeg14VIAT`)mCa=;eQ(~`#3BDbFj2n4bjK-vfUF#c?9Ir6%>CyAeh z)0H49Dir1=OrTlf$}fmF01J}im&FzE$q1Br4&YXz4)~DOutlQx)zdIzY<~>Oh@=K;mAIY} zi16z1wg~TGJ7q7{eHO9=JpNFFph20iFS_ozLA*jaeubefgMLkY8gAt(I7dg_VXv%O zrgTZhGFnfG`(wqX_Du_9f*M`A--M7o=wUzg(j05Mr}$X|hJHN>)f=sSu^*Ak$E8b1 z(*V=QeUhc5!?Sm2)V*v6SaHIkv2Bf%R4`k_UC<~8)p_w=kGR*_;Pai#=$XJr9QD87 zj!;;t)_klc-$_fGp<-rEk%(90$gtaTQvf2+M z#<@6U-m@npku;uJO7ox>SYp~F2=MlZgaql@Y>_9$xj?T70z_^=Ro{qe1tb$CcJp-Ye?)ek8y@Y)a^U8R!$5kszOS$S0+(NGS?x1A){}m*R+pI z3!0@V*1;JeA(-n!uvEk@9|zUmKF(9bvRU%(6QnEBwd&E(27n^SUP-9Z0t;#JToXSq z*A`cmHe(?s=!K_lN=e>bExPm50hmMg z*Io1@k0c0^x1A!;IRz9BcESLf?oy<3O_8RJ_PQ>w#s;9*sJn0s&rW=WG=Y&B`zlvH zXvF{c0L1|)N#1l3&`n&e&t#(l^RaQWYts@f010#+K$Y2dg9k=lHkSj_tblZ z7|;j+-&&L~m1wNPfA%k~4KvG0u9_h4~EYB1bRq0x&lUj|&S;`jcou;4S(! zwlOXv69VS_GC=&)SB}+pRbr32GA+-I1iW-M+mgL7_H11k4u!M6fkx&2_G!-~%!493 zJDZpP3Gz!)v^L_~#PKF19eRCv*iz|1(ezlDuXQ@+NnNHx|&bjXcJ)9a72kxG4-x6Z3 zy?C-ruy!&>B3lt098(CWmO!<17Ifrbbn*7jFv(NndKs`E<y1b&dxs%56R8|JK@MHpJ3W$Ytf%Msca`a zrA55dszYv$h!1nX-APiSswY6U;k%Fz`6z%eY>}5R(yIGP>Fr|YSvcxw7}+dAsCRAV z@xvQJJ(_20X6kVR{b77Jaqk1_+z34sVk5@m%}F7ZT3D#Fj`q0BOhxJJB5}Mz7=&ez z(<`Z6aZWNLNedHaW@v{7qP9i;rY}z4>8S@LGuPJ!>t928I5??uf7s`u#u3Hd{u-x( zFc-a%Q3-q04{eX~FfmHUDbzALT%-SZtGlrhs)mR1~fS+1T41Tg-@!;^m48TcH(nhwIv{Gxd~BRir6V;JC{ zThl&Y0nD(sJ|d~UTI_s9LlZ{P0tz|b=7b7bv-7=C$g~sS#Q;mA9NX)Qi+?ZLm{2+MrQfI#Ti%pQyNmDYjQx zOfiS9nh_bTmq4$N>`x17XX-4XM!|Oe(`}drRFXyBE%+diYp**ixloudB0>rKlj~kq z79?>Yo8AC7z3sh`tl!G2H~WLH z@+|Mf+6Qc-rMDxG0C@+ED}qkDEWV|WN{g}M zQ#S*DTfb0e$?!(-ZE{=E2n(mYj)7Ofs`o%H9s=czO7Bs&y#geBo3;{wJ07b&^X*c+ zmbSOQKkt1=4XllfyE~T74b|@e=An6F_=gX-QpH1mXLgRtoVgO43?$t&j2;j$+buLx zJE@WJml>=U2mz&Q(5Fu|>FE4Mp*0!XL#_f$sf=1DrOZmsHAR(`1|!op`v8|+yMRBP z^Ri)_b+1)ka2{n#BzK`!ekO}jO zyuZSgbFM-6{{C$rYx$Rkgb*F=Oeb=XrO99txBM)X2A#H-;Mci@PqgsQLhI|gIy-gS zdtp)LH4&+(b4wi$YYblit`RTc6S2#w4kIo#UYyk!?d4{SrHT1xpXL+L9dhP%?QtV> zECt2tuJG?axl>^-|MIPB1oel?-3if50&VZIJNdLWp`PI`^WREOe2#>{CV6Snm;i)9`H+UKi$WMKLAXxp zOU8|=h4pDn09PHeOvlG_2z)GH5tMJZ)v{+z*!W;pyRCQRiL|tI{=vENf}nft(X&(t zp?D-dCFtV8L%sbZJvn#4roDWP%emKhWy@A7?4m5;A+~DX-&^{k zbfp!7RvINs@N7=Y)#h&*7B`_@$@)jz~&doMft;JQ0v%S5IVp~)hlZrz5=`2v;A3ElF zxV2%zXp))b_}9r4=wqlI0Z4U@jr8!MG`@Bm9}(t-GTO1Z6xa%b@~c%Sq@#ZXpO< zfA_yQoKZOr-sZ%UcKd`=lloilYx6$#%9QBu>gwu+6_?_E5ogf9TK_fZAd0jmdk>>l zb^z#xHx;@CgY@y^G9M|QJ>O5C-hk!^@X>5vX2DQuL$Gp%R#mxz6F=hbY+%p~?91~h`OUu0AbV)fmO0aE?fvX4z zL=j6w^Xycc$IuulnLsvk`ll=jNO)}0cN6@0eGoW;o1^(}Ys}wK7#kbeomxLT7TWAm z@O{Y7>eDo2KO92L_CqR+`qNqNVT?g?!Ea-QvrBaq7htN=NNH1QcgSL?Nusv3CF>_dRp37wUstY(@X=k&ecp;FhTS{0Fj04NrmwaPYk5Sswo*@4rj}$PHH;plEgy$NoidKKtCc z_b+$L>$^(M1OMNNF#ljc@0qLko;ZEd0(O4;t>N^E`yYp;V;#>F#W+cNw|=ctXDFon zynxrwbbU54jl8wYaL7yQgDK)~EWMO04Qq_grG~IH&5eXb^ev;S{w3rjZ~TTItT1?% z8k(hlOwe6@Nj$Qj0<^j588Fvf_KwG?YRAo|j=A%qo9sgl6!z;#C6t0Rt<}Z4p4uZi zD25vmd-dOk6U=Xty;;SF#~FZ91X_2c9=p4}2@Sq>6m{~OUv=}@v7M|$9p|r%GGGZx zXO8P)&R7iZ&mYD-eXaQsC6=J>>oqy>u4R_)z6`mKN8}b4OBDMvGi!oLoc_Jxiiry1 z)sMelba(cJz2ok-5)|ka4v~Sl!1a@nBJ7G#TDs>Q^r7~*kwi|2#`H~YA#j|K}OmQkASOt!_W8hM$vyy@>9V2 zk_d}qcOwPa;oj>?mHm^0#T9a`IMx%de0EMAP6NFQJ`~R)pU0;T@1$jT7#xYg@Wzqi zH61DA+Oa#J{D67kCh@RSazsE`3Jo-WoAQrhbOpkje#abv617inXfnDXx;OktfNx!mP}Xg0lI z?jtz*7xX}m7oZ?iHP-qz5sc;segRqP3ZWEGwV z35JJ9oYCJ`_TI(TK+n2gLj>b7Qd7gTw(fJRaNORW^N@67_HyZytoxs9O9EthypD%o5!b2nWXKW6PxZ;dFf_OH`s-D- z3}v}jys^TPDNm0Dejmx9R?UO zH?5(2L@vQ-{MK;h+Qr$>{lJWpqs#jMMYlbC#VBBKs_=uJE_yzg`(X2-a*cg`?B5I1w;8dbnEkiEXJcGSM@qUF5jpKYEV>dG&HQUhO=GJ zvbkUL9#dOJ2jC}qdO?`s{h|kve_>+)Gi(2EP`(-?N>8(l5oKjC#e(u`=(z~G8zk^q^CYj>YcQP~V9kTZTY zxTmnvuM?I%#leFjh6v!yThg%LZ<`yz_E4;0AC4u7jhd!W`{&He&l}V()qlvkcPal( zO$YGnKPdL=k2hw+HxuOR?~HdPhYmL*X~wrcs%#8AvZ&_CT7cJwv?s*vRG8W4V11Ye ze-bTEi$X+Q-8w2N0{Jv1`FbiFS!voOI(hfcb$`SNqDO)QSTul6985uHD~TJj-NV!Ms|{q5e7j#@!2N zo3@4&oi0C9l_J!O5q2K&P(-QB<3ABX*T+13*17Mnn#tNTM`swgrl zDLgLwa_nYCe4g6Qu3Cj}*HJ9)YZ>G<6i6r8Y&@zj5CGvaI1Tjq^ndq9aH@_y&h;nf zWj<+gj-Vwev_X6NKbdTdo3ma9`1Vdv7;+E?Csid})dI?~u&f?S&jtpgmaW1OeXT}vhJ z&(AfJg#EORiA#njgF!YOS)^CGgEKblVD`(EE4ZQV{qivq^_0zYZ8yBbVuVt#7AQ@1 z#K<~~K0z9y3_2o5cQZ5Ki%ShsS2hRVm(rtQdc(4>o4NHi%P?sxp|_-4;kc{fRA(mdrY>EXaDAOpe;O9B$nifHb3|Oz4y7l$Iv0!{fPljs5~$)Bwt&P zBJ-OHC~xA1id;DSddk~=jezeifMO9ufZpOn(dn{xrq$S5qagj>J z!Mu@G#@nHdhVHG&lCF`_r`%isQMd^aK*kd^^w zHyD$wKAvb=$1Ma+>n||atp^Q^mb(}fS4~$ViI00O?Anw9upy1PLYo|3=F(f+Awi*i zKg@>tH#p_OG8ok#e#y+FhQZL@czS=U6eCU3ihY2MO~aoQ=(214Ejn89dDZ9Op63F& zFR#>FLljBBRQCjwCuMEw$w^6J+u6m59t@`Sr$Tsp+xvLpICAlBF>Ud>2uHmV@c~DL z$~0p87UPCJh8++)m{k50br4W~qn$qQTUT{G7WMln#-zX>O7+Kt`0kdW(CDyRD0c*e z{H_m7&YvL|gl#y=C90W|$?iGRd%v{FGLguv_K#6hBUbyaq5N6;A&SGa*hA3oG5Izy z*bUn&Ke0}sQ<6eyo_Q@*w+e3$B_uvOSI(Jrwi0=zrW$Q!DpBlVeeiDQ5hyJ~;eD;g zIzkWp@?PLd%v6V-MOioz&!7MZgIqC#VzsG7kdX@|k*X_| zkL_0>WZ&3cTP=qt`J)h!Xz2+qkE_w%Am%JDsg#GdNQq!A}L0-Hy&G z*Fq-744#)NkG$CZR|Q~77lNU|J*`viu{5C@x)v58CPLD;p|5iJ=j#&M4xk0ByTK0; zrwoaZu`y8=EosRvy}@t6o_s8ygs%^7>rQ=tSz%cxy4-E?`0v@}9}}jUyF0&_Bx&H+ zaOTsGROYC-{)&(IF}h0rk6t9nDr$`7s#onj8d__irpRKn&S>Fk#thXVoGB^hRDBmA zgIFfi@D57fwb0(0i~&a;@gcFSq$pSzsFJaq8STx~-SllB&nYP*$yw08$1$_WcU8{o z=&$bZzEgCtb)>7Er(+U24eISHHi#dr8@D;*(Q0XMA2;dz5x{9-o~s1t({iHe!1Y&3 z$8=#N?#cc8@81;E_Gd7KckuC12gWAg7!CY!J-cUZj{7f6(T7>$?GO528xDZB6B`%* z?fZAlay&*l!CV7e(oYe7bqJ%H6Dzj;UsrJtOiVcVA-nzI5Urahu$;F`AgKP{@G&8% zSeOVse*RfBwD5N0gla>tqeviy?sV96Ek_Ak@-d@?L?<}ZC_!mx6oGJGhST@R3|HBa z$&vZ^cR~vf6_=IN{jK$X`CP_AXy4Qoy>xRzmTvPIeNXq~B-20>z=vHj*VZwRlfz3) zp4`NqJ3Qt7@6w<1uXBGsRN4kcQOD_&*8|ff2-sgN4j(gpK-9`ZRevYRMxvsxe z$6H7lq^X>Ds#`4T>ZJNSwh@FhVK#uc(dXMtvQO*v?%+1sp^x#Se{lhfjnNH_l|9ug z#4!}D=jgBVK7&0>1f5H_^9OpKwuc`G_JiAP)sEhCTB-azHs-+}K9FznwC>6HY6Sr| z(di#=ePd$M5kp*mw>G(5Gqbh*)=1JXkmk{{O;1-{b{>jW_NuN1hVL=|on}r!%IA8z zaZ!a(@u3=GGc9+RXB)a<-yet$9eJ)$kL68E09Dk=?d<=*(4AEKBFwn}Ofc!|4-HE@ z#&VkvGZ&Yfp2dkM1Zmbq_XjS8*&dFGpyODodNO+E1D|(60_HUPb$jB~fq9k?fU*(_ z<=lgaY8y0nbntrYkOu{+X;yvh_J0sg;AiW=Ww1NvYCYfeGW&gmcKeEHz3|x9G_*0j z$4s<)=zSn|!PbmZVELv2m_&?hztms|{=lD)C8c|)1!{gs=%$+`ZEwpG9y=S<$GQa6 z+w3gNp8i}LGZ2s9v@S5N9cqc(j|?kuoHQz!cWvy`z?p;zgnhgnwMFzt#n>b(8yy=b z6BAlzdZI|;T~+i^bm7pAlACYxkDdAePN8}mUYbyRia{FeND z6d~c7p0WC+eT9n{_v;&u&~aP$Q0tx^($Oo0`3iljEw_oj+Hp1@D*chNa=6XOeEkx4 zlJ*3`^c=oY%XNUVTR$6H^wuPQL(t3oDMK_Q(BJ>o`nnqk;(K!}29X_R^=gDt;V~O* z2)`CivtoRZD7dE1lRw5SHT)B9KC4UGZJB=0iGZ#`sw1qFT{4QbM2rIgRi<4FP$x%X z{FUBYzak$WHYnmQX>FT^&YRz`qlq*iE-4BXIh$CB0r|DWDs%b`}T(eOsnP(rsZLs7s6P~UNR!7{il>tJomt}49PEMt6)C9 z0=)@`%T`JE)AfP&@`z9V0q?6AQAsQqEu~nb=X)`O(Un}TMPI+BsX1wN*wqFS#bo2= zDtseh>Gtj+AQnqK=xCM*55SVLiAherU5GFWuSv)y0_%r3N@=XyI=-lqr)jSKV4FF& zUojKf*gArO;eorj#BpxiC7Qgutjq{103Hsy((AvKRdD9Bs{PXark3V>IM%E5#-#c9T}5@Z9VZps-R<{9g5= z@qg&GQe+|(ckEa<=!%Q&z>)R~tDB;xo4QBWC1tkC&fFI`$foj5p26|)mY!1b?dI34 zoQ!Uvr~pUYslxysFlj3seNYLtzZd@;pWDj86T_mjkU%ZXkhnfKpIYV4gwDfb0{EtC zmHR5*+FD1qqOLC*Yyb?0X+n#^$jCTxuxZ11Vdj+WR-ktMV3X&nJ1Eiws`{Txc4h~A zqXvNck`0FU?H%tMpU%z4@%Y?Y5_X4Y~wu%PgXq> zgC_F|@agNmjqUFr3t-Z40ivP6h!nkuOCYkMMvSNlZn_B$mi&1`K$oi$!NK#tDwN-2 z+>0N&o{roO4S7Hzk@YTnq!3mE%km3t_D#hG4dIWMAD=-($_EAuJkuLC+{zGQ0m@H(UPne+q|(&e#CI|B;FbE&7p< z$Y#j!Fa^6q|7}390)kZ2vDSu>GLlNmRk2!o#1& z^!Dy8e<|*UC#%C|M1(I}kGehuR_4BK=jK-Kg$jjAE{QF{;mtGGOwYKO>Qt-2nMei$ z^&*&C^B5{BCdZq=8|$*fia@B^iP?F5GLljHyVcn=MseBItE*k*EfGn@p5Z@Ja0LI~ zAWPZTnHj-$ImCDJS$;qfusGhVIVCr2fm>k`|B8XjlHi^94lZS$Ep#aH@=fLHNK1S8 zN~JfqwPjDl#n38@4a5Jz^GB*w<5N4neTJQ%p`5o72!gfum9w1p#$Zh$`ust3bb|57 z=Uc=S(svaUR#`gU%iBgmbsA&$72VF$nKyi?gpth9KI9h_opq_{U&?L54!hp;l{Os& zZrhc!4XN+x*Qe4^LoctAUDY1XbYe?_$N2hN>?4VS!^Z^r^PrR7$p=-{{02s5l7OLR zIa?9@@isMiFOf+P+{gC&r4KD#BzVJi|$Y#j@Yw-QxyA z%zw`p>ZZQ2?V6xEvP|Uk(9lQk!iF!w!I6k^0lRyk0b$U-RrtlT!fSf079llGtoE<> z2hRceTfhpH^w~qf#l#a3}wy zmSpr7dZjd{5n8|f_ZC1h`A?{zZ2Id^cSCRd4|no!7@aOxOLWttlgunzE zUX7Sd2(dv*>J!oKl8dWb1$lQ9{n46wcimjcPqwyt1$Hm`6WvyO^$V2O z)uYVp+6-kVXZ-N;>BGBsZhv_B_Rw`9GGTN4TKBwrbfy1jZ;;S>rZ%*+6qSaC{aMAf z-fX)Sv0453F3y{dL~x=lJ_iSf^A2BQybgDC>}m#6%|wY8&MLEJ_EE{r<^EjtRiDVx zV4Lr&8yp{A-X03V75w)?DsZg-bPpPyB6|3+E6#Zrk#P3HrzWc1sN}nPYPO?qol~;zltaEkJw1$mH=QxHOf2g6=Nw2B1-Nsw`Vvatl)i zd8^KPG^7 z&U)CrAr}MO&sx@ykku4a_}XP}=xtbN_o zm)a4&K9>3iGmqnq=1#F$Sk@i*@!_hxpO-apr=!2K7z5QFAfv!HI@j1F`r*%!s^|*qb$A9L< z&0QC&0w;o_dB45=liRvSv@LcHPA^>G?(Xiq6VvQ#I&}&;-!-eUV8fG5EwmF9PT#>5 zO&`9MU#gLgDU66a|C}#y!tQYL+PGi4^V++iX}4is7x~kOpXonCGUA!D{DZ*D%PW-N z^48)K?qszAOI>w?>{7sS@KyVCPg08B^-z`b@81d_j@vk4kD96dYm$9h9`@9gZoG1Jf>VaOlo&Mq*zN(u_Ql3j8L{*99I zEy>Zs%GTKTO3FiQr}2tl6bD|ryL z$GT-HlXbS*o!G(xMNZCC+JPaq!$=XMDBU z`wtK-*RdLGk|(zoj;Ywz`z(yXY5@QClyj?I_Z@DCP+5mZslCo8XaeMxT$CS+k9IIJ2+wnV-HTcxjD9@kA ze1r523{dk^qo`uXpvC2?GDdwQ6DlBhs_TGtIw4o zYC?9~=Lt-P#(#S~3y&2ePzT)K#06}6UCNP8H5q2KOJ%PK8 zS!K%V?(S~0(eJ2}yjMos8}c7Rl)}?_a&LVG=8=Fd)Zr!~ndnk6jW33KVK3LdELq4A zQ(^D)xBWp&lkwwcx~}gTUBOF`1D~M$&<|n)uUK9o*Rt+A?Vr2)?Q+mI-5k>)!pQ3( zP&?VDiGTFIoN{GaNrIo5QGWSMU%YCs>wzcM&lK-qhcl|8u9$$yD$kFO^AxABooYAC zE0k!D)hUD*+_Z^MF^P|_exP0fimJoW%i($dHqtsy#3Whc*5LB<7d`C}NaIB*TcZ_P zpPZe0AA*1ItJBH4c!&}iUP;|(Y1RvfK~oq}r${lv*-W+lpZ-+U)Y@pDGcU;0PY%H8 z8uNjzkavw*E-}lUcjv4e9@u;d2qV@wOXIMci;Bs4XNa24TnLu<-aX4`@?*h{;Kz@L zt+Uf{gRpBQnK|fI<`1K8o5{#$qfg%Z)l|^CfC#xM&)$m(*F=v@Q)tf^`nSK&<# z8@wcXg0S5Fx zEh22MOV>S&zHIOwTV7ZxJc<^AcLC7vxVwQ_(nb#pGCA86)`2O6d3-_H=qqN6_#dB}~OBWH0F z_!zGr6zOxifSj7?S$qq~H8{OR6(9QjQLv_}gk@T>xU6SIl?JW0tbo|t1k`j4DZ~Jy6Z7enK&^;6jk4CBQs%KaAyn6KYUvNxg5IT0L*jeBrbQZud;HYQE9hw189$U z?NMpcO|7zmXP$m`TyRC5ZwSEaq;&V5BiiEd9>1GSIp)MNQ8c&ZEM0a> z5*=L&neiPGQyioVCr8fdM%L8XS8Dn?pzShMFwjcnYIX$3c5PpcFnqw5=eqlr)<|Yi z+aPJH;t)5}Gde21>-w4vn=BjBz%n=Gh<{MLEx9^xvol*smMACDbRKlTL!aMq2;g)BQ!Kw*?*+5FJ;oFPvhHkWtM>*Q`RnWJ zpu#A%8t@5g-b!mgc|ZFrL!%b>$v%Iwb>;Z#1O5znQ;m&e`ru8eJTt7QDl=)1ew)JY z9UIe03CgjqAad`#5(b%XcWP$B@j&|T5d~yqG+#qxU(`wQ$5A+HbH!~5*Dk9!fJY1W zt%=9Y&`PJ%_RzgH9@v`5CJ$D9bogyC_~%x~A|>kZYYlq*4>h`y$~>`5~mr-K_Cf#U*}HF+bi;|#n*t7 zAD2JQ;yJE1f2T=~ko548N|gvUo=@4Anlg(Q-0YdEol2FvlkFf$7XA7K90%-$&BK1- z2U7Chf4;@#U%CwG!B6%c{0X&;nFQq$P;tI~#UoWZyE|PD%uWyoYt7nSMoJZCUoR-h0>MA8*3B-nXPLFl5Z{5Jp3Xsn|3v z82k_){C@0{ni-blU^V1Aik@x&SqBL0Pm>^Sgh;5YA>YQ~+6P_P&xosOrwSK)a60FyM>ybI+9)bMUxlu7r#y2>ZpHwq;X3+6J0 zW+-f_HE3gd+vNRwsdHQX3y&e!>y^InS@<#Y>3DL2Z{0V*(09(q+gczbo&byYwG77e ztN3(hJ`U?v&=1%>fL=qZ&IkMU)*02KVX?cf9je(iFkk^BfYu8&l9YdHZzfV@it`h4 zf;`SD`MQn;6!+$+sb4<3@@-V7T62#eZBIF!sYhjDDc6+Yd)X4Z!#Hw z^1jYJ3AF;YKjIS4>Y7FsCT2;Xk*`f-OF0T_mo_&ISbi}di7%-iT*9EyJr^aJ8`EXv zw@<`2ZfsoxG03DcDST)i<$A~B%zIYxtKPDQq}H7coj0VB@HvoZ;#*vAU#d$9gMG{K z#PVke2O|(RlKWf-clk?7$;!6&kK=thQK_W%o%hYt6+SEVZlufII|pSstog~GL0>GD zJ7IXt5^j&4q9PWGr!MkMKKDva?{Z5!eYw?f0j03p#lx%2a5gt3L!yVHBNw|6=tNhq zSKBzLCum;mL|}<&!7`9{pwDSft5-AA_Dhh}(W!;MonU_L+m9b3%$&l>{_1#$5<#uU z&$S36O_##L=!c?Dqsq-?p|4X*?`UiVG1uYJAPbkxe8(tldVZ58UbYS1^U%V=Fu2G2 zy8EY1(?dVKQS8hKwHX=@b65N3$2fKhb*s@I_=8|(YY+fsM?c6Jn_6iro)Mp&4im51 z`UFKm`qgBF=>dmt;7;FMhCMjJI8nwesVkzfYdEo*kQf;J-BDhXBP)NLZ$y8PcXI#5 z%e?s{j6XnTv0E#DUFt&Fr{ipNfP$n|Zxp_2R-2qr)=)JJr-FTyX z|Ag_C1ljLS3otF`1KSew{aY!CaDqVK_=SoZY(3W~OMjQU<`8B2Azm%7T_ zJ}sgM(m@VWV>lDk^ipE356 zLF5vIi%>B!MM*rlshIywLCKCy*}BX>MLow--OR+0FMrelJZnjS(g*?6MZZ zP&_=cKxd)_h6NIR=9c1KJ8L zn3S1zEq=D1t&jU`+`4(BVZrD6fjM^%eAT%i;?1ts$uokoQ{k!SItd7@%l(ZBT4ETp z-cPY-hI931{4vzwX4jL-Z<;^Hfi01*Vw04fKJd9qtbV-uCCCR7n(7~qZ*m&FlRCAa z-G#P^OK50}FWi^HoJ`fAPa*>M#&_tNsi}FXryb7arOX!GBY%X1k0>mlhJ;xW&|-lf zm$8;kl8Jif^L)N|qRTWPM>2@aTW{Z{1eN$7PN1`pl95ryS{!C)t9BJ8lSOv~csJ*; zPn{_)7FqUC)j5B;bxU}i#-Ria0)oKsXZ1?mir{Kfij7=QG%6v7Y&JHaXf2n=Ok`M*B{tTtcWJ~_^YSe8JcxxWzu0Sj80*x zsUNrI{Dm7+LP|o|JIHLWYur$4y1U(kEL`V(e!{K2lT1nZ9D-e%W^Tl>!s^6LuVI<)q^FIll?0u zU`z`RQWAMhF8x|B+;qhhnasckpq|NfcHf<;GSj#a;If-ifUe8a<81uxS(xZdzKKG) zyn%=Ly(l8FIZ&^=FLJr6ryBNOxb6$`(;EB(3lU1TuOUzP(^~Gd-V5>&tH5>t1MJg$ zaLf${hsXkS$AUSIT)@i@V2y(f+rsbfBqE1ms$KN7c3+$Sll^FZ`$kfv88o z<67&+y}AA)G8VD)1lgl9WiGDq>uLv75LPlUXH>s2y+*z>Uze^Zfn(@b?X^kK;Ug3< zlrFEtY;%AAF7>jKkdl@si1j#hOmlT`%bqt|SN^zlsD_RkFUk5|}` zu}FgrZ(f8oYR;4?mzU($)aWtlx)Xp60~;`W_a@5a15<*Dt*wFOEd#_mjD$7R8wK+@ z!EEg;SmFDs12@f0@PDFK`3|n-V}%7x>_a6@mR`2cNR;;K!S3$r>T2tl&Kq6>=Iu)W z4l_vGW5)Paj+(bO-w=_QNDJT!+@v8HSy^f(CNkj7xwE@#P%BXT2BLPfTFg)5c+65+ z)jBx*vwBWdrCZ+nQ&dgLH0IeFU;bMl3Q!Pz?jaF4>hh6>=XmX{P<)E0fezp6w7s}n zA6d)OZ#@4RAhRwjOXyFFUbw%%cR&Z4%C)x4 zRZm|Z@F)?^HL+AKqH-OBcryZEZwJ*s0(sMm;pJ0-fwAo>dgkh9e*t#_=(a=*`V6rz z2YJDD2=K8U(6%Ku9q39#kQfx1*}U~n#vSX@#1bcaw;`FC3xh#x4K<8kVonytgV!si z`Vos|AOisLmw=qNH;c=&I!si$pr*Z9v%@NABvk(L-7rtsxk-8Zzy!*~hxPo^t;>?L zoVYI!HE$mbA%0#Q1CZ%Gk%gL*nduZg5ivHXsjr`sum6YY>Ek@^ zcSPd6iuemJk8ZLT#}>!u7!W2Vo@@xdxv6@$_F)M55qNXzk8OXND6k(xYrqsj z8q8cX!+)P#aTaKuvPYprfHQnhDkd8K$%g;@aBi6V(r1jBgcncD$ER`JGAmXcCr;+= zXvPX~cv99fR5H;nwWdNseqv&NQLmY202Fp@DWO#PUt$_ucV=71tnTvJ+(9F*?|HHY zs~~~`If+ppGco5di4ajM z@CXD3B7eZe+g2%9FjFnR&7smm*YmFl%Cm9&QLTUBf`+*ssb6{jLJ^r@#@@B-zIS!~ ziDfYD>r?9vP+bF%aH_;PwIlJhx@E{#cDiPtro1WJYef<$_=!EkN^g{A#S^rLh$8Zm zCROGCKj`5Nolt?q6rZf5+NMD2hh2SfarE7g#?se zbb1<=Zy)rgYL%3}V|>O%b*x zigU1aH(ENK+DBr#pwXJ&FlA%36W~!45wRYdh`FcH!%R*~`S;TSnl~$ZU})2QxDgXd z$Q=x>Nfz5{{H`^JLd8R8dlxkn;zti8Nx}MGezTD6NfW-T)Y#S8QZPsc3xfDnQR?0H zSp&wC?Fb0Z!<{&z?K6FVOg*Fb#M(*qE^$3Gn+tq=dY-zl*PP-WGMc{M*}ekstPIKl zbDacQ=r6VFD9NjF<1W!yUST zteXX4+I25Go|*rXCMMwX!f61eRqH3T zlUlm==X>5nTm{7nG55%cM{UQOd!zUr>ZvU&)L`j?nWxQP>@znfU*f2$Vn;_uTaF$~ z1$QqwHooRt#{odrx8!{E+*~XGwhTsEEXQsZEIe&)xhy+8I=Oet+L+H5PUuhlGc+XP zplxRS;X@0+!VnIoV5)B4i4hfXgGs2fO6y1Vu6~9s?1@G$b2BF=jv_?=}pX!H(LOuX|aoE zaXrK6Hj4|0ih4%MU;p5fp1~1YeJS{QxlSFNowMD`j1chpv2xtE!}S`kiTjd}Wtocg zlQn=g5J9KvLwMax2sMS5cm4aBS}Fv9`V~{o1-<13& zpt1#_FVEGq+!ak|qyxh=zvq6$?5e6lBt#M$#5Q35N#V7wCMq@)&wrd+hxKtoibG6j zBM8t(O7{OJMDu}xI3_x~99T0uPE~M!^J!S1t?ZRoVy^)k1>F2jS1?_5mHGcdHh!_@ zdtFxL1GOvo)ESND)_Td2i!b`q>JC(#)Z%+pK({U+IERkhbch;fpvbI<^~2~4Bo439 zufkwuKyzNXKbxtL0B*?{-?`jMp+a;|>7@wbDsZN9pj)cE10T46E)mj22{B+!hRwW{ zKfT|@zm)$S_*m<|SX{#1iQwy56!Ervs&oXc{@1{lP}mF)M4DviC*$crXbTJeHRmmL z?leFElMZn9_ZpV(1SrNn93u|+X_UV|9k6c{_vQ2F&k6=in2!%&%Tp@_`c=V(+H4 z0~_@<2Z~J0<9X2j%UM`hK@DB^wzA~F;{mX!uE=JjDPS3h)EYRlR@m>fH=5>gdObKY zLV4E9i->qV_F%Hc5r1>EHNGn1Y~Uku@VN! zU{#M-DTA)sXr~h-J%=kSoLAoe6C8IuLybKDt54Sbx~-L1YG`Ocru20U8$j(_q*o)6 zP{pqaSMa#M`TUY$JWd=$fK&mB^VvG`VtY!EEup30a@oTP@I00T=>1gXe4gg$IBt~j z$e|z>?0U1mH}prT|7hri!3Q6*eDZ`)aElT+2%~|o8h*MNkXpzHcH5{w@~5-i88&dN zff6vs>^OT3(`no;^bg1l`UF4yrC;X~^;y1IwwbA4;be2pf77tPy}g|f3yT|%!9Wzm z5v7O2e+{32Q?eCgedyJl@xzgkSLt!K7(<}Z+Gxy~1_o(lz)I!lCXe&Jfl7=Z%jklt zqG*~e0e5}?Wq#)|;{$gKSD~&EkkFEI+a5~HXv9@=RB-%N9ze)JrwnhLh}kS_YylZU zV6+m#_&GXH%*LyL;&0`GrHv}N-I*lW>iIS|f&q=A*+f2#-OaK7-fTl>RmeKqh5v)) zzsBt9gtN2%n*G~1fam^X>y`_l?ws`gE<1H~s3AmxpSBDgPBmMq3`^0<9&EAt?9gm! z=^U)36la;uIXfu&&Op0K4q_cDG5X+B2H(pDRjA);r^556+GXI7mbPv%So!-2<1DsQLu7zdybkyAwFA)iCIm zpnVz;CZ=*f!2JF1ZL@6b-`eI=OgnW;bp6ZkMJHuOK0JV&gh9X|r=J!q7pTWHArmxx zq4oBZ@+}$d>2P2aWnjtv9}z@<-2@s(_0erw(4)`Rcw*@i6Wb%*ACxMCf2Teis-P*# zLV;Zwwuy;}wM~J6TUvN5hhN@HzBBHy0Y*P0gIYNdHU&iz!D!xIarKPE; zP9FfO1_{XybWIHO1;FOd*wplEroVp&>giiBLjQZS37!cJkDGhJ`rn$qc4RRXv-{6Z z51I=gQ7)OjlA z4f&J2N)y!P{8rG_8QKE+MRVBSxEc3^i&`|>Hr?R+efSqEV=?Y;?bhELslQsUe>7Eo z#Q*qP;4e4#Eri?$?Rc@p3){{WZ z9V{9LY-Qwo(aohh{>ARlK)tOpfsqv%Tp7l7XLj{axbHt;@;P{!cYW=UltmrA2kvWo*m-C+@Q<`?vbmmSv#OxT!F|X*QGD0zz`CMjtI(F7brWCo zN?1Dgqrowrm#kc`<=OtcXlCG;XI-1!?G@4xs|D~{Xz%R}P4&e5y`B(Oi)Fhv56iy{ zk#=XQo~rcb-X6D$Px*J+1yzdi%TL&>)Rqf1^xfUGW1?hYs;WuWx{9}%l5KWhX7`+Z!dMJ^8o7U{7HP8`>FI%|13Hx5Fe8 z>iO9&`~BdOK!*csh|dw1E{&#?l{Gf*yF`ZC57B_AM&%m%sF-wF8-JaxYmCz;P{x4~ zr-06OmmY>a}@99>f}%J zJf>KyAJWEu5a=2>s0aQ<+*mt4d3gAZf{I+fyFiHslz9Z|pKs%2s6daZgdc9+fLHCRN({%v@hRh*(E?cHbMpsYEo7bKh;ZBwzdDi>?q-}sPRYILNh$8qf z#+Ij~tupf#5RpKc2kD`wlK)4qn3rYO-`x#y$xx)h`9{b4LJQwH;lnvwCj#FV7Qhc) z6E&n)B~W)}Dzcf+z5?lRE!BsHFA|^?LPPo)YAiRSBq=_>g8I;yuYJ*8F0W&Taio(F zMs#jTXfZgpeyE}D<91p#6)liR>Ywpof$acR#8t$~p*D6`hUcTyfyAt1w>!fZ&hd4s zcd+a)Z^Qqrdop|7?K?BQG8~sl%PKH!p^#K^mB;Q)sC{?dfTck%FzI4d#n(3{d2WAI z@*o~M_fcyzXdvkg*W6@wl;Qzg33-nk^G#i!{((03SMcW=dfJ6?0tmu)rDUG`y2zLt zSE7R3GUl|Ek$(5w#ox4#ZU4qytWSZ^fRzd*Vt>GIL2gajfNJ}!2iSsD^ixw(`kxD5 zJSCQ72CRvV%lWIdXh|6%vu**OVc(1Qs}}7(uIKaKz^M>mwKTss|C@YKUS;_gzXTWV z_%C`1*}Wl6y^R#ao(%vnya*dG8u~Caq-b({$VfugBNfFc zNE>*c4}Jkum5O-Y|3PBO)H103{Z_s599X=z1cv^weS_Uws9)lE)Z8=gZ$mYj|4N9A z6H~Jofx&wAmkCVaeNIt|dp~zpEEPl_3`Op{Y>hKAp6Ux@+g+Ir$3{9Td2QgQnLkq% z;rl{0CqVO;B25@DI9t(Yj^uOM8?_sa{r!^ixBW@W#GNyfo|nu1-aW6Rm!1vz>%C87 zq?ly*W;uC`jw`K80uj!3tuO$S-o76F=JWl3Qcra(_-@-c#tjzQTE@%V`zl{H-P%zi zojqxV)6VE#*(-x1eW44jQ&@t79Zqz z*puZu=G*WP3}x*KPmq2)ciMtvF^y%6lp@pkcOgf;Qtc@MAYDr`m;Dm=CFkh_;Pf+q zY{Sb-w4j*6mU>D|p~d6N1xi7NTU)|8&r{fN4K@!SpA?hD|7wfR_Bo}xa~Kk>Ci41f zi1Ej`y8w37)z=GTJJdo#&vbRS5tGyEKKbekkKdjk-5VyRU*g`(leerL59tU#N#eFZ z28o>(Tjd`CBPO&#KSTJ+!zwF(8wLgF1~?&m(b6RWC8FZ{``poa zM|K5u()mRSMFmGo%QBmz-*ySiDzB|OtmGHh91Z1YRoF^1&iI`<-%UKxBNRdlBAYu% z77EZA*DzgZh^ROAcGkX2<_N1kO$N*vV!)sXHmr*Z@eefi`hyB}*vOgR*(BiSwTE4i z*YJF#VKj+&x3x>9y`IwcYMD!XQP7rSoK%#RgYzXfEulN70TDfa9$vwM70rL!lXwne=iT&yUeNn;sOr_)I$^_CU!anMf<1>Tt+eU4ZgFQOe>>3jm z1oW9NBVJ;XOgYyQ>_JA|YnvI8b9_SOE}*37BYmiKH8NEw=s3VMu0!^TTaXW@?krXZ zS)zV(UgXAIB>$t=_(KPw%jc=HqUh!UDY_YIv=83z&XTy+Sk;^!!_upAw_&>3m~?by zMb^r(_mDMgmp_#B#u&nD7sOqMTX!ZgGrpxg@_%!1r06ICN5>iLf_fv&8~6#`U|c>s za2Fkt(V)GQikj-{3jhpWq~9M-2fPl`nQQJQIKhOrwR=d!_5Dz8~lKK01n0pjDG( zhu;TL+!swx=L$}(J1=#)Sx<0a9|)KIGdPS09^%k~trw22{u3cNF*ST9&u|`pdV8=< zVlx`68`79*aN&e-Vnt{?bE$omC(NDFdi(i)>@DA^q?CJM;=xmC=aO|80Uh1!CFWMU zOHY7*s-xFb^&QenK|5XHw|OpIcmBM|r)<1AOnn=Vr%^XD!ffO<2`W$f+`gu&T|b5; z8-8=PW01omP4%$K-J8CCQo0@2qN08d+LN%zMGz}78e5F--KiPSsH|~cOz7z$;>F`} z{MwI*QzsReoqO!juL;SbMW>{s~c~hUmh3e~=Nl#Gdni6A)OQF`VMMQ1lq| z4x{OkzFbiSo!SU`x4U#8@nF1A=T{RqSCPEb$ESdG6{*(&8s!W%I!nCaEYVV#Zpm-G z0CE5jL6AzW=|tL%si1pH2{!BO?KUPDzf#SNmJMbDkZC^G&E4adMbQH$gGN!{c>;)D z@2>hOgwp?lnA~Ye)6XZBQ#Pj}z-9c=t%~2)A0_@$t!40Ud>$O_z@NR)mH7zaD2Ic` z-^w{YOyQLm*SFY2q|eqVDu_~?-FfA=d9gDYio~TWUc=>I;SMR%eb2L2z>95I*zE@>Oo*UmbaZy@&zu}d zAD^WLT}7<7jXygs^v$OAUp(EQ0j@vreUSm=kaUa*zSbN?#-u5f`~<-VUuw&n)%Wz4 z_tP35rWbph`2>D$d{A>aY_QBV%-vEDAKbs{{!A=WqU>DZ6(uoNtJt1I%ZhmA{IohP zoRyukUHo$eFRrS5lYyYeU!gG}Hp0QTwH3Glf(k=z+1Zxb@U$mE5+4CfjZ<#$NU& zw94f|^$Wj`1_mNC>Ucf#_Yl|&h0sFL6Y%IWLstGT(vLzgF*?G=L{XSQR6<%Y2D$vv zt<4` zrVElib-xCHl|yr@;XEXU#f*-H3_mZNTvRuQ8Ya3@LA*P+ZRQU>>QvUIyd3#s{5#o% z-s2!leIv^Km4eaXc;{{iZCL$ zRfBN=&eqtmn4)3?80tNKe1L*BZk5jO8l^D6*zkabH7Y7DPW_z_xUm4j+0=3r8y|s_ zNJ_F(0=yT`W&`R^%~6C1E|~dlS4e@+)F{;6sc$6JD-AhZ=N$Fcd1z#$6$;I4>ti*L zfmwX`1}H#)2_$vBUZ{sg9DbvlTCzFX_~k))bSQelZ`%b0Z$@LNOFeaXyQj6tRnLIP zJhJgWxB&f2aNo2F=EQt-K$@pj4jPkfPwO0QV%pezd3^NeZ6{oLeLc#4k7E3-S0h8B zA>QTXCCOKTHXs66{;|eYJ6-9j4}^L5WK_~`7lBMO0sosvgny_`Yx%0m9a}pWmZx5> zI9ToK>YeOb+jz6g`l*o7cdmwBdmaCDX}U*GV-7Oi@X9^U%9rM8z08J}Oy|^S)ZV=Q zcLKrGF-_k!jSidJo_g8UIZ7R2)c5=2@m+O6lVHtIBe1DZ_~NJ{%5o`#_DNUfGkJhB zDO+xhB>Rbr9aRKYj!v`F3 zANkH0igDc|#1g=7jNZQo0+;cp_z8c&KXfo-H<5MqBZhLW&e`~Wgk*3`Uv*pG2Tf>?8uvn(RhZ>%ZNZeU!xQo^pE!JoN%9pl|4v&2!m?k(e;StszD zP)m}L2uYmX+y|as4lwQxvvahWj7qi@&UNh@_AU8(`sV%+xR|`m17a9-=-=5?IJIhWBMMM zT>|M0F1xN_w@RAceygl+t}4Md{tkdj0Gx{_+K3$uvZ8m?zHh*zS98N}GTt2IDbptR zFxxeKE$Pf=flod>EoG~TBE*N2whpPm`3YKyG1t9@sgQSwvm4C~WW(JVj}s^ck`$l> zB)rd?I0piljis!#Ej7V@#Wm(Mmp=)=>H`j3S z&UNxiukncZ3Grw#j^Omv6r209mbnjaf*C#9&1ISL7NLW*&BuRZE3rrY4R$SbDH_53 z2gzvH=mmSn%8Id8#fPFOrvE`D8bItLPq262kP08g8h-p9BP2z4=|A~Q-=(mMhsalS zH%y+yGcEh3D=tl2U@PHmbq@B1YFLCT>!RO`R#s60=QB)?N57B~|C|cvy2`}R9Yg=j z*8d=V73WA>#I>uB|B5Q-z&s%h44vdm*)$mRj z!5O43Lu5m6dKg5)^D**=`R=#VpxQ0$S78qVpR8YB%3=_xnPR-S=NuKOlNW{cnvLn_ z&B?~({8cgK2hJx>5~zC%N62(vmr#V%C7!dgv$YS6k|7EDA3YII%ZiS^K?u=tAd%Z0 z65EILINsR{5b_dsS#_6imoOd8~fpajsr||!xM5mbA#H}*4OQi$J0kk%H5Vq zzXxHBe2q|iV@}aJ)?PD<(hpWUuUB0fFj?&F54Ci4jqU6-8@ay6wa?XoH_YBX)J%qi z( zKnNW2ZO+K0gG5vN&WQlftOO`s2OZ@9ax9ys^Onbg;#w~7kqpwT&er$M-8htXqZ zD1GElhA*{5U$~{^PuOV!#QPTyfbD+2BnQslOqDhUx_gN{`Y-riH7^9`G|T%#LIT+6 z5|N^U2;f8Sw=OSB_Y%~BdAFvOdpf3r=HrD6-IqH|e#9buk8Lpmi1N6>Xt4l(%b(r% zvAMNXo<{t|FN(i2oAA{i{gb|gqJA~kfN<^Q{SmkEuu5!F|HUuDuZ!7%4|zuJ*p+ES zU;Gj!D!eXM62)EGTGq=%4iiQL{}aUy)6-FmlK{Yh`3Knk*!Ew)UR1}jEyd-vZmdZ? zNeOML@EaLEA>^2hR?P{^t4Nue^01zNC+7jr$K5x#fcX(2;c?w_!I=tsY?CsVu6&%XfYIwC; zW}1kKgsjcRK3lLpNl%^dJ*LDo&LQa}+KiqM2z%IDVGV3yT^TNGqT|S@1 z;If`>0o1fRJpXFVzVt_aGU&ENhIjsJxG5s&_K$zsl9KE2aTT)m@I3R4KGUs@zJ3Mq z=)yJ;zv|a`MZK-*A@s7@ee_gyu=K5MUx{X~G@zQuIM4>&20TPjo)`#euMvhC!PyXS+2br+zag~0YiY-?Sdu*kJ-2dX7YYWKs-NGmzQ(JYrzCE zz_SWdnfZZ-fCfPnyTP}c&sL&S%2OLtUHdyh&)`IU zJ(X5XjYssbDeO~?o_a>98l78oVNh_v^=0{oMCj$DX0fC7SScaQ1bWb#qW58f=|n8x zXPf^WpcW~XmO%vyn87p*xbe%(4QsrPIowvHr@GK0o8p}UL~FCH7x#RrsgU$p+Bg{? ze~s!6HRSbFS4-PkA7J(`^)kS(1YxeF*V>w3xF3@m*hRf_)rUK(U*>@u`G-CA(b>_e+lh2XXmT3_s!70oi@xgM z9-Q-_=_@!*0f>=zm)-FNU)J&Uroj9izEtYk-soKot#K6P@5_1gg*(qw^zMYQO(7ux z%n9`e04dMxcW@P@taW(3=~aSYIU0pJ>Amm2i)QCH5#%7SmnD((eeMBB1(NOi8U=@C3Tm94op=>1lyJE26E3dX1VIV~F`3RTE@nz;MI8qTW?KS38^=F7 za{ArTxqwIRtE*AKK|8$EdXi|)Q!`_{Vie&krb|)zAwf|msS%zO(Zr~l({1%-1-a_6 znUPjT1D{iKr|?tqyEmsE$K~Ztr<~xI*$SSY$Rd+u+3a!2v8ZlGu!{% zP{w@L-w4P`8Byl2J#Q_3w|BdX(knmWNd(FCS9aCSgk8^V4%?L zXJB+M6`zxbl=N@U0{)o?-(xaPb3~VZp-Dau>{*J~6>5%s{{KY}t5iA3r1ve7Nrapz ztE>Wp_r)75p*HXV0hP_8@w{=oRBBfjSVsU&8xTOQ!pXTgkJ1I4U)wd#slxbTmj_h5 z)L*zk{F5zJsegkw+|eR^;I+W)akkOHY}J?pI6K{hNkoai%jXc|D|J7C&~+@wVKC9{ zmwCy_A9w5>F*FZhMA;)*7ccg%w_W(2OP%gq3yWbM`ZJwQ+2B&Lo8I)ZI7c#JcRwmN zu0VmE)q`*Q?yHgU_SBBA1v=Tft<%C(`b8(|76+=fbU_`N$X)Gz>0K&g$_{CFcGdXc z?2>lFdPP$6uRmB1zE4?~)?xxUu0mzwyZyR2SwVa0U3Qr(3`vKBzOffNuh9LCYBPmD zwWWI{mjktDr8R|uS0;N<~lNnDAL1tz1-D)fy}*7Dy5ueYrJ z(j+$g!TKlEIUQ8F_GZ*rfn>gOyO`U2>*RpKBdr6>OSJ*a4O1Xrn@igV5%VkxooVVh zsQ-dfiwyYMqkido!Mi2nauDLaUD+RQHMO;6!Mu9Iab@xB_fC`*$Re_&m0pnhoaA;4 z2c-MXy=i)I*8y1lHqWerg=*8|piL0%k=I1-W2eVv+v|o?4JMS8m0b5!Md17vR;ihI z^ZnsHnGCJMYHU4%n0w4W4T#(j{t2F2bswowsobAPkx3CazT*K62ta+&4+n%QATGVB zs{+0p66Xdt!4bYL3%s6th_}u+UuWyWQfI~~W4braZ=V9%|E8XB?GY?5R$GRg>TMe9 z%4=-*0~Y9QU1S44_sCD}ujBBj>8e<>bHKVkUThXS+l?4@<0~^>!SnMo7^%U8T$JGe z8jk@dM_P^0{{{Uz1WLdqoSZz75Znu+6CQ5-ADFHb41mDF`Y#Y(o)xd$sj4;#-H{qr zq$sFpu+jVKw(j7mx-T?gA;|9_=3c2{;~8kR%*L~G9C?gnB7sp0b8JqkBRyzt5eie2 zSD0MKbP}bd;RQ12diSZx|g@E1FL$ViT>2#A(4D|K|W881qB_B@4}mYkAuV>081 zT#cT#2}#vGCk~x{ymsua%yoW=)9?<-cMc;;{w$u9`=$7+=MTfwGtF_;Gu4!BhPLn_ zbP!f6wUE3#^1{jFyq}|FC6Xa@>WLDso)fxW0}$w2=v9q9c5vFn@v3{Yr(!?vzWqGR zu>+PO^})Sbm@0v_P#5uZ&TX$K`*UTZH?gdD@U3Nw}S$K*nTA#>0|?b{m)`C9nt79K5hU?O>4Ssvjx z3wMsTljpKx>dT+XT5%N#<2jxElK;<3uoNzE@lu&_`E`A0N-!>=$pYz0N6V1nveYOUH&pMAFM)oext$; zik9G1fvAm*^);ObiE@FZMoPw?FamUx-|6Ik(BN?R@M|i`<#wJ5hwhOwQy8aZ9&zH+ zJscUYoT?=hu`{e~!L2s{>_S9y@?R^FU^N!zzySziE&u3=;% zIeW#F#l-zqg0(>J=U$^UargjdS}+#&ao3<5LmS?%Wh^Y6%4d6~ z#s3LR0Qyq|2TE2BF^JhBpAc95RLPBmBGK6-53L`=O9NuPdbS44L<;FPT+=>G1O_4} z1uZYTk)XuNn8h8A9Tk*S5>P>eBGAHxH8PpbCn?7Ec4sP@fe~6Y=QaWS?9F)LLT?-D zohK7(UIF08)_?RT_I_t>-9u@%X;JKYxc3HsA9I+Gx8mDKF6{l0k^c=T2hHl*GBv!o(Iz{dqaxI`@QLR@P;IN^*c~?l$Hu zTF;L0G>T4BnunKnd1K?xD52x~I|ustojc^h8RzDSPTjINam}0WiHXs1ai_hO<;qC| z+aE%rw88>{VDNv|G4Olq>u=HBoCSiA%K3ZC6qxA2qpzLo%k+qtj2 zLP*@QngWFa-@QE^&_<{b;{LpX&dzJ35fBm_?zXRmV!B6!)(6q3xhH*&;hNQpJK4MZ z`kfYUaw_+$b*%0V9;+rk)zz7LrlOsgUJ6K5`c2w5BO4s`%&TrRbSRUp+l@vn3PnT& zO;118X*Z>>8a4jc*dPNPzF6yZp*=I_z*4uq3|P@F10NZe8IQsn0bhS8O#$H94_?{{HW zWdHdc-Pcp4x;1uqempelxzLCNNUTVJQaYSIy#;uQZC%gy52j)_Cue33 zJ&HB$e^&vUZO5jgE?2cl|GG%j$pdIE;w^=bk3Ntn)`a`g$!^sHxCaM&v`@@ z6;(n`QI957rbfE1laezzk}qJR;;Z;L7D87L{xErRal*lpaogk@X;NL~H?neSHmovoP1v z9aFKX{fWTV)Yy=hFh5-QA2zNsDw2 zNVjw|(%s$N&CvPox$*7$d!L6tK!F*~*?XV0KDGQV>?bGV=Skv!?i41v&zaugAFbX~ za$+hmavwLw(NXo?JA5Lz_(Rprp+wrHJ1J7y&ynkhNER77iC(&8>=m1h-$k`stajsp zU-9hG0Y+)tkBnzYp6Ji<73r(Aok%kBiY46fJKu-PulDIiKM@!PgaiA48No8l!Bw=3xC392^56PwSD6%Y9N^ zIUvobdS2yACM06~4o$G}M*yKiUa%ETZ#tZI2k(4Ab#plMoi=9zzsDRI#mbz*rRQv! z-{?;D{PxpN!KqZhw7z)l13L@IMOPG(8< za*^bDfUuMb@AVh3MrrJ!jWGk{F|4#vB+3P(wKfvq-%XyMW7_17!S+u;+K5SZsA83P zs;9Nq)wMetDrYVGPZCPXkt_|WpOP1#Sa>pQdjCP7YX)07Y0H{Jk+2D3Nhm@_{pt++ zf+$MgdT={zNm+YQnore-gBjob=R~hOE_1Nrm7`85C`81z?DRMK3m$u*K5EExyuZU; z9{{MJsHh9r=$o9GYxg=JK_}v>;;I#JH?Er0iWk`G0+mRP_x7?s`Q{r)pDpbRl~^!l zia=GRqxmgHc5koG5Fx*ub9%Cks%*OT zVrl8`6Tr>P6c*j6tEyZyK_t!SA2Xt|6T0*0EN7~NeSEk8-zsc44ov7kyV5W|e&2|S zDo2I$6$dm=H1xf+bVFB(a@E$VqafJ^#$Zutaq~b!{kX(FfNVG|Z=@_OiGhE5I7{~$ z`V+C81wGQ*(<>@zQux6OUb$Q|nvVW#&GwGp0{Vf++5C(nIFpEjJ^LmP!!12ycENKJ zxwg(U(s^MCzOVGkv5;;@LOkgE=vqctOpJGS?5kbNg1(QTImS*FH+hBJ(ap7YyGmuS z>R6NI1tjikKVs0TlDqMWH(EHDHw3#-H(&hI{r7*Mm(9Tc8H>t!FS6>(4ZFKdg*A56 zKZE+znRB`$FYAxT8U_EvIwNEF+)j+8;La-|FQ}~^dH)Lt`W#4=|3X9?ASTw~Q7517 zc+D=1AB0D$<4sI(<;_+4pkfTtYR%*Xpwm7ZGVC?jjYjh!Ls6QAfa|(`qOQ}^w@A^I zlP2AT*=8SB$I`Z6JggS5%@|;H|J^OclZdVP)%~y~$s2dy*@rIQl6vDD2&$a+>U(eF z!G!9tRH}BGrQpu$<&qBrm>1~J8<)ppBYR~d-}*&9wy?FBIe8Y%aC;-jF^xNtG5Ol| z2!%O36Z@RY_Q-p-D@XE1|KrDZ#_#zAg*I(~@v2v+Qjx+VkIqICcY@aL5u-$6dixEp zyU>dZ#?Q%}k1c^iqLVWcq`CHorIwmc`412CtR%@et5V;&sb@2pUiV=suD7RN#w-@F z3Lz5c*mglXTk0$m6gb_RgQTo*u(BI~!v$0Y_-icFq6(27;AL|49x>+k%726q*ONYB z)_jrGjm1r$4-SU2%X3+nHj`&P|Mtu&f!!MYhwWSbWSftby|HVnt775+#S0!`ou$lE z&UJ}M%!lu^$_$o@p=8PsQnO2enbhDoK$4}#qR{qD54}(nCQ=pw%MxfkW8#z^)d6U2~| zHghP;9sHy@(fO?*P%ve5Z)v9mgwCDMr-pJl-TX!pe2xNi=^t5QEs306xvysl0}@L( zIYN@`_a+M8$7$S7w>D*>vxD$1TX>raabxx4c(n8fW`t6=pPP`32Xe^k51)Xp&*ub- z7VDuNrA$-0vc+~nZP?gjqo#~}?iH{o-@s!FdA{$l^!g-gtsetXEI`NGR_g6WQfd1C9PkuP=v0(iE6qxDF{mP^L<|d2T`Qom= z#oFiKRNOGRd@k2*Z)pPx3W}i@wRo&XknD3hkkiT8-cZ(jru$S=n5f5U8|9fY>IU%lF^J=HC$-=M`HC3RJ=5Bp0CKq_XN zNUJgB;?yi!>4vInvG!KVPyJoRMsvs65O-$S52IJdz%5}gKfi#2!rt6DN`#85V@}97 zQR~oV=kKRfbc=akd%dA2&0k;bu*Y3eQWDtKGTbV9uPU!J$jBS*JCD3VTfsavejE=z zKLE`Pz5cPD6PoIU4E$Fs!v}M(U9CAE3kf~H=*<$8C}qv`X#INX`x}$Xpfe(J47eu2 z$dU_x>5rIT1YT=`ngW-@6{oBHp5WXaq1T>dzzVRsY8cnoE!k5EjZ#jl&(3xNE(`{+ zxRVX+?K~$+LWM3pGv0c+g8qIan_o!|?Dki-cTmnQi%hP?9nG}@>*|P*Q8B0(8D;j0 z+o0TDyuY9`Je(yKa8t0vUFzmOu#^k*aS5+E9yE2b*Up~O zBMP!w(vT+Dj$Q{R+~0Uumg+W4aJ!Td6yVR}M+=t+tMs?$N)qMvVDRClpPu&A6eO>T zw`eWj|GAsqLpZVx8%N9_ro?YpiTP1pkdlTqH-FsTv7i{MA`2UIl!kJNCcfdiJT*6h zUx3*C_Vw{9Fcv$TJN=KlDlV-HRuRH1rNzTxdo*te)LYUYQ-1;mF;@?qr?tjdlM2U7 ztl_;gu1i~6J;3s6f-^&mOC@^mXpfw$DI!8qIe+3;?Zl9C-B>PCWlc@-$Bje5jU1)l z5f(N`U((Y+u$KTaWPCbQDqL5#7<%q9gA9lgYC5@~qH{ryvl9Fvy>92OMR@1>_k>0_DAlBAEb1Gw|d zjl;K?J?m%LLQQ}B1P-5`A5+gDn~$CbHy$z6)YRk}l6~mSQzc7Px4XF#PVC$SjXEj@ zk-@KIr|12r4k1K59YsxUAiZT+&C!Ct{NsjghIH(E7^mxx3yzMtI*2ie^G0+f;1Y zXw{EzgPFXex=sC0kQ$rtUtm}^0dbIQT9c=g!uz=`M$-LyQu9C*nu5%;;aRu+3o8*U9z-;N6Q`+%5Cjk=^6OgNm ze4e@Ser5X{4R~J{l?(N|%Ast~xhgjR%ic0@rgwZ~iRirg_3=e`iY(2MI`sAnIusQs z_Tpme52I$fa+PnqEE@8l3& z9o$T%^9YEH#13vGz?pDVR>#kF9Tg=}2|L&q-hiQe!8Nr;-apYmW`l*gLt=XI_-S5` z$_7^t+go7}fZ4t^i29h<%;j4|M0W=Llmm@~64S4*_c5AgXQkmT_JM|A$w$Qxf|XkI zlZm$C;tJRPOw*784gHIAZJth2v4Cf+QnuozrZTV}B5*-~Z0rc^=*@rPJSspdqnWFC z1a;jQ?eBlXYCTq=>6)MgEa55LJAx_g&>&}bruS;sqBMU8+BE(8^~Q=j;=7ER0i%SP z{L(l)t@euRrz4V!i%SBRTatd;5+29L7X%!Z!s+s)(Wr{*9uZ|S*_L{Oh7bIBf;JIRGAZ_k01l~oS#7`hCR&;(DVrZD+i4ew0wk@qTvCJx zXmZhM3FIx*F|l8PxK6-4y43AyykmQ~H1yy$6~L?Y>ihJ2^{lFe#R4Zqf5H^JXGz=+ z%9&11_9F|&FiY!&Mgwj~V~y3Jpwg6lEf6D%SV6mdm+KR$8a`(^VCy5`mi^=#giihj z*>9l8Lu0bsB)bH?r(3e4lSV^hRQSXwm_U9hM%tntWx^qNVTI@!u&COAA;FtLT&0jI zY^xK97osoQTFRd|Ni$1K?@R)!Jxhzs@y`X~EgP(B#v{c(%V6_qMqTjsIiLMNlqwKU z>!sCA!M&sz|5z3x9v%%c0w3&}cbl7w-Sr-*>q9lDz?KS7itg>56@TDHpJUMKU6yR{{miNn-<;v>ULuGcNtx{j#<|_};xIX2OW4Z<^vNu-U|B@wdaO-C==}5*!@mR@=vOIrgsNGtx zR=Jh7S6LZ$I6q!nCbVTR#e<-h*K9+|5%`Q<>qdiP`i$mRSHVOu)RHy&7ZmJ~a^$lE zHJYI&x8fK#^~V_SwcZc?@g7}i5|Q#9Rd8H159K;n2hdTzEJNs-MM9kY`T!&_I~y|Q zRu|w^$LF*+JH*Sj9zJ5mIlsFqBKyGtR0c`TPuOY0(XamrMI`&n$sZ2#S-sRr95AG0A!Yxkl&UN6_p1> zNJhj%<`(lmU%$)@%Q4CZYuhf6e{msj67m2=DcClaJCTnz3Vmm?-|5#AP}B7nEad_z zwcdF%-fQPxI4rVf*ch=_2niPL{~__up<-{$BNB-T_WSr40vhO z4n*JCHox<;qJ*}D7)&0<94J*vfz~7Yt`9S`1`#(Ts1;rPw0*n~tg*O(E7{Z@4KQf! zJ;z&@F=F+KtuG~_NT5)DM8qF(I!7d^IMmLvv0t|!B%4e6k}l_c(D%O{P0h?&)KVvF zPn#tELJb(bVgG^}I7E@vL;$#;Oh!zsuBnTt=Gk=egWC#sia^}nRBdab#KHFiCBql% zD^*|J-gzNj4;7lHVzCvpP2r4p|?U0a|*!i+1;sT_`ge4_? zPRy5z>|a=T=3~@F;CEN)Ayt%~g2%Xo&Uy)*M|)D7tKX#w#{Nl;(qek)#dq4R0dgSF zdYCU5i7P+pBc8t4+5_w8=>cYj&-m&Wyg`NQOmvWv|m;B*$5V(@a5yQ7AqL9bp?QZDW8 zs(<`RN)gQ98a3Irf$Y)Jgj;q?<1al^R@iK%2K({PR=nPepdrxb_YQo2ZuQ`o!9k-- zUxdiM9_c;KFP&_a%uh^zcly40aTg+>1Lnv>xe#{X2wV|C;7CN+fYbJ`zZV{VN0`Yw z(TZ&U8P^Wl`M+A{Xc)v1*Ph-ctZOW%T|P?c1pxMV^!y0DEd+`s zP5Oti7{*@71;F8O0VruHdDbDFYNK!Tuts5<~>VyQ%5JJAU%6L|y zj51ZkX_+O4Ga_KRHDiL{?=Rf|F|)Q?=~Sv$E0#1Cm0ba>9mTJ?Xv$!MNH;aV5W;kW zDe1I~S(lXMKY@lUn*V>F3%a>sQ-><;N{T00Bc`BWX961eU&;HV) zB*g(W_G_TgPB9`t;_Z%=J+ zk<$6$jS`U+Fa%t^xZWlSm(g!diUd{E(pREIcM|kuJC!gE2Q!8)jp)q*x*Qn@C ztpm@fI3aoFGyaxCV(>b}s==Y5jnh@x?Swk;vT1I8o-NY+&5}vJ68V$kTrLQ54LGHJ z?EV;OkyVTp_alkTh7M7H)r1r-F6~G~_Y$2sccXF};e$f@5DBuV>@I4l%>W<~xGSt7 zLBQj=9V@H6{9duiR^PcMJMdv|R)^za|yB|)vRRuRu?G6U_9=Tp>AvR??+Wx8jU~>_; znHkN!A3^h}$?v3DLc?V@X_OEo&;u@>WD*;(D5@LM)s?&bcUs3uT<|CelG;*PT-a>| zqRIE~-w(PoDivVxU8)`(69Ny8YKbLzGy}G$m2r1lI5-|dxJokQVcJ^Ct@-Hy zNvXVE6a>wOHOn8$Jta45_}{cYsdlZqy9Mt=yzej>Dbpdkd&mtLXgHWSI2s3L3&wAA z<(kx0g=iG&{RE;R>poVLUSYmCOHVn_`y{IdlI(aNCE|{yR0C!N;Ihtr@-w7y^J#2Z6x3 z{;e`gBzKMteUPp!HobYQ_vZ+Q7i)(#aT_>};v+`iMka#2hzbbW|5_}3U0J$JlKKtu^ zD^!d!g{%8e)V=y zIe)3GvJ3#`Ey5hYeFw9-2J%uNU!so-o(RzU=bVqD3{7g`d5Apql*CIpWKH~HMS?q&uX7#Q*? z@Pl&$(U$<@4I&2T0)r4Sn#E))BmHHS>DY-VWw^~*yUixbT^tQxP2x|Iluc7p3MCYU zZ2u$xH9EZh9O&`owt2pFqT}<^ra8<>T*dn!Vs87w03(|5_VecyQgNRzzkW@Q1KIcD zObVjAD!6tba&zPHoN-F-UIA2(9^i;7B%)behuuI;|CNj}H2FoCXeGCI+xb8+>B=mBpQl_|D{5CpST?N zKdUwqlWZ|~Ph%CJ`uWHO2qh9P!a$oil)0yomX-#D8@;_aNNzkl3$lMa4r%C%K}>BH z@-?3iC5k$i`=VY73{fD&Rs($0=DLsoOfOs9-d5DB2FAty%iy<~+)j@`%WgpP&st=D zeLWd|n`!hBG=pD?ik|jLODi%OB&SOiT%HaAoTqWP8_S7vOplSer@M1;G=B<|eM{TR zsvk%0z4)Wj*gwpp!2g0+@DB&FYfiAQ>9CCW$WwJDc0siaWVJIskio6HtBZ^%0 zeA4M``7#W9@Q3r%QiplFd%DGYkPqjmFp>XE{)kKz=c!byG9{|8oMAX{GcuxLu6)Yt zhRnpW2yOu)sJdY`NmuCJ!7h)NkKUhvJ=hG8JdI;WBlj0u1nC8EX>}lf{-Y|m`-YJE z-)o9Li<93A46?s~{d7WxWSPN1;(e2DMQ(f7V9>Q406r6})ic58JRmE3VJ~!bvusPa z{&KF;KjF2@rjPCJU*$+fJ*dU(-SP`pjezpUKpd_!s;d+#Ak&)iCxhU63|*ri3ltYS z$+^#`VlSCR4Z-mcZ)wT8bxpv%61~Ui7fNV!xW-B&d>5Ba9f2Lj5b~5vcgg3D*l&); zlPr$*=czrzAI4a1?ihMbfTyC4yt=$>yS)COIG`%~MhqxI3QsosqAUckt46f*mBYV% zdk1J->-qUGma&!I88`FU6ILDit0b%FoAw)IQ4oFQ8hyTA^sGCM71q8+wIsu8v8vfK zr%`bY6tOUs*jD5Jq7yRME{>o%8BIBakfi0euH0s$7R$4Bm;d&sZShbl;y?<5u1ySr z{4VyHvu(vZW}n=rS(#t$jLdac5Ho}{HVHo1KTQV%<5sjs_h32;0K{JPU`D_xOrF!m z<(BG7n|$Pb@gN9bu-Jl1USTgH&T@78oRG^l0pNg`nic!8hd?P{Fh@V19vufpXgPKA zBA4z}kZWGT?-dkC1C2qkE?g#0y+&=11IMXMlL?cWdtkUC*Jig*+PLvIELv_59ZH%57Q_srYx+7tMFXOomulfF`T$SMu46Bgpmv z`Ho5qYE^mKXkseyFB z0~Sll5~Y@VLZYhD8wHt1_V3+6Y2+)rFi6_^pLOr?ZXdv*!rmW~c&y7loSY09 zE(YU9V)i$%H2GHITsch#0}zn)VChbOem_nt5YnA>3Y|IJ-bq{xt=~*YN|J_g`suB0 z2Ls-I#_fa_H(nxABA`#=yM&BmF`ZMBS#gfkf+E#$Mt( zH+H)u)|-3F(kIt>B9J!LP5rj$@qK5+^>;9lNlMGIriHA+LSJ_(lmDu_^X& zo~{$T75NrDJ_t=ydUcYZJe_bVAt$i=aNtR`OP{p5aR zF+oAhiMw8|*$1LJ4mb0u2Ovf~R9L4^cdh)GE#yAqU+W}HLj2Ell9t?~eX$MuJEF#Q zBn4pC3=F;Iw7FD@Akj9m_o%{Eawi;A-Sjp#8139yo`Rg3Mp6fz~#<_PMLjGyBRzDe}$ zKDWKYn&BBG@N6LO6;0&BPgxmZ!-v+4EP-Sc(6-v?a_j}&l$KF7%Kt6wg{bi5;K6OD zl;dDVeP3GueGAZ5wuW~ai6rnb9Zo<1g{L_IY!xUK&)^rqEL@7N7ea>J+K92FqZyP9 z4$H1illY^`U}6@SQJ~X(9l+;yI#i|4b7*UqEmyKX4<0ZGLGPi8dv_0DvXTG|@zod$ z(Jg|UH9UZECIbl+(wX_-Uy5S1i9NM{tOZ6Q@$|HE^7K3&r`_TY15QB%@H-?S-B1fh zC%cgT zwvl`5I*0?fPo(=8W}kQ~pCaD!MiH~?h%NxoW7u41@Gwa9OmyD@oRhb^d$?Ub>?ThF znj3niIkK37MFM8-{MAPG^gagB=ZPzW!UABZ1;PKMfX-QQ7gv;M1-=O|PyJv7fZ@OE zY;Oe_yP^M+s(BfmTIilcyi6T=-q84lQYhu>l0s*h7VKY*166_P0Yi4=OU2c9U7QPE=Eg;;jn7vlh z;qXXW2O+_4#nr$d(3hZuERX!iAt#<#_zzufdfDX=P|NI2F4_r~6M3BH@b4($T&t?7 z4N%vn^lA*Ht^YI%Es#93e>hcsD+$p1bBEivp!;LrGgVF#h%W1+?^_G*#KK75zl_WU z^+cjdZ&H-Ri>yH}f=?B5_{rxU%a=_@^6g6&Vko6VsdgPRLx-es;bbLI(TU2kR)ZZ-|u<2d4%0;{4?2|B&6Tv4F-LL_Lg{Y$=Fu z645~y3>1J)J3N&SdDc0Y`B?@~A>37GGt1+QpTc?cq1iX_iE)D>_cs z%1q<1+n|BxO=$F_(p|^EKZvVvhH7Ef=@~8X6uc^M%HiYGkheTXR zX*1L)AHFnh`v(%RDXb!YoyoiM9TVD>x)V`RxM^Bwv;hksO3PM6coCH8HP$~mWpfkq zZ_|T3uMR+By|y{TQqZI1#f310@}CNWYv`V*J;M#cE{bf1IPkrP-F7dY-oIh>iU-4% zZTCF!>vXmR!`l~6+b)0;b}O0>WE-R2^Yx|1e%RyHwOK}B?PNzz{gZ=|l2V54nB^HDK(-0BQ;5%z=@!Pv_SZC~Km<0Wvbt5!NIcdeC%B zFG3kUkfKF!VN!8v>Y9is^5NIR1; z7)XgI1_Z%+e?2|3LdL@Ows&X+6#=IO?RS-~sGe9%p73LWivE|ya@ojn!9r7}sC^!QseTH>doIR8&6P5CNSM3}-4-%%X4_ zH*;5hwz!P;>=&i1-(eQxLWhjb#>dkXxC zynzxho%Fq}LV%j6f*a^tQ$q_Vc0HhU_|#iiDw?ebmQ0=k|NrF5(&1w7E=phha%=KS zRu+1%s`&IDw`>sHG8}grZZ(u==l-4Fn^!`hOY7$%FCw>qd@C{LGIZb8X*BNQi?wf?Xy-{9>=5eH|eEaOi z>RcR$F+pYhrOYeGF=4-@o6|c$9aGCS4+e&AVQ=(dPpm1oI8(Q@QMLsctprd2L+jF}=!y*>nC!e!80JqxRA(T;LFYi<#QaH6BpWDswp8GV!m1nu^Y zx%jj#<;X6!u%R*zfP7jT8wo*UZX2&V)ROV<+S0c4w6HH6x7p_05%Qy;x(3)S5VWJ* z0xFVW>nTx@hiB8Z3?W= zM-QLn^_45g)zge<_YFeWO=hE#e@%ABG5Y|45_tBMX(y<~&7`mIS9tkt%%(B|wt@C& zw;g`C7O-uXtH(Q>PH#0^?KNxlXc4HOfS9U3-jmy5FZcMiJWq%8Tutx=HEndkwDXM@ z!P}N(#`^EvH~8*rMIbCW@TwEue$}X}pLo;U4Mn zc!%;mk}`-ziQ$Ai1tYTE4-Z6J>W9d)T)|0x059N+TY~vL#%r$GdxR74w069& zRu711v6-jH{=nHxnmdD&i>)_0gV!L*pMJ;9e@8dhkpY7D4{5EQb!sq=+o_#!Ti|Gp zpxT!lAyAG1kJ>^5vdO7lS~}kcpn7`kc$FrbR4J5 znRR+{%sb@<7VV7oR_?i4?MJoPK}C_Y$=saC;~$%{?LcYE*g{MVnns(G?U^>JrQ1`< z*v!PQYpab`2arWyeI$dV@3~Wp9raKjE59dng!y;cC8elVwxw#-M@BT%Y8v@rClmi36YT=kO?qj5csM5%$e-8|Cq6-W%oafvm~F zX#v+SHL&ehhY;W(lxG#J==z|~(~dSWfTZ=joMA&!6LMf=Q%6&J;AvbGi>^N33i@7-0b#br1CQH&8oQ zsO`XY3avN|dZ7`PdhWh6BDGy#rsF(i_X0?)kRUp7Y?d!04r-0$LAO)s!rtkJAx5{* z_};>Vm{6cY2adD9k5C*3T%I}<^j(vSa?O@MtIBo)T>Zrhz9mxN6lo<)as6d zN3QO{EU&ayrc_LhK~ee-bu#Aeb=Y^2WC3&{F2sDJ_xYbVRKpBRmbQH6iXEn;0d0af zHlFG0nLHo(vb-EYLm&M{)+C8D=_>XARxYcQl%&T=4M^Y*SQpPu#E~waFwc$T@QK#EGmY z%++mQ(!H+m#=w8X$et*l|578?t4glJi^WGAko~RqD0GxFMS94EISfTIZpEmi``Uq6 zixAoChyHPf6xnN!#ZRNWVD3|C1~7{9cJ?xy;)%4w6qVBD74qYXM!5|P4Rj4n)$^|8 zw>0C>9mG=E=T+4)t6k`Q)4OF&CG58vSmv?O1u0FD@Su74FNF4; zbkHL4P-TvHg*#BN(&i4kgE!mW+tJ!NSPVY5we9HXY8h<&A-^uGb1~q{0UcA(>8Iw6 zU|f?#J!E`_j&9KiQlig~!i)N(7+$$5V>K%+83pJ5_LfUNEBfHzWO05W)qQ`UErAE} zIkeUNOM1G2Kk`EhMJKzI>RgEd!Qe--w2E0+;*TCZmgDVQ>0Av*ISzVf_=1)#nXAHF zphiHmA?Y@MIXbM1i^IrI+CZ$KzaJ@GUQ9dbH*z6zV!lqpq0z*n&4DUOqJy8+UT2F( zDrsvtFbqD?*sibF`Zi~yxR)*b-uWS`9~uV)O=jl=?)4cos4^b@S|wm48!KHk9`q+e zhQ{a*S)UhhI0n{*72Y>$f@<8dBFBMBOm}JBKRjMQxqKJsn$yntfxzS+6#QEFf5Lej zF>U{W7_OwQIf=S^*El^HbtFb0n*b;UEHseGh>O!8w8Jm5NWm`$F${W3>m49{!9J&i zgGgK|)y^%~@;7xokVr~lV>0~Q^BB!AIuW&osO~l4NL;DD&s1+p>p%h#p!D_gxr=3p z8~I)CS0t$xDbr$U#`6aAm6?`Ib;QqY`i37xpxY_Pa|t-VzVyN`?ix+xW2oPDJbDR6 zm#mT1@K2O483EAqr^De7ZR2O2JkF}8C+VoSc<_VFTKKG=FhA_L&jJYFL5`MKfsF9? zzmpx@d-7lCuK~?o1+riEiUzKm5y@{~@NoiALyuSf6~ukz7`O=Jk^VuZ2t+ib!1HzW zMNv*fWU~j1D9tgE3Ajyj6+D{oMTcO?kIRim;eVL5Ri18 zIlB#|+oAFk!{5fTR`{EKkx~r*0lloThK~jER>lTcd_q$xZ`RitBQle@6x_I(c8R(i z3pZiPW6xmk>FDVurJ|R}EKYkezt>fGi2)qR^uJvJnf8v+=Dm^Dk|?*>!>z@k&odP# z%(vTmt$x7_hu4)&%z5?IpSk1|4$4nH&Q1v1&y4!nd7v+t@1nc`F34r}Yk#v#qN*HC zy%4FjV<@U!eV=ZRcwdZA_}7}^5b$%^Ryxsd?2@J&2qJo0oSKWY~0?6VY|MI(y|8-qjss{(oh`(^ghNhWd_AL(Ejln$`K20ey zfSIgz$BAFNI;i1QL;-Ab{v;4>LHA+=4sf-sXVgZv-OhlKW3pH!M6{Y!3Vzns?N%gW z9k24#OFkEo1gAOV8h7{fya&S)DlNZBJB(%sWt-3tmc?y*zw3dr2nk6N60t!9UOU_i zM1r`hxchN4M(2iS+GWgr_#sU)YOa;fx5G-@3|-w!Q@x@LZcBSpvFgbm=k9syjQJpU zj1>TBb3nf!Mweq^ib+bg&d%bzk@uka z9!3&k0Ms|#Nq7?PuRkzoUnIV@f@v6U4FJYFPrKwZ0D%|5?5^5EoPpyJ{sbO}`+yov z>`Gu}zo&wWTYrAyHoJbyktuV9&`2PGj4O~ji7V5m4g_JCJlANx^{fE^Spd8+BwAWpU@D&g?)7^Zn}rA0P{*4_?@)n3 z#NHtM8;MS(pz<)v7ZwalVnf5hDh(@lkGfbUOE#d6Us_yl|8(PVq@E=a!`c3VivF2| zlLkZH>o-H8H9wSd(eg13G7WS;8h%N)lM3pQVk#>`!MLU+1=-KQ;cS9I3?!Y6ha_VM z9phobo8*8HYmM(EZi!+n>z%X*4k_W(_Vy33t2yzQ%GU#nwhJTl z3cyW&2l;b}9v3*qgW$N?1Jt*yQ-)G+(>tNhP4NA*MHl>oYHLm_%Rb_LwlkWSPZolS zk%SlPQ~BqPncd@ziybS^4{%o=0h-SYmj>^!fAcK7EMxRI5B>XS3HT>UKoknw+CHT9 z)a;pN%ov1OSs?-JDxeuk0Tdt(o$oH{FeV@o?lG@d4{tC*5ZYUa-WVIe`hwbG) z4aT>nUVlF4UK12MbO_H@?uh{+rNJ~PA2|-NvS#)rq)KfW51XILFJA~)O+FX<_O9|LldABzA_$EbpZEc9YPISLCp=A`rOc>QU`Gt4Sb z``uQMh0vbGSXtRU3mB{H2TBl3gvJ(7vAz>2 z1HpcQKpZagBCFJk07#;I>-oN|N5?H}ElC0I=sQtP#^^6v_^zqS*<37UwER1pcAHat zA7?s#)tMmiTfk*<4@{nQclf~er_vl=vmZ64npn_jS#+lJ>!utdg4JW{KAl*kp0Czm zr=`tO%K2V z2AA8-Z-Bf8xh3PpI}iBg>e_@F#LN=}aN>x;0w-=dGh56^Zlu@S>@ zOH0`Zx8X+TBv^!eo?FnVm%!C_9(ke;pDVjm@l>tywZiqth6e;Ib}9%M9JqL#-qk3- zX^zSl09N*QZ$6RGMl%jPB18hAPMzK|nLjNq`;K-e5_L{VnrC{jT`VAVol44lZrIHu z-pwyjzKHYvi##k(6Yci$mqCzJuCC(Db0K}9IL~RH>t|!~xxz0*C|gx4htoE^3tlEw zN;e*u5hm5&t)RR?smD=UJ^duURN*Dc%$_fOyh#Z}9*E=19h21yXzVkp{r#m~I=&rv z)}?gw)I90?^7+9s?Cbpx)U;O&p())+1`UrSZJuMLspLc=WeuO3kP|ljSi_?gU?A#O>7#RNIx{#nr7fX~p~03~eHl1cpOm5Aa0eCmJg{e_ zZ=rRp7=gvx_-9rhzB#;^a0*lUMIksX*Tl{))id0AG&0;RNiQMAT4CKo;Wl%a&tKAQ z+eLp$H>PPV)zv-y_KCq$Z{3*ln>V6iEqKqd4z3Eiwrpcq*Z5Cedarp;U3Tg7T<>#n zaS`%4ewo7{G@Sp$pQm1jp06xnZ2Z3JC|V@-&I^rv26>UpIyjy4FANv6H)R>dl+&H< zm8a$;uOonOk=eN)=_=>q`#0-gb~8?_pA+A9zIu86X6@*>7~a8}MSv|Pn+)slP#wKt z=2(@wrduB_;D9{utQ%rbyBQE`s?{0t_04i|W@cUXH3->w`}u7?3#zQt zFEzze$T9n&y!HgwDJbf6L#@h{K2`LwQS(0On^|8}{av23$+0#9x5xnCT-eO4!1mEm z(ow#yj8Ji{f16vLI9}>WLE7-SwXHXiBS!5#0b3a)fVOM+R7vP(X`Xkgex#nky~ple zCqy*|TLpZCyjX2%g1rZ{7ImF0=yw;gB`j|=J)S`CHH=rOP_E3q8)TSylRNOEKGG*$ zYgXCkz^_kNX#zdc$N=j!5u$IX>sq2xF!4V1LJqiv4)@LjHLLBhgDI`hWFiwSIxMwU zBiD6OIm6K<$8LM3dc$`c67e7(@MmclueLp6T|N@~SA;*uIf!bgt~2Lb$Ldd#b8AZ- z+Av<6d1B|o1<6cU;@WGCq3x!%pJ%wW!RDWx_4ZS0JFJ&uj~B$@xOew%vB4JbVcwvz z+S%E%<2N?uEbXj_g<5Jo7cime?UiQB!d3d<`Z+oiHIcjAXgG$)GsN;M2%LvN`P%HH z2ymU3I3Nqr?o6h1Wv0F*kkfL^ksa%4Ij~?pfoThHT+a@lz-Z^xzAfK2-_INbi$eNW zSMBXR6xYop=W}H0YAaKZekJ!xvkPE}Q{cihzuf+8Se~xF%f&VB@!75IGb=k)S zO4xG)qZ|4hkg&3GV{6n3VR>w2#Rw$av0O&P{$gT}2o9qL#ug04X*xPM-`CrOkH3O` zx4-7zo6T(N8uCiyaXK&!1@R=6U?2#DQwBgF{iIgRx`T?~H@l=?N>o-?eoJr{nyZ2^ znP?qcV`^{Sa1-&IegFy#5m^60;VPIrupG!3&E!7*VbE$u5RB4vcFyX!R?t+-s4a~9 zz7{8dhVr zIR(E|JFj((nv@#vlg1h98wS;qi zlacdOx{0rqB0N%4>;7~01PFg@Ym2bXssq+6fna~4^xNR`l5@BhbAqSU%Sj%7#mR|* zvwj9TK4P6qTksrMOz*0E%@ZA1uBv&-qIAWpkV^@M$@en`c!H&!@cJB;-OL-$pik=_ z2L3MquYCO)zz`{EBS|O_*WPY4{?mrl#AKs(JcS6_dqXIZ>n1kay-@_nq8Ri?Hw z{n;lTZPSW3%xQEAGb&O&Ss1hFmu4PkXQ^R6(rui=b&?xKZ>ce;(TS!r(a=uZwtbg# z*r)4bKj-Iq)ZfeNkPlCc@@drkoL={M6(@8x*rZ&&rzbwUvWh#pEEGnL z?%V#nQ)Y`}aH*_5f%}HXQweT$85?1r82>8!OeI%+`7IQKoSi+mIcr_eMaB8>Ceq%CRp$*bkF`7xYse~`!w0^iRw_HXGdll{RAVtq+v-|uoeER+hNfjw} zA9x=3-{z+%VxTWxTEXak9_KQ5^!zxtYfkyHkHYG?h?SR@Z)4*~#?SxRClIze@pUp! z?I8cPD0P�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%0DdZKU}N;Yqg#N_g_3A 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 W_vG7W01uvGVDNPHb6Mw<&;$U2N{_Ap literal 0 HcmV?d00001 diff --git a/docs/images/ptpython.png b/docs/images/ptpython.png new file mode 100644 index 0000000000000000000000000000000000000000..c46b55a8e6bfb489a558cf67390f10c7c7c5a606 GIT binary patch literal 28700 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 literal 0 HcmV?d00001 From 8f85dd53b64219fa611f3c7e361e582a31e2e73b Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 7 Jan 2016 21:38:52 +0100 Subject: [PATCH 119/470] Use highlighters instead of input processors for highlighting text in the history screen (much faster). --- ptpython/history_browser.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/ptpython/history_browser.py b/ptpython/history_browser.py index a4265c76..8daca667 100644 --- a/ptpython/history_browser.py +++ b/ptpython/history_browser.py @@ -11,15 +11,15 @@ from prompt_toolkit.buffer_mapping import BufferMapping from prompt_toolkit.document import Document from prompt_toolkit.enums import DEFAULT_BUFFER -from prompt_toolkit.filters import Always, Condition, HasFocus, InFocusStack +from prompt_toolkit.filters import Condition, HasFocus, InFocusStack from prompt_toolkit.key_binding.manager import KeyBindingManager from prompt_toolkit.keys import Keys from prompt_toolkit.layout.containers import HSplit, VSplit, Window, FloatContainer, Float, ConditionalContainer, Container, ScrollOffsets from prompt_toolkit.layout.controls import BufferControl, FillControl from prompt_toolkit.layout.dimension import LayoutDimension as D +from prompt_toolkit.layout.highlighters import SearchHighlighter, SelectionHighlighter from prompt_toolkit.layout.lexers import PygmentsLexer from prompt_toolkit.layout.margins import Margin, ScrollbarMargin -from prompt_toolkit.layout.processors import HighlightSearchProcessor, HighlightSelectionProcessor from prompt_toolkit.layout.processors import Processor, Transformation from prompt_toolkit.layout.screen import Char from prompt_toolkit.layout.toolbars import ArgToolbar, SearchToolbar @@ -143,9 +143,9 @@ def create_layout(python_input, history_mapping): Create and return a `Container` instance for the history application. """ - default_processors = [ - HighlightSearchProcessor(preview_search=Always()), - HighlightSelectionProcessor()] + highlighters = [ + SearchHighlighter(preview_search=True), + SelectionHighlighter()] help_window = create_popup_window( title='History Help', @@ -154,7 +154,7 @@ def create_layout(python_input, history_mapping): buffer_name=HELP_BUFFER, default_char=Char(token=Token), lexer=PygmentsLexer(RstLexer), - input_processors=default_processors), + highlighters=highlighters), right_margins=[ScrollbarMargin()], scroll_offsets=ScrollOffsets(top=2, bottom=2))) @@ -172,7 +172,7 @@ def create_layout(python_input, history_mapping): buffer_name=HISTORY_BUFFER, wrap_lines=False, lexer=PygmentsLexer(PythonLexer), - input_processors=default_processors), + highlighters=highlighters), left_margins=[HistoryMargin(history_mapping)], scroll_offsets=ScrollOffsets(top=2, bottom=2)), # Separator. @@ -183,7 +183,8 @@ def create_layout(python_input, history_mapping): content=BufferControl( buffer_name=DEFAULT_BUFFER, wrap_lines=False, - input_processors=[GrayExistingText(history_mapping)] + default_processors, + highlighters=highlighters, + input_processors=[GrayExistingText(history_mapping)], lexer=PygmentsLexer(PythonLexer)), left_margins=[ResultMargin(history_mapping)], scroll_offsets=ScrollOffsets(top=2, bottom=2)), From 634ce388ba953367969146338c12974f99f6e174 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Mon, 18 Jan 2016 20:47:10 +0100 Subject: [PATCH 120/470] Specialize if PY2, not if PY3. --- ptpython/history_browser.py | 6 +++--- ptpython/python_input.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ptpython/history_browser.py b/ptpython/history_browser.py index 8daca667..d38b05cc 100644 --- a/ptpython/history_browser.py +++ b/ptpython/history_browser.py @@ -35,10 +35,10 @@ from functools import partial import six -if six.PY3: - from pygments.lexers import Python3Lexer as PythonLexer -else: +if six.PY2: from pygments.lexers import PythonLexer +else: + from pygments.lexers import Python3Lexer as PythonLexer HISTORY_BUFFER = 'HISTORY_BUFFER' diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 5785d72c..40a43219 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -39,10 +39,10 @@ import six import __future__ -if six.PY3: - from pygments.lexers import Python3Lexer as PythonLexer -else: +if six.PY2: from pygments.lexers import PythonLexer +else: + from pygments.lexers import Python3Lexer as PythonLexer __all__ = ( 'PythonInput', From bb849fca134246dfcd1c313a25e9f9b786c6a735 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Mon, 18 Jan 2016 20:50:10 +0100 Subject: [PATCH 121/470] Fix: Jedi doesn't have an attribute ParseError. --- ptpython/utils.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/ptpython/utils.py b/ptpython/utils.py index f6d0f1a1..08e94f0c 100644 --- a/ptpython/utils.py +++ b/ptpython/utils.py @@ -65,9 +65,6 @@ def get_jedi_script_from_document(document, locals, globals): except KeyError: # Workaroud for a crash when the input is "u'", the start of a unicode string. return None - except jedi.ParseError: - # Workaround for: ParseError: incomplete input: type='ENDMARKER', value='', start_pos=(4, 0) - return None _multiline_string_delims = re.compile('''[']{3}|["]{3}''') From f9bca69af7b7823146af744493906f89260ea75e Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Mon, 18 Jan 2016 20:48:19 +0100 Subject: [PATCH 122/470] Use PromptMargin from prompt_toolkit. --- ptpython/layout.py | 40 +++++++++++----------------------------- 1 file changed, 11 insertions(+), 29 deletions(-) diff --git a/ptpython/layout.py b/ptpython/layout.py index b330c4e5..9f71e94f 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -10,7 +10,7 @@ from prompt_toolkit.layout.controls import BufferControl, TokenListControl, FillControl from prompt_toolkit.layout.dimension import LayoutDimension from prompt_toolkit.layout.lexers import SimpleLexer -from prompt_toolkit.layout.margins import Margin +from prompt_toolkit.layout.margins import PromptMargin from prompt_toolkit.layout.menus import CompletionsMenu, MultiColumnCompletionsMenu from prompt_toolkit.layout.highlighters import SearchHighlighter, SelectionHighlighter, MatchingBracketHighlighter, ConditionalHighlighter from prompt_toolkit.layout.processors import ConditionalProcessor, AppendAutoSuggestion @@ -19,7 +19,6 @@ from prompt_toolkit.layout.utils import token_list_width from prompt_toolkit.reactive import Integer from prompt_toolkit.selection import SelectionType -from prompt_toolkit.utils import get_cwidth from .filters import HasSignature, ShowSidebar, ShowSignature, ShowDocstring from .utils import if_mousedown @@ -250,7 +249,7 @@ def get_tokens(cli): ~IsDone()) -class PromptMargin(Margin): +class PythonPromptMargin(PromptMargin): """ Create margin that displays the prompt. It shows something like "In [1]:". @@ -258,34 +257,17 @@ class PromptMargin(Margin): def __init__(self, python_input): self.python_input = python_input - def _get_prompt_style(self): - return self.python_input.all_prompt_styles[self.python_input.prompt_style] + def get_prompt_style(): + return python_input.all_prompt_styles[python_input.prompt_style] - def get_width(self, cli): - # Take the width from the first line. - text = ''.join(t[1] for t in self.python_input.get_input_prompt_tokens(cli)) - return get_cwidth(text) + def get_prompt(cli): + return get_prompt_style().in_tokens(cli) - def create_margin(self, cli, window_render_info, width, height): - style = self._get_prompt_style() + def get_continuation_prompt(cli, width): + return get_prompt_style().in2_tokens(cli, width) - # First line. - tokens = style.in_tokens(cli) - - # Next lines. (Show line numbering when numbering is enabled.) - tokens2 = style.in2_tokens(cli, width) - show_numbers = self.python_input.show_line_numbers - visible_line_to_input_line = window_render_info.visible_line_to_input_line - - for y in range(1, min(window_render_info.content_height, height)): - tokens.append((Token, '\n')) - if show_numbers: - line_number = visible_line_to_input_line.get(y) or 0 - tokens.append((Token.LineNumber, ('%i ' % (line_number + 1)).rjust(width))) - else: - tokens.extend(tokens2) - - return tokens + super(PythonPromptMargin, self).__init__(get_prompt, get_continuation_prompt, + show_numbers=Condition(lambda cli: python_input.show_line_numbers)) def status_bar(key_bindings_manager, python_input): @@ -518,7 +500,7 @@ def menu_position(cli): # Make sure that we always see the result of an reverse-i-search: preview_search=Always(), ), - left_margins=[PromptMargin(python_input)], + left_margins=[PythonPromptMargin(python_input)], # Scroll offsets. The 1 at the bottom is important to make sure the # cursor is never below the "Press [Meta+Enter]" message which is a float. scroll_offsets=ScrollOffsets(bottom=1, left=4, right=4), From 7af858f6d9e5e9326375596b4fb62727e37d4a9c Mon Sep 17 00:00:00 2001 From: Eran Tiktin Date: Tue, 19 Jan 2016 21:54:44 +0200 Subject: [PATCH 123/470] Update README Fixed some typos and phrasing. --- README.rst | 46 +++++++++++++++++++++------------------------- 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/README.rst b/README.rst index ffcb8a62..2fb646cc 100644 --- a/README.rst +++ b/README.rst @@ -13,7 +13,7 @@ ptpython Ptpython is an advanced Python REPL. It should work on all Python versions from 2.6 up to 3.5 and work cross platform (Linux, -BSD and OS X, Window). +BSD, OS X and Windows). Installation @@ -100,8 +100,8 @@ error. .. image :: https://github.com/jonathanslenders/ptpython/raw/master/docs/images/validation.png -Other features -************** +Additional features +******************* Running system commands: Press ``Meta-!`` in Emacs mode or just ``!`` in Vi navigation mode to see the "Shell command" prompt. There you can enter system @@ -111,7 +111,7 @@ Selecting text: Press ``Control+Space`` in Emacs mode on ``V`` (major V) in Vi navigation mode. -Configurating +Configuration ************* It is possible to create a ``~/.ptpython/config.py`` file to customize the configuration. @@ -120,8 +120,8 @@ Have a look at this example to see what is possible: `config.py `_ -You love IPython? -***************** +IPython support +*************** Run ``ptipython`` (prompt_toolkit - IPython), to get a nice interactive shell with all the power that IPython has to offer, like magic functions and shell @@ -131,8 +131,8 @@ ipython``) .. image :: https://github.com/jonathanslenders/ptpython/raw/master/docs/images/ipython.png -You are using Django? -********************* +Django support +************** `django-extensions `_ has a ``shell_plus`` management command. When ``ptpython`` has been installed, @@ -146,40 +146,36 @@ There is an experimental PDB replacement: `ptpdb `_. -About Windows support -********************* +Windows support +*************** -``prompt_toolkit`` and ``ptpython`` are better tested on Linux and OS X than on -Windows, but it should be usable: +``prompt_toolkit`` and ``ptpython`` works better on Linux and OS X than on +Windows. Some things might not work, but it is usable: .. image :: https://github.com/jonathanslenders/ptpython/raw/master/docs/images/windows.png FAQ ---- +*** -Q - The ``Ctrl-S`` forward search doesn't work and freezes my terminal. -A - Try to run ``stty -ixon`` in your terminal to disable flow control. +**Q**: The ``Ctrl-S`` forward search doesn't work and freezes my terminal. -Q - The ``Meta``-key doesn't work. -A - For some terminals you have to enable the Alt-key to act as meta key, but you - can also type ``Escape`` before any key instead. +**A**: Try to run ``stty -ixon`` in your terminal to disable flow control. + +**Q**: The ``Meta``-key doesn't work. + +**A**: For some terminals you have to enable the Alt-key to act as meta key, but you +can also type ``Escape`` before any key instead. Alternatives ************ -Have a look at the alternatives. - - `BPython `_ If you find another alternative, you can create an issue and we'll list it here. If you find a nice feature somewhere that is missing in ``ptpython``, -also create a GitHub issue and mabye we'll implement it. +also create a GitHub issue and maybe we'll implement it. Special thanks to From c0a99ee2d380440d3cdffd88ecf1763ca932f6c2 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Wed, 20 Jan 2016 22:43:22 +0100 Subject: [PATCH 124/470] Added Pygments to the list of dependencies. (This became optional.) --- setup.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 510e00fa..33ace1f9 100644 --- a/setup.py +++ b/setup.py @@ -19,9 +19,10 @@ long_description=long_description, packages=find_packages('.'), install_requires = [ - 'prompt_toolkit==0.57', - 'jedi>=0.9.0', 'docopt', + 'jedi>=0.9.0', + 'prompt_toolkit==0.57', + 'pygments', ], entry_points={ 'console_scripts': [ From acdd1b20e24ba87e077397d2816851230ec55ab0 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Wed, 20 Jan 2016 23:08:18 +0100 Subject: [PATCH 125/470] Use style_from_dict/pygments instead of deprecated PygmentsStyle. --- ptpython/repl.py | 4 ++-- ptpython/style.py | 13 ++++++------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/ptpython/repl.py b/ptpython/repl.py index b385f387..cc630aaf 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -17,7 +17,7 @@ from prompt_toolkit.interface import AcceptAction, CommandLineInterface from prompt_toolkit.layout.utils import token_list_width from prompt_toolkit.shortcuts import create_asyncio_eventloop -from prompt_toolkit.styles import PygmentsStyle +from prompt_toolkit.styles import style_from_pygments from prompt_toolkit.utils import DummyContext, Callback from .python_input import PythonInput, PythonCommandLineInterface @@ -163,7 +163,7 @@ def _handle_exception(cls, cli, e): # (We use the default style. Most other styles result # in unreadable colors for the traceback.) tokens = _lex_python_traceback(tb) - cli.print_tokens(tokens, style=PygmentsStyle(DefaultStyle)) + cli.print_tokens(tokens, style=style_from_pygments(DefaultStyle)) output.write('%s\n\n' % e) output.flush() diff --git a/ptpython/style.py b/ptpython/style.py index 0eb5b647..0625c91c 100644 --- a/ptpython/style.py +++ b/ptpython/style.py @@ -3,7 +3,7 @@ from pygments.token import Token, Keyword, Name, Comment, String, Operator, Number from pygments.style import Style from pygments.styles import get_style_by_name, get_all_styles -from prompt_toolkit.styles import default_style_extensions, PygmentsStyle +from prompt_toolkit.styles import default_style_extensions, style_from_dict from prompt_toolkit.utils import is_windows, is_conemu_ansi __all__ = ( @@ -40,13 +40,12 @@ def generate_style(python_style, ui_style): assert isinstance(python_style, dict) assert isinstance(ui_style, dict) - class PythonStyle(Style): - styles = {} - styles.update(default_style_extensions) - styles.update(python_style) - styles.update(ui_style) + styles = {} + styles.update(default_style_extensions) + styles.update(python_style) + styles.update(ui_style) - return PygmentsStyle(PythonStyle) + return style_from_dict(styles) # Code style for Windows consoles. They support only 16 colors, From 31a943caa218c8188a842bc4e3b38cf201fd6ebc Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 4 Feb 2016 23:21:30 +0100 Subject: [PATCH 126/470] Upgrade to prompt_toolkit 0.58 --- ptpython/history_browser.py | 51 +++++++++++++++---------------------- ptpython/layout.py | 33 ++++++++++++------------ ptpython/python_input.py | 2 +- ptpython/repl.py | 2 +- ptpython/style.py | 5 ++-- setup.py | 2 +- 6 files changed, 41 insertions(+), 54 deletions(-) diff --git a/ptpython/history_browser.py b/ptpython/history_browser.py index d38b05cc..4d9388fd 100644 --- a/ptpython/history_browser.py +++ b/ptpython/history_browser.py @@ -17,14 +17,13 @@ from prompt_toolkit.layout.containers import HSplit, VSplit, Window, FloatContainer, Float, ConditionalContainer, Container, ScrollOffsets from prompt_toolkit.layout.controls import BufferControl, FillControl from prompt_toolkit.layout.dimension import LayoutDimension as D -from prompt_toolkit.layout.highlighters import SearchHighlighter, SelectionHighlighter from prompt_toolkit.layout.lexers import PygmentsLexer from prompt_toolkit.layout.margins import Margin, ScrollbarMargin -from prompt_toolkit.layout.processors import Processor, Transformation +from prompt_toolkit.layout.processors import Processor, Transformation, HighlightSearchProcessor, HighlightSelectionProcessor from prompt_toolkit.layout.screen import Char from prompt_toolkit.layout.toolbars import ArgToolbar, SearchToolbar from prompt_toolkit.layout.toolbars import TokenListToolbar -from prompt_toolkit.layout.utils import explode_tokens +from prompt_toolkit.layout.utils import token_list_to_text from prompt_toolkit.utils import Callback from pygments.lexers import RstLexer from pygments.token import Token @@ -143,9 +142,9 @@ def create_layout(python_input, history_mapping): Create and return a `Container` instance for the history application. """ - highlighters = [ - SearchHighlighter(preview_search=True), - SelectionHighlighter()] + processors = [ + HighlightSearchProcessor(preview_search=True), + HighlightSelectionProcessor()] help_window = create_popup_window( title='History Help', @@ -154,7 +153,7 @@ def create_layout(python_input, history_mapping): buffer_name=HELP_BUFFER, default_char=Char(token=Token), lexer=PygmentsLexer(RstLexer), - highlighters=highlighters), + input_processors=processors), right_margins=[ScrollbarMargin()], scroll_offsets=ScrollOffsets(top=2, bottom=2))) @@ -170,9 +169,9 @@ def create_layout(python_input, history_mapping): Window( content=BufferControl( buffer_name=HISTORY_BUFFER, - wrap_lines=False, lexer=PygmentsLexer(PythonLexer), - highlighters=highlighters), + input_processors=processors), + wrap_lines=False, left_margins=[HistoryMargin(history_mapping)], scroll_offsets=ScrollOffsets(top=2, bottom=2)), # Separator. @@ -182,10 +181,9 @@ def create_layout(python_input, history_mapping): Window( content=BufferControl( buffer_name=DEFAULT_BUFFER, - wrap_lines=False, - highlighters=highlighters, - input_processors=[GrayExistingText(history_mapping)], + input_processors=processors + [GrayExistingText(history_mapping)], lexer=PygmentsLexer(PythonLexer)), + wrap_lines=False, left_margins=[ResultMargin(history_mapping)], scroll_offsets=ScrollOffsets(top=2, bottom=2)), ]), @@ -244,7 +242,7 @@ class HistoryMargin(Margin): def __init__(self, history_mapping): self.history_mapping = history_mapping - def get_width(self, cli): + def get_width(self, cli, ui_content): return 2 def create_margin(self, cli, window_render_info, width, height): @@ -289,7 +287,7 @@ class ResultMargin(Margin): def __init__(self, history_mapping): self.history_mapping = history_mapping - def get_width(self, cli): + def get_width(self, cli, ui_content): return 2 def create_margin(self, cli, window_render_info, width, height): @@ -328,24 +326,15 @@ class GrayExistingText(Processor): """ def __init__(self, history_mapping): self.history_mapping = history_mapping - self._len_before = len(history_mapping.original_document.text_before_cursor) - self._len_after = len(history_mapping.original_document.text_after_cursor) - - def apply_transformation(self, cli, document, tokens): - if self._len_before or self._len_after: - tokens = explode_tokens(tokens) - pos_after = len(tokens) - self._len_after - - text_before = ''.join(t[1] for t in tokens[:self._len_before]) - text_after = ''.join(t[1] for t in tokens[pos_after:]) - - return Transformation( - document=document, - tokens=explode_tokens([(Token.History.ExistingInput, text_before)] + - tokens[self._len_before:pos_after] + - [(Token.History.ExistingInput, text_after)])) + self._lines_before = len(history_mapping.original_document.text_before_cursor.splitlines()) + + def apply_transformation(self, cli, document, lineno, source_to_display, tokens): + if (lineno < self._lines_before or + lineno >= self._lines_before + len(self.history_mapping.selected_lines)): + text = token_list_to_text(tokens) + return Transformation(tokens=[(Token.History.ExistingInput, text)]) else: - return Transformation(document, tokens) + return Transformation(tokens=tokens) class HistoryMapping(object): diff --git a/ptpython/layout.py b/ptpython/layout.py index 9f71e94f..2c2479ad 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals from prompt_toolkit.enums import DEFAULT_BUFFER, SEARCH_BUFFER -from prompt_toolkit.filters import IsDone, HasCompletions, RendererHeightIsKnown, Always, HasFocus, Condition +from prompt_toolkit.filters import IsDone, HasCompletions, RendererHeightIsKnown, HasFocus, Condition from prompt_toolkit.key_binding.vi_state import InputMode from prompt_toolkit.layout.containers import Window, HSplit, VSplit, FloatContainer, Float, ConditionalContainer, ScrollOffsets from prompt_toolkit.layout.controls import BufferControl, TokenListControl, FillControl @@ -12,8 +12,7 @@ from prompt_toolkit.layout.lexers import SimpleLexer from prompt_toolkit.layout.margins import PromptMargin from prompt_toolkit.layout.menus import CompletionsMenu, MultiColumnCompletionsMenu -from prompt_toolkit.layout.highlighters import SearchHighlighter, SelectionHighlighter, MatchingBracketHighlighter, ConditionalHighlighter -from prompt_toolkit.layout.processors import ConditionalProcessor, AppendAutoSuggestion +from prompt_toolkit.layout.processors import ConditionalProcessor, AppendAutoSuggestion, HighlightSearchProcessor, HighlightSelectionProcessor, HighlightMatchingBracketProcessor from prompt_toolkit.layout.screen import Char from prompt_toolkit.layout.toolbars import CompletionsToolbar, ArgToolbar, SearchToolbar, ValidationToolbar, SystemToolbar, TokenListToolbar from prompt_toolkit.layout.utils import token_list_width @@ -94,7 +93,7 @@ def goto_next(cli, mouse_event): if selected: tokens.append((Token.SetCursorPosition, '')) - tokens.append((token.Status, ' ' * (14 - len(status)), goto_next)) + tokens.append((token.Status, ' ' * (13 - len(status)), goto_next)) tokens.append((T, '<' if selected else '')) tokens.append((T, '\n')) @@ -266,7 +265,7 @@ def get_prompt(cli): def get_continuation_prompt(cli, width): return get_prompt_style().in2_tokens(cli, width) - super(PythonPromptMargin, self).__init__(get_prompt, get_continuation_prompt, + super(PythonPromptMargin, self).__init__(get_prompt, get_continuation_prompt, show_numbers=Condition(lambda cli: python_input.show_line_numbers)) @@ -478,27 +477,25 @@ def menu_position(cli): BufferControl( buffer_name=DEFAULT_BUFFER, lexer=lexer, - highlighters=[ + input_processors=[ + ConditionalProcessor( + processor=HighlightSearchProcessor(preview_search=True), + filter=HasFocus(SEARCH_BUFFER), + ), + HighlightSelectionProcessor(), # Show matching parentheses, but only while editing. - ConditionalHighlighter( - highlighter=MatchingBracketHighlighter(chars='[](){}'), + ConditionalProcessor( + processor=HighlightMatchingBracketProcessor(chars='[](){}'), filter=HasFocus(DEFAULT_BUFFER) & ~IsDone() & Condition(lambda cli: python_input.highlight_matching_parenthesis)), - ConditionalHighlighter( - highlighter=SearchHighlighter(preview_search=Always()), - filter=HasFocus(SEARCH_BUFFER)), - SelectionHighlighter(), - ], - input_processors=[ ConditionalProcessor( processor=AppendAutoSuggestion(), filter=~IsDone()) ] + extra_buffer_processors, menu_position=menu_position, - wrap_lines=Condition(lambda cli: python_input.wrap_lines), # Make sure that we always see the result of an reverse-i-search: - preview_search=Always(), + preview_search=True, ), left_margins=[PythonPromptMargin(python_input)], # Scroll offsets. The 1 at the bottom is important to make sure the @@ -508,6 +505,7 @@ def menu_position(cli): get_height=(lambda cli: ( None if cli.is_done or python_input.show_exit_confirmation else input_buffer_height)), + wrap_lines=Condition(lambda cli: python_input.wrap_lines), ) return HSplit([ @@ -536,7 +534,8 @@ def menu_position(cli): bottom=1, content=exit_confirmation(python_input)), Float(bottom=0, right=0, height=1, - content=meta_enter_message(python_input)), + content=meta_enter_message(python_input), + hide_when_covering_content=True), Float(bottom=1, left=1, right=0, content=python_sidebar_help(python_input)), ]), ArgToolbar(), diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 40a43219..06884c85 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -615,7 +615,7 @@ def run(): cli.eventloop.run_in_executor(run) def on_reset(self, cli): - self.key_bindings_manager.reset() + self.key_bindings_manager.reset(cli) self.signatures = [] def enter_history(self, cli): diff --git a/ptpython/repl.py b/ptpython/repl.py index cc630aaf..9ba77715 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -14,7 +14,7 @@ from prompt_toolkit.application import AbortAction from prompt_toolkit.enums import DEFAULT_BUFFER -from prompt_toolkit.interface import AcceptAction, CommandLineInterface +from prompt_toolkit.interface import AcceptAction from prompt_toolkit.layout.utils import token_list_width from prompt_toolkit.shortcuts import create_asyncio_eventloop from prompt_toolkit.styles import style_from_pygments diff --git a/ptpython/style.py b/ptpython/style.py index 0625c91c..43068498 100644 --- a/ptpython/style.py +++ b/ptpython/style.py @@ -1,9 +1,8 @@ from __future__ import unicode_literals from pygments.token import Token, Keyword, Name, Comment, String, Operator, Number -from pygments.style import Style from pygments.styles import get_style_by_name, get_all_styles -from prompt_toolkit.styles import default_style_extensions, style_from_dict +from prompt_toolkit.styles import DEFAULT_STYLE_EXTENSIONS, style_from_dict from prompt_toolkit.utils import is_windows, is_conemu_ansi __all__ = ( @@ -41,7 +40,7 @@ def generate_style(python_style, ui_style): assert isinstance(ui_style, dict) styles = {} - styles.update(default_style_extensions) + styles.update(DEFAULT_STYLE_EXTENSIONS) styles.update(python_style) styles.update(ui_style) diff --git a/setup.py b/setup.py index 33ace1f9..8ac20360 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ install_requires = [ 'docopt', 'jedi>=0.9.0', - 'prompt_toolkit==0.57', + 'prompt_toolkit==0.58', 'pygments', ], entry_points={ From 8d4259d18473127a8abc916303dfdba45411c8f3 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Wed, 24 Feb 2016 21:29:07 +0100 Subject: [PATCH 127/470] Release 0.29 --- CHANGELOG | 9 +++++++++ setup.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index c35509ac..0d3d1aae 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,15 @@ CHANGELOG ========= +0.29: 2016-02-24 +---------------- + +Upgrade to prompt_toolkit 0.58 + +New features: +- Improved mouse support + + 0.28: 2016-01-04 ---------------- diff --git a/setup.py b/setup.py index 8ac20360..ec725fcc 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ setup( name='ptpython', author='Jonathan Slenders', - version='0.28', + version='0.29', url='https://github.com/jonathanslenders/ptpython', description='Python REPL build on top of prompt_toolkit', long_description=long_description, From c04be9bf8ce28d8b5525a70904160dda82d3ecd9 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sat, 27 Feb 2016 10:59:03 +0100 Subject: [PATCH 128/470] Release 0.30 --- CHANGELOG | 6 ++++++ setup.py | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 0d3d1aae..27a3411d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,12 @@ CHANGELOG ========= +0.30: 2016-02-27 +--------------- + +Upgrade to prompt_toolkit 0.59 + + 0.29: 2016-02-24 ---------------- diff --git a/setup.py b/setup.py index ec725fcc..aff50299 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ setup( name='ptpython', author='Jonathan Slenders', - version='0.29', + version='0.30', url='https://github.com/jonathanslenders/ptpython', description='Python REPL build on top of prompt_toolkit', long_description=long_description, @@ -21,7 +21,7 @@ install_requires = [ 'docopt', 'jedi>=0.9.0', - 'prompt_toolkit==0.58', + 'prompt_toolkit==0.59', 'pygments', ], entry_points={ From 5de029836433b8978ba4b5ed1bc508e4731c76a0 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Mon, 14 Mar 2016 21:22:19 +0100 Subject: [PATCH 129/470] Release 0.31 --- CHANGELOG | 6 ++++++ setup.py | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 27a3411d..efbaa22e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,12 @@ CHANGELOG ========= +0.31: 2016-03-14 +--------------- + +Upgrade to prompt_toolkit 0.60 + + 0.30: 2016-02-27 --------------- diff --git a/setup.py b/setup.py index aff50299..4177fe3b 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ setup( name='ptpython', author='Jonathan Slenders', - version='0.30', + version='0.31', url='https://github.com/jonathanslenders/ptpython', description='Python REPL build on top of prompt_toolkit', long_description=long_description, @@ -21,7 +21,7 @@ install_requires = [ 'docopt', 'jedi>=0.9.0', - 'prompt_toolkit==0.59', + 'prompt_toolkit==0.60', 'pygments', ], entry_points={ From de080f84d4ea03f1d1a362737554e53a145ce33a Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 24 Mar 2016 20:48:45 +0100 Subject: [PATCH 130/470] Fixed bug when PYTHONSTARTUP was not found. (Reference to PythonRepl.cli.) --- ptpython/repl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ptpython/repl.py b/ptpython/repl.py index 9ba77715..04aabb04 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -61,7 +61,7 @@ def _on_start(self, cli): code = compile(f.read(), path, 'exec') six.exec_(code, self.get_globals(), self.get_locals()) else: - output = self.cli.output + output = cli.output output.write('WARNING | File not found: {}\n\n'.format(path)) def _process_document(self, cli, buffer): From bdd69773f0913bd5a771e66ca957002b94bf473f Mon Sep 17 00:00:00 2001 From: Timo Furrer Date: Sun, 27 Mar 2016 16:42:03 -0700 Subject: [PATCH 131/470] Support $PYTHONSTARTUP for ptipython --- ptpython/entry_points/run_ptipython.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/ptpython/entry_points/run_ptipython.py b/ptpython/entry_points/run_ptipython.py index 8fbba170..1def5fc2 100644 --- a/ptpython/entry_points/run_ptipython.py +++ b/ptpython/entry_points/run_ptipython.py @@ -57,11 +57,18 @@ def run(): # the IPython shell.) user_ns = {} + # Startup path + startup_paths = [] + if 'PYTHONSTARTUP' in os.environ: + startup_paths.append(os.environ['PYTHONSTARTUP']) + # --interactive if a['--interactive']: - path = a['--interactive'] + startup_paths.append(a['--interactive']) sys.argv = [a['--interactive']] + a[''] + # exec scripts from startup paths + for path in startup_paths: if os.path.exists(path): with open(path, 'r') as f: code = compile(f.read(), path, 'exec') From a3182d75daa3cd4cc5aa03e33aa1386e25863998 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 29 Mar 2016 21:04:04 +0200 Subject: [PATCH 132/470] Release 0.32 --- CHANGELOG | 8 ++++++++ setup.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index efbaa22e..102c48fa 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,14 @@ CHANGELOG ========= +0.32: 2016-03-29 +--------------- + +Fixes: +- Fixed bug when PYTHONSTARTUP was not found. +- Support $PYTHONSTARTUP for ptipython. + + 0.31: 2016-03-14 --------------- diff --git a/setup.py b/setup.py index 4177fe3b..edba667b 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ setup( name='ptpython', author='Jonathan Slenders', - version='0.31', + version='0.32', url='https://github.com/jonathanslenders/ptpython', description='Python REPL build on top of prompt_toolkit', long_description=long_description, From 3020ed8c15caf3bb85df4dfb7cfa77cb4921310a Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Wed, 6 Apr 2016 22:46:25 +0200 Subject: [PATCH 133/470] Added IPython to alternatives. --- README.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/README.rst b/README.rst index 2fb646cc..f5752004 100644 --- a/README.rst +++ b/README.rst @@ -172,6 +172,7 @@ Alternatives ************ - `BPython `_ +- `IPython `_ If you find another alternative, you can create an issue and we'll list it here. If you find a nice feature somewhere that is missing in ``ptpython``, From 05e85916bbcf63641ce6e9e47f50f1f54453e780 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Fri, 15 Apr 2016 10:12:25 +0200 Subject: [PATCH 134/470] Make it configurable whether or not a blank line has to be inserted after the output. --- examples/ptpython_config/config.py | 3 +++ ptpython/python_input.py | 4 ++++ ptpython/repl.py | 6 ++++-- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/examples/ptpython_config/config.py b/examples/ptpython_config/config.py index 84e3f5f5..1e8f77d7 100644 --- a/examples/ptpython_config/config.py +++ b/examples/ptpython_config/config.py @@ -67,6 +67,9 @@ def configure(repl): # Use the classic prompt. (Display '>>>' instead of 'In [1]'.) repl.prompt_style = 'classic' # 'classic' or 'ipython' + # Don't insert a blank line after the output. + self.insert_blank_line_after_output = False + # History Search. # When True, going back in history will filter the history on the records # starting with the current input. (Like readline.) diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 06884c85..80757891 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -188,6 +188,7 @@ def __init__(self, self.show_exit_confirmation = False # Currently show 'Do you really want to exit?' self.terminal_title = None # The title to be displayed in the terminal. (None or string.) self.exit_message = 'Do you really want to exit?' + self.insert_blank_line_after_output = True # (For the REPL.) # Tokens to be shown at the prompt. self.prompt_style = 'classic' # The currently active style. @@ -444,6 +445,9 @@ def get_values(): description="Visualisation of the prompt. ('>>>' or 'In [1]:')", get_current_value=lambda: self.prompt_style, get_values=lambda: dict((s, partial(enable, 'prompt_style', s)) for s in self.all_prompt_styles)), + simple_option(title='Blank line after output', + description='Insert a blank line after the output.', + field_name='insert_blank_line_after_output'), simple_option(title='Show signature', description='Display function signatures.', field_name='show_signature'), diff --git a/ptpython/repl.py b/ptpython/repl.py index 04aabb04..451b82ad 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -76,6 +76,9 @@ def _process_document(self, cli, buffer): except Exception as e: self._handle_exception(cli, e) + if self.insert_blank_line_after_output: + cli.output.write('\n') + self.current_statement_index += 1 self.signatures = [] @@ -136,7 +139,6 @@ def compile_with_flags(code, mode): code = compile_with_flags(line, 'exec') six.exec_(code, self.get_globals(), self.get_locals()) - output.write('\n') output.flush() @classmethod @@ -165,7 +167,7 @@ def _handle_exception(cls, cli, e): tokens = _lex_python_traceback(tb) cli.print_tokens(tokens, style=style_from_pygments(DefaultStyle)) - output.write('%s\n\n' % e) + output.write('%s\n' % e) output.flush() @classmethod From 29ef64f52485f98f3e57d79075f4ed01b353e620 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Fri, 15 Apr 2016 10:33:16 +0200 Subject: [PATCH 135/470] Use CLI.vi_state. --- ptpython/history_browser.py | 3 +-- ptpython/key_bindings.py | 3 +-- ptpython/layout.py | 2 +- ptpython/python_input.py | 4 ++-- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/ptpython/history_browser.py b/ptpython/history_browser.py index 4d9388fd..b4dcea74 100644 --- a/ptpython/history_browser.py +++ b/ptpython/history_browser.py @@ -424,8 +424,7 @@ def create_key_bindings(python_input, history_mapping): manager = KeyBindingManager( enable_search=True, enable_vi_mode=Condition(lambda cli: python_input.vi_mode), - enable_extra_page_navigation=True, - get_vi_state=python_input.key_bindings_manager.get_vi_state) + enable_extra_page_navigation=True) handle = manager.registry.add_binding @handle(' ', filter=HasFocus(HISTORY_BUFFER)) diff --git a/ptpython/key_bindings.py b/ptpython/key_bindings.py index a5a89172..81bd66f4 100644 --- a/ptpython/key_bindings.py +++ b/ptpython/key_bindings.py @@ -82,8 +82,7 @@ def _(event): event.cli.current_buffer.insert_text(' ') @handle(Keys.ControlJ, filter= ~sidebar_visible & ~has_selection & - ~(vi_mode_enabled & - ViStateFilter(key_bindings_manager.get_vi_state, InputMode.NAVIGATION)) & + ~(vi_mode_enabled & ViStateFilter(InputMode.NAVIGATION)) & HasFocus(DEFAULT_BUFFER) & IsMultiline()) def _(event): """ diff --git a/ptpython/layout.py b/ptpython/layout.py index 2c2479ad..b1329f31 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -339,7 +339,7 @@ def toggle_vi_mode(cli, mouse_event): token = Token.Toolbar.Status - mode = python_input.key_bindings_manager.get_vi_state(cli).input_mode + mode = cli.vi_state.input_mode result = [] append = result.append diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 80757891..f5d587a0 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -626,13 +626,13 @@ def enter_history(self, cli): """ Display the history. """ - self.key_bindings_manager.get_vi_state(cli).input_mode = InputMode.NAVIGATION + cli.vi_state.input_mode = InputMode.NAVIGATION def done(result): if result is not None: cli.buffers[DEFAULT_BUFFER].document = result - self.key_bindings_manager.get_vi_state(cli).input_mode = InputMode.INSERT + cli.vi_state.input_mode = InputMode.INSERT cli.run_sub_application(create_history_application( self, cli.buffers[DEFAULT_BUFFER].document), done) From 5fc384853f8b70a61e3584bfd899519fcd172505 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Fri, 15 Apr 2016 11:30:42 +0200 Subject: [PATCH 136/470] Use ViMode instead of ViStateFilter. --- ptpython/key_bindings.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/ptpython/key_bindings.py b/ptpython/key_bindings.py index 81bd66f4..aa9624f5 100644 --- a/ptpython/key_bindings.py +++ b/ptpython/key_bindings.py @@ -2,8 +2,7 @@ from prompt_toolkit.document import Document from prompt_toolkit.enums import DEFAULT_BUFFER -from prompt_toolkit.filters import HasSelection, IsMultiline, Filter, HasFocus, Condition -from prompt_toolkit.key_binding.bindings.vi import ViStateFilter +from prompt_toolkit.filters import HasSelection, IsMultiline, Filter, HasFocus, Condition, ViMode from prompt_toolkit.key_binding.vi_state import InputMode from prompt_toolkit.keys import Keys @@ -82,7 +81,7 @@ def _(event): event.cli.current_buffer.insert_text(' ') @handle(Keys.ControlJ, filter= ~sidebar_visible & ~has_selection & - ~(vi_mode_enabled & ViStateFilter(InputMode.NAVIGATION)) & + ~(vi_mode_enabled & ViMode(InputMode.NAVIGATION)) & HasFocus(DEFAULT_BUFFER) & IsMultiline()) def _(event): """ From 9072d244e79208515402023cbf8ded0554e64a19 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sat, 16 Apr 2016 09:01:15 +0200 Subject: [PATCH 137/470] Fixed bug in example config. "self is not defined." --- examples/ptpython_config/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/ptpython_config/config.py b/examples/ptpython_config/config.py index 1e8f77d7..9f337547 100644 --- a/examples/ptpython_config/config.py +++ b/examples/ptpython_config/config.py @@ -68,7 +68,7 @@ def configure(repl): repl.prompt_style = 'classic' # 'classic' or 'ipython' # Don't insert a blank line after the output. - self.insert_blank_line_after_output = False + repl.insert_blank_line_after_output = False # History Search. # When True, going back in history will filter the history on the records From c7887237d1dab7415495619029509c9253a0eb6a Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Fri, 22 Apr 2016 21:28:24 +0200 Subject: [PATCH 138/470] Use InVimode filter. --- ptpython/history_browser.py | 1 - ptpython/key_bindings.py | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/ptpython/history_browser.py b/ptpython/history_browser.py index b4dcea74..925262ec 100644 --- a/ptpython/history_browser.py +++ b/ptpython/history_browser.py @@ -423,7 +423,6 @@ def create_key_bindings(python_input, history_mapping): """ manager = KeyBindingManager( enable_search=True, - enable_vi_mode=Condition(lambda cli: python_input.vi_mode), enable_extra_page_navigation=True) handle = manager.registry.add_binding diff --git a/ptpython/key_bindings.py b/ptpython/key_bindings.py index aa9624f5..1c26dcd9 100644 --- a/ptpython/key_bindings.py +++ b/ptpython/key_bindings.py @@ -2,7 +2,7 @@ from prompt_toolkit.document import Document from prompt_toolkit.enums import DEFAULT_BUFFER -from prompt_toolkit.filters import HasSelection, IsMultiline, Filter, HasFocus, Condition, ViMode +from prompt_toolkit.filters import HasSelection, IsMultiline, Filter, HasFocus, Condition, InViMode from prompt_toolkit.key_binding.vi_state import InputMode from prompt_toolkit.keys import Keys @@ -81,7 +81,7 @@ def _(event): event.cli.current_buffer.insert_text(' ') @handle(Keys.ControlJ, filter= ~sidebar_visible & ~has_selection & - ~(vi_mode_enabled & ViMode(InputMode.NAVIGATION)) & + ~(vi_mode_enabled & InViMode(InputMode.NAVIGATION)) & HasFocus(DEFAULT_BUFFER) & IsMultiline()) def _(event): """ From dcadaca0c58fa0afccd9689572f58e90e0311093 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sat, 30 Apr 2016 13:11:12 +0200 Subject: [PATCH 139/470] Modifications related to the latest prompt-toolkit key binding refactoring. --- ptpython/history_browser.py | 7 +++- ptpython/key_bindings.py | 27 +++++++++---- ptpython/layout.py | 13 ++++--- ptpython/python_input.py | 77 +++++++++++++++++++------------------ 4 files changed, 73 insertions(+), 51 deletions(-) diff --git a/ptpython/history_browser.py b/ptpython/history_browser.py index 925262ec..501c55e1 100644 --- a/ptpython/history_browser.py +++ b/ptpython/history_browser.py @@ -10,7 +10,7 @@ from prompt_toolkit.buffer import Buffer, AcceptAction from prompt_toolkit.buffer_mapping import BufferMapping from prompt_toolkit.document import Document -from prompt_toolkit.enums import DEFAULT_BUFFER +from prompt_toolkit.enums import DEFAULT_BUFFER, EditingMode from prompt_toolkit.filters import Condition, HasFocus, InFocusStack from prompt_toolkit.key_binding.manager import KeyBindingManager from prompt_toolkit.keys import Keys @@ -488,7 +488,10 @@ def _(event): @handle(Keys.F4) def _(event): " Switch between Emacs/Vi mode. " - python_input.vi_mode = not python_input.vi_mode + if event.cli.editing_mode == EditingMode.VI: + event.cli.editing_mode = EditingMode.Emacs + else: + event.cli.editing_mode = EditingMode.VI @handle(Keys.F1) def _(event): diff --git a/ptpython/key_bindings.py b/ptpython/key_bindings.py index 1c26dcd9..6946859a 100644 --- a/ptpython/key_bindings.py +++ b/ptpython/key_bindings.py @@ -1,9 +1,8 @@ from __future__ import unicode_literals from prompt_toolkit.document import Document -from prompt_toolkit.enums import DEFAULT_BUFFER -from prompt_toolkit.filters import HasSelection, IsMultiline, Filter, HasFocus, Condition, InViMode -from prompt_toolkit.key_binding.vi_state import InputMode +from prompt_toolkit.enums import DEFAULT_BUFFER, EditingMode +from prompt_toolkit.filters import HasSelection, IsMultiline, Filter, HasFocus, Condition, ViInsertMode, EmacsInsertMode from prompt_toolkit.keys import Keys __all__ = ( @@ -64,7 +63,11 @@ def _(event): """ Toggle between Vi and Emacs mode. """ - python_input.vi_mode = not python_input.vi_mode + if event.cli.editing_mode == EditingMode.VI: + event.cli.editing_mode = EditingMode.EMACS + else: + event.cli.editing_mode = EditingMode.VI + @handle(Keys.F6) def _(event): @@ -81,7 +84,7 @@ def _(event): event.cli.current_buffer.insert_text(' ') @handle(Keys.ControlJ, filter= ~sidebar_visible & ~has_selection & - ~(vi_mode_enabled & InViMode(InputMode.NAVIGATION)) & + (ViInsertMode() | EmacsInsertMode()) & HasFocus(DEFAULT_BUFFER) & IsMultiline()) def _(event): """ @@ -116,6 +119,16 @@ def at_the_end(b): else: auto_newline(b) + @handle(Keys.ControlBackslash, filter= ~sidebar_visible & ~has_selection & + (ViInsertMode() | EmacsInsertMode()) & + HasFocus(DEFAULT_BUFFER)) + def _(event): + r""" + Always insert a newline when Control+\ has been pressed. + """ + b = event.current_buffer + b.insert_text('\n') + @handle(Keys.ControlD, filter=~sidebar_visible & Condition(lambda cli: # Only when the `confirm_exit` flag is set. python_input.confirm_exit and @@ -158,14 +171,14 @@ def _(event): def _(event): " Select next value for current option. " option = python_input.selected_option - option.activate_next() + option.activate_next(event.cli) @handle(Keys.Left, filter=sidebar_visible) @handle('h', filter=sidebar_visible) def _(event): " Select previous value for current option. " option = python_input.selected_option - option.activate_previous() + option.activate_previous(event.cli) @handle(Keys.ControlC, filter=sidebar_visible) @handle(Keys.ControlG, filter=sidebar_visible) diff --git a/ptpython/layout.py b/ptpython/layout.py index b1329f31..b2780217 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -3,7 +3,7 @@ """ from __future__ import unicode_literals -from prompt_toolkit.enums import DEFAULT_BUFFER, SEARCH_BUFFER +from prompt_toolkit.enums import DEFAULT_BUFFER, SEARCH_BUFFER, EditingMode from prompt_toolkit.filters import IsDone, HasCompletions, RendererHeightIsKnown, HasFocus, Condition from prompt_toolkit.key_binding.vi_state import InputMode from prompt_toolkit.layout.containers import Window, HSplit, VSplit, FloatContainer, Float, ConditionalContainer, ScrollOffsets @@ -298,9 +298,9 @@ def get_tokens(cli): len(python_buffer._working_lines)))) # Shortcuts. - if not python_input.vi_mode and cli.current_buffer_name == SEARCH_BUFFER: + if cli.editing_mode == EditingMode.EMACS and cli.current_buffer_name == SEARCH_BUFFER: append((TB, '[Ctrl-G] Cancel search [Enter] Go to this position.')) - elif bool(cli.current_buffer.selection_state) and not python_input.vi_mode: + elif bool(cli.current_buffer.selection_state) and cli.editing_mode == EditingMode.EMACS: # Emacs cut/copy keys. append((TB, '[Ctrl-W] Cut [Meta-W] Copy [Ctrl-Y] Paste [Ctrl-G] Cancel')) else: @@ -335,7 +335,10 @@ def get_inputmode_tokens(cli, python_input): """ @if_mousedown def toggle_vi_mode(cli, mouse_event): - python_input.vi_mode = not python_input.vi_mode + if cli.editing_mode == EditingMode.VI: + cli.editing_mode = EditingMode.Emacs + else: + cli.editing_mode = EditingMode.VI token = Token.Toolbar.Status @@ -346,7 +349,7 @@ def toggle_vi_mode(cli, mouse_event): append((token.InputMode, '[F4] ', toggle_vi_mode)) # InputMode - if python_input.vi_mode: + if cli.editing_mode == EditingMode.VI: if bool(cli.current_buffer.selection_state): if cli.current_buffer.selection_state.type == SelectionType.LINES: append((token.InputMode, 'Vi (VISUAL LINE)', toggle_vi_mode)) diff --git a/ptpython/python_input.py b/ptpython/python_input.py index f5d587a0..18aa75b2 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -13,7 +13,7 @@ from prompt_toolkit.auto_suggest import AutoSuggestFromHistory, ConditionalAutoSuggest from prompt_toolkit.buffer import Buffer from prompt_toolkit.document import Document -from prompt_toolkit.enums import DEFAULT_BUFFER +from prompt_toolkit.enums import DEFAULT_BUFFER, EditingMode from prompt_toolkit.filters import Condition, Always from prompt_toolkit.history import FileHistory, InMemoryHistory from prompt_toolkit.interface import CommandLineInterface, Application, AcceptAction @@ -85,11 +85,11 @@ def __init__(self, title, description, get_current_value, get_values): def values(self): return self.get_values() - def activate_next(self, _previous=False): + def activate_next(self, cli, _previous=False): """ Activate next value. """ - current = self.get_current_value() + current = self.get_current_value(cli) options = sorted(self.values.keys()) # Get current index. @@ -106,13 +106,13 @@ def activate_next(self, _previous=False): # Call handler for this option. next_option = options[index % len(options)] - self.values[next_option]() + self.values[next_option](cli) - def activate_previous(self): + def activate_previous(self, cli): """ Activate previous value. """ - self.activate_next(_previous=True) + self.activate_next(cli, _previous=True) class PythonInput(object): @@ -167,7 +167,7 @@ def __init__(self, self.show_status_bar = True self.wrap_lines = True self.complete_while_typing = True - self.vi_mode = vi_mode + self._vi_mode = vi_mode self.paste_mode = False # When True, don't insert whitespace after newline. self.confirm_exit = True # Ask for confirmation when Control-D is pressed. self.accept_input_on_enter = 2 # Accept when pressing Enter 'n' times. @@ -230,7 +230,6 @@ def __init__(self, self.key_bindings_manager = KeyBindingManager( enable_abort_and_exit_bindings=True, enable_search=True, - enable_vi_mode=Condition(lambda cli: self.vi_mode), enable_open_in_editor=Condition(lambda cli: self.enable_open_in_editor), enable_system_bindings=Condition(lambda cli: self.enable_system_bindings), enable_auto_suggest_bindings=Condition(lambda cli: self.enable_auto_suggest), @@ -367,13 +366,13 @@ def simple_option(title, description, field_name, values=None): " Create Simple on/of option. " values = values or ['off', 'on'] - def get_current_value(): + def get_current_value(cli): return values[bool(getattr(self, field_name))] def get_values(): return { - values[1]: lambda: enable(field_name), - values[0]: lambda: disable(field_name), + values[1]: lambda _: enable(field_name), + values[0]: lambda _: disable(field_name), } return Option(title=title, description=description, @@ -382,28 +381,31 @@ def get_values(): return [ OptionCategory('Input', [ - simple_option(title='Input mode', - description='Vi or emacs key bindings.', - field_name='vi_mode', - values=['emacs', 'vi']), + Option(title='Input mode', + description='Vi or emacs key bindings.', + get_current_value=lambda cli: cli.editing_mode, + get_values=lambda: { + 'vi': lambda cli: setattr(cli, 'editing_mode', EditingMode.Vi), + 'emacs': lambda cli: setattr(cli, 'editing_mode', EditingMode.Emacs), + }), simple_option(title='Paste mode', description="When enabled, don't indent automatically.", field_name='paste_mode'), Option(title='Complete while typing', description="Generate autocompletions automatically while typing. " 'Don\'t require pressing TAB. (Not compatible with "History search".)', - get_current_value=lambda: ['off', 'on'][self.complete_while_typing], + get_current_value=lambda cli: ['off', 'on'][self.complete_while_typing], get_values=lambda: { - 'on': lambda: enable('complete_while_typing') and disable('enable_history_search'), - 'off': lambda: disable('complete_while_typing'), + 'on': lambda _: enable('complete_while_typing') and disable('enable_history_search'), + 'off': lambda _: disable('complete_while_typing'), }), Option(title='History search', description='When pressing the up-arrow, filter the history on input starting ' 'with the current text. (Not compatible with "Complete while typing".)', - get_current_value=lambda: ['off', 'on'][self.enable_history_search], + get_current_value=lambda cli: ['off', 'on'][self.enable_history_search], get_values=lambda: { - 'on': lambda: enable('enable_history_search') and disable('complete_while_typing'), - 'off': lambda: disable('enable_history_search'), + 'on': lambda _: enable('enable_history_search') and disable('complete_while_typing'), + 'off': lambda _: disable('enable_history_search'), }), simple_option(title='Mouse support', description='Respond to mouse clicks and scrolling for positioning the cursor, ' @@ -423,28 +425,28 @@ def get_values(): Option(title='Accept input on enter', description='Amount of ENTER presses required to execute input when the cursor ' 'is at the end of the input. (Note that META+ENTER will always execute.)', - get_current_value=lambda: str(self.accept_input_on_enter or 'meta-enter'), + get_current_value=lambda cli: str(self.accept_input_on_enter or 'meta-enter'), get_values=lambda: { - '2': lambda: enable('accept_input_on_enter', 2), - '3': lambda: enable('accept_input_on_enter', 3), - '4': lambda: enable('accept_input_on_enter', 4), - 'meta-enter': lambda: enable('accept_input_on_enter', None), + '2': lambda _: enable('accept_input_on_enter', 2), + '3': lambda _: enable('accept_input_on_enter', 3), + '4': lambda _: enable('accept_input_on_enter', 4), + 'meta-enter': lambda _: enable('accept_input_on_enter', None), }), ]), OptionCategory('Display', [ Option(title='Completions', description='Visualisation to use for displaying the completions. (Multiple columns, one column, a toolbar or nothing.)', - get_current_value=lambda: self.completion_visualisation, + get_current_value=lambda cli: self.completion_visualisation, get_values=lambda: { - CompletionVisualisation.NONE: lambda: enable('completion_visualisation', CompletionVisualisation.NONE), - CompletionVisualisation.POP_UP: lambda: enable('completion_visualisation', CompletionVisualisation.POP_UP), - CompletionVisualisation.MULTI_COLUMN: lambda: enable('completion_visualisation', CompletionVisualisation.MULTI_COLUMN), - CompletionVisualisation.TOOLBAR: lambda: enable('completion_visualisation', CompletionVisualisation.TOOLBAR), + CompletionVisualisation.NONE: lambda _: enable('completion_visualisation', CompletionVisualisation.NONE), + CompletionVisualisation.POP_UP: lambda _: enable('completion_visualisation', CompletionVisualisation.POP_UP), + CompletionVisualisation.MULTI_COLUMN: lambda _: enable('completion_visualisation', CompletionVisualisation.MULTI_COLUMN), + CompletionVisualisation.TOOLBAR: lambda _: enable('completion_visualisation', CompletionVisualisation.TOOLBAR), }), Option(title='Prompt', description="Visualisation of the prompt. ('>>>' or 'In [1]:')", - get_current_value=lambda: self.prompt_style, - get_values=lambda: dict((s, partial(enable, 'prompt_style', s)) for s in self.all_prompt_styles)), + get_current_value=lambda cli: self.prompt_style, + get_values=lambda: dict((s, lambda _, s=s: enable('prompt_style', s)) for s in self.all_prompt_styles)), simple_option(title='Blank line after output', description='Insert a blank line after the output.', field_name='insert_blank_line_after_output'), @@ -477,15 +479,15 @@ def get_values(): OptionCategory('Colors', [ Option(title='Code', description='Color scheme to use for the Python code.', - get_current_value=lambda: self._current_code_style_name, + get_current_value=lambda cli: self._current_code_style_name, get_values=lambda: dict( - (name, partial(self.use_code_colorscheme, name)) for name in self.code_styles) + (name, lambda _, name=name: self.use_code_colorscheme(name)) for name in self.code_styles) ), Option(title='User interface', description='Color scheme to use for the user interface.', - get_current_value=lambda: self._current_ui_style_name, + get_current_value=lambda cli: self._current_ui_style_name, get_values=lambda: dict( - (name, partial(self.use_ui_colorscheme, name)) for name in self.ui_styles) + (name, lambda _, name=name: self.use_ui_colorscheme(name)) for name in self.ui_styles) ), simple_option(title='True color (24 bit)', description='Use 24 bit colors instead of 265 colors', @@ -511,6 +513,7 @@ def create_application(self): extra_buffer_processors=self._extra_buffer_processors, extra_body=self._extra_layout_body, extra_toolbars=self._extra_toolbars), + editing_mode=(EditingMode.VI if self._vi_mode else EditingMode.EMACS), buffer=self._create_buffer(), buffers=buffers, key_bindings_registry=self.key_bindings_registry, From b10777c49f5b67ce7ccc7423aed61df015a55e43 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sun, 1 May 2016 15:03:47 +0200 Subject: [PATCH 140/470] Revert "Modifications related to the latest prompt-toolkit key binding refactoring." (Going to try another approach: synchronizing key bindings.) This reverts commit dcadaca0c58fa0afccd9689572f58e90e0311093. --- ptpython/history_browser.py | 7 +--- ptpython/key_bindings.py | 27 ++++--------- ptpython/layout.py | 13 +++---- ptpython/python_input.py | 77 ++++++++++++++++++------------------- 4 files changed, 51 insertions(+), 73 deletions(-) diff --git a/ptpython/history_browser.py b/ptpython/history_browser.py index 501c55e1..925262ec 100644 --- a/ptpython/history_browser.py +++ b/ptpython/history_browser.py @@ -10,7 +10,7 @@ from prompt_toolkit.buffer import Buffer, AcceptAction from prompt_toolkit.buffer_mapping import BufferMapping from prompt_toolkit.document import Document -from prompt_toolkit.enums import DEFAULT_BUFFER, EditingMode +from prompt_toolkit.enums import DEFAULT_BUFFER from prompt_toolkit.filters import Condition, HasFocus, InFocusStack from prompt_toolkit.key_binding.manager import KeyBindingManager from prompt_toolkit.keys import Keys @@ -488,10 +488,7 @@ def _(event): @handle(Keys.F4) def _(event): " Switch between Emacs/Vi mode. " - if event.cli.editing_mode == EditingMode.VI: - event.cli.editing_mode = EditingMode.Emacs - else: - event.cli.editing_mode = EditingMode.VI + python_input.vi_mode = not python_input.vi_mode @handle(Keys.F1) def _(event): diff --git a/ptpython/key_bindings.py b/ptpython/key_bindings.py index 6946859a..1c26dcd9 100644 --- a/ptpython/key_bindings.py +++ b/ptpython/key_bindings.py @@ -1,8 +1,9 @@ from __future__ import unicode_literals from prompt_toolkit.document import Document -from prompt_toolkit.enums import DEFAULT_BUFFER, EditingMode -from prompt_toolkit.filters import HasSelection, IsMultiline, Filter, HasFocus, Condition, ViInsertMode, EmacsInsertMode +from prompt_toolkit.enums import DEFAULT_BUFFER +from prompt_toolkit.filters import HasSelection, IsMultiline, Filter, HasFocus, Condition, InViMode +from prompt_toolkit.key_binding.vi_state import InputMode from prompt_toolkit.keys import Keys __all__ = ( @@ -63,11 +64,7 @@ def _(event): """ Toggle between Vi and Emacs mode. """ - if event.cli.editing_mode == EditingMode.VI: - event.cli.editing_mode = EditingMode.EMACS - else: - event.cli.editing_mode = EditingMode.VI - + python_input.vi_mode = not python_input.vi_mode @handle(Keys.F6) def _(event): @@ -84,7 +81,7 @@ def _(event): event.cli.current_buffer.insert_text(' ') @handle(Keys.ControlJ, filter= ~sidebar_visible & ~has_selection & - (ViInsertMode() | EmacsInsertMode()) & + ~(vi_mode_enabled & InViMode(InputMode.NAVIGATION)) & HasFocus(DEFAULT_BUFFER) & IsMultiline()) def _(event): """ @@ -119,16 +116,6 @@ def at_the_end(b): else: auto_newline(b) - @handle(Keys.ControlBackslash, filter= ~sidebar_visible & ~has_selection & - (ViInsertMode() | EmacsInsertMode()) & - HasFocus(DEFAULT_BUFFER)) - def _(event): - r""" - Always insert a newline when Control+\ has been pressed. - """ - b = event.current_buffer - b.insert_text('\n') - @handle(Keys.ControlD, filter=~sidebar_visible & Condition(lambda cli: # Only when the `confirm_exit` flag is set. python_input.confirm_exit and @@ -171,14 +158,14 @@ def _(event): def _(event): " Select next value for current option. " option = python_input.selected_option - option.activate_next(event.cli) + option.activate_next() @handle(Keys.Left, filter=sidebar_visible) @handle('h', filter=sidebar_visible) def _(event): " Select previous value for current option. " option = python_input.selected_option - option.activate_previous(event.cli) + option.activate_previous() @handle(Keys.ControlC, filter=sidebar_visible) @handle(Keys.ControlG, filter=sidebar_visible) diff --git a/ptpython/layout.py b/ptpython/layout.py index b2780217..b1329f31 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -3,7 +3,7 @@ """ from __future__ import unicode_literals -from prompt_toolkit.enums import DEFAULT_BUFFER, SEARCH_BUFFER, EditingMode +from prompt_toolkit.enums import DEFAULT_BUFFER, SEARCH_BUFFER from prompt_toolkit.filters import IsDone, HasCompletions, RendererHeightIsKnown, HasFocus, Condition from prompt_toolkit.key_binding.vi_state import InputMode from prompt_toolkit.layout.containers import Window, HSplit, VSplit, FloatContainer, Float, ConditionalContainer, ScrollOffsets @@ -298,9 +298,9 @@ def get_tokens(cli): len(python_buffer._working_lines)))) # Shortcuts. - if cli.editing_mode == EditingMode.EMACS and cli.current_buffer_name == SEARCH_BUFFER: + if not python_input.vi_mode and cli.current_buffer_name == SEARCH_BUFFER: append((TB, '[Ctrl-G] Cancel search [Enter] Go to this position.')) - elif bool(cli.current_buffer.selection_state) and cli.editing_mode == EditingMode.EMACS: + elif bool(cli.current_buffer.selection_state) and not python_input.vi_mode: # Emacs cut/copy keys. append((TB, '[Ctrl-W] Cut [Meta-W] Copy [Ctrl-Y] Paste [Ctrl-G] Cancel')) else: @@ -335,10 +335,7 @@ def get_inputmode_tokens(cli, python_input): """ @if_mousedown def toggle_vi_mode(cli, mouse_event): - if cli.editing_mode == EditingMode.VI: - cli.editing_mode = EditingMode.Emacs - else: - cli.editing_mode = EditingMode.VI + python_input.vi_mode = not python_input.vi_mode token = Token.Toolbar.Status @@ -349,7 +346,7 @@ def toggle_vi_mode(cli, mouse_event): append((token.InputMode, '[F4] ', toggle_vi_mode)) # InputMode - if cli.editing_mode == EditingMode.VI: + if python_input.vi_mode: if bool(cli.current_buffer.selection_state): if cli.current_buffer.selection_state.type == SelectionType.LINES: append((token.InputMode, 'Vi (VISUAL LINE)', toggle_vi_mode)) diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 18aa75b2..f5d587a0 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -13,7 +13,7 @@ from prompt_toolkit.auto_suggest import AutoSuggestFromHistory, ConditionalAutoSuggest from prompt_toolkit.buffer import Buffer from prompt_toolkit.document import Document -from prompt_toolkit.enums import DEFAULT_BUFFER, EditingMode +from prompt_toolkit.enums import DEFAULT_BUFFER from prompt_toolkit.filters import Condition, Always from prompt_toolkit.history import FileHistory, InMemoryHistory from prompt_toolkit.interface import CommandLineInterface, Application, AcceptAction @@ -85,11 +85,11 @@ def __init__(self, title, description, get_current_value, get_values): def values(self): return self.get_values() - def activate_next(self, cli, _previous=False): + def activate_next(self, _previous=False): """ Activate next value. """ - current = self.get_current_value(cli) + current = self.get_current_value() options = sorted(self.values.keys()) # Get current index. @@ -106,13 +106,13 @@ def activate_next(self, cli, _previous=False): # Call handler for this option. next_option = options[index % len(options)] - self.values[next_option](cli) + self.values[next_option]() - def activate_previous(self, cli): + def activate_previous(self): """ Activate previous value. """ - self.activate_next(cli, _previous=True) + self.activate_next(_previous=True) class PythonInput(object): @@ -167,7 +167,7 @@ def __init__(self, self.show_status_bar = True self.wrap_lines = True self.complete_while_typing = True - self._vi_mode = vi_mode + self.vi_mode = vi_mode self.paste_mode = False # When True, don't insert whitespace after newline. self.confirm_exit = True # Ask for confirmation when Control-D is pressed. self.accept_input_on_enter = 2 # Accept when pressing Enter 'n' times. @@ -230,6 +230,7 @@ def __init__(self, self.key_bindings_manager = KeyBindingManager( enable_abort_and_exit_bindings=True, enable_search=True, + enable_vi_mode=Condition(lambda cli: self.vi_mode), enable_open_in_editor=Condition(lambda cli: self.enable_open_in_editor), enable_system_bindings=Condition(lambda cli: self.enable_system_bindings), enable_auto_suggest_bindings=Condition(lambda cli: self.enable_auto_suggest), @@ -366,13 +367,13 @@ def simple_option(title, description, field_name, values=None): " Create Simple on/of option. " values = values or ['off', 'on'] - def get_current_value(cli): + def get_current_value(): return values[bool(getattr(self, field_name))] def get_values(): return { - values[1]: lambda _: enable(field_name), - values[0]: lambda _: disable(field_name), + values[1]: lambda: enable(field_name), + values[0]: lambda: disable(field_name), } return Option(title=title, description=description, @@ -381,31 +382,28 @@ def get_values(): return [ OptionCategory('Input', [ - Option(title='Input mode', - description='Vi or emacs key bindings.', - get_current_value=lambda cli: cli.editing_mode, - get_values=lambda: { - 'vi': lambda cli: setattr(cli, 'editing_mode', EditingMode.Vi), - 'emacs': lambda cli: setattr(cli, 'editing_mode', EditingMode.Emacs), - }), + simple_option(title='Input mode', + description='Vi or emacs key bindings.', + field_name='vi_mode', + values=['emacs', 'vi']), simple_option(title='Paste mode', description="When enabled, don't indent automatically.", field_name='paste_mode'), Option(title='Complete while typing', description="Generate autocompletions automatically while typing. " 'Don\'t require pressing TAB. (Not compatible with "History search".)', - get_current_value=lambda cli: ['off', 'on'][self.complete_while_typing], + get_current_value=lambda: ['off', 'on'][self.complete_while_typing], get_values=lambda: { - 'on': lambda _: enable('complete_while_typing') and disable('enable_history_search'), - 'off': lambda _: disable('complete_while_typing'), + 'on': lambda: enable('complete_while_typing') and disable('enable_history_search'), + 'off': lambda: disable('complete_while_typing'), }), Option(title='History search', description='When pressing the up-arrow, filter the history on input starting ' 'with the current text. (Not compatible with "Complete while typing".)', - get_current_value=lambda cli: ['off', 'on'][self.enable_history_search], + get_current_value=lambda: ['off', 'on'][self.enable_history_search], get_values=lambda: { - 'on': lambda _: enable('enable_history_search') and disable('complete_while_typing'), - 'off': lambda _: disable('enable_history_search'), + 'on': lambda: enable('enable_history_search') and disable('complete_while_typing'), + 'off': lambda: disable('enable_history_search'), }), simple_option(title='Mouse support', description='Respond to mouse clicks and scrolling for positioning the cursor, ' @@ -425,28 +423,28 @@ def get_values(): Option(title='Accept input on enter', description='Amount of ENTER presses required to execute input when the cursor ' 'is at the end of the input. (Note that META+ENTER will always execute.)', - get_current_value=lambda cli: str(self.accept_input_on_enter or 'meta-enter'), + get_current_value=lambda: str(self.accept_input_on_enter or 'meta-enter'), get_values=lambda: { - '2': lambda _: enable('accept_input_on_enter', 2), - '3': lambda _: enable('accept_input_on_enter', 3), - '4': lambda _: enable('accept_input_on_enter', 4), - 'meta-enter': lambda _: enable('accept_input_on_enter', None), + '2': lambda: enable('accept_input_on_enter', 2), + '3': lambda: enable('accept_input_on_enter', 3), + '4': lambda: enable('accept_input_on_enter', 4), + 'meta-enter': lambda: enable('accept_input_on_enter', None), }), ]), OptionCategory('Display', [ Option(title='Completions', description='Visualisation to use for displaying the completions. (Multiple columns, one column, a toolbar or nothing.)', - get_current_value=lambda cli: self.completion_visualisation, + get_current_value=lambda: self.completion_visualisation, get_values=lambda: { - CompletionVisualisation.NONE: lambda _: enable('completion_visualisation', CompletionVisualisation.NONE), - CompletionVisualisation.POP_UP: lambda _: enable('completion_visualisation', CompletionVisualisation.POP_UP), - CompletionVisualisation.MULTI_COLUMN: lambda _: enable('completion_visualisation', CompletionVisualisation.MULTI_COLUMN), - CompletionVisualisation.TOOLBAR: lambda _: enable('completion_visualisation', CompletionVisualisation.TOOLBAR), + CompletionVisualisation.NONE: lambda: enable('completion_visualisation', CompletionVisualisation.NONE), + CompletionVisualisation.POP_UP: lambda: enable('completion_visualisation', CompletionVisualisation.POP_UP), + CompletionVisualisation.MULTI_COLUMN: lambda: enable('completion_visualisation', CompletionVisualisation.MULTI_COLUMN), + CompletionVisualisation.TOOLBAR: lambda: enable('completion_visualisation', CompletionVisualisation.TOOLBAR), }), Option(title='Prompt', description="Visualisation of the prompt. ('>>>' or 'In [1]:')", - get_current_value=lambda cli: self.prompt_style, - get_values=lambda: dict((s, lambda _, s=s: enable('prompt_style', s)) for s in self.all_prompt_styles)), + get_current_value=lambda: self.prompt_style, + get_values=lambda: dict((s, partial(enable, 'prompt_style', s)) for s in self.all_prompt_styles)), simple_option(title='Blank line after output', description='Insert a blank line after the output.', field_name='insert_blank_line_after_output'), @@ -479,15 +477,15 @@ def get_values(): OptionCategory('Colors', [ Option(title='Code', description='Color scheme to use for the Python code.', - get_current_value=lambda cli: self._current_code_style_name, + get_current_value=lambda: self._current_code_style_name, get_values=lambda: dict( - (name, lambda _, name=name: self.use_code_colorscheme(name)) for name in self.code_styles) + (name, partial(self.use_code_colorscheme, name)) for name in self.code_styles) ), Option(title='User interface', description='Color scheme to use for the user interface.', - get_current_value=lambda cli: self._current_ui_style_name, + get_current_value=lambda: self._current_ui_style_name, get_values=lambda: dict( - (name, lambda _, name=name: self.use_ui_colorscheme(name)) for name in self.ui_styles) + (name, partial(self.use_ui_colorscheme, name)) for name in self.ui_styles) ), simple_option(title='True color (24 bit)', description='Use 24 bit colors instead of 265 colors', @@ -513,7 +511,6 @@ def create_application(self): extra_buffer_processors=self._extra_buffer_processors, extra_body=self._extra_layout_body, extra_toolbars=self._extra_toolbars), - editing_mode=(EditingMode.VI if self._vi_mode else EditingMode.EMACS), buffer=self._create_buffer(), buffers=buffers, key_bindings_registry=self.key_bindings_registry, From 36f96f494bfb33374ee773f9a3c9c84e9c023696 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sun, 1 May 2016 15:17:12 +0200 Subject: [PATCH 141/470] Synchronize editing mode between PythonInput class and CommandLineInterface. --- ptpython/key_bindings.py | 4 ++-- ptpython/python_input.py | 18 +++++++++++++++++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/ptpython/key_bindings.py b/ptpython/key_bindings.py index 1c26dcd9..fef08b4d 100644 --- a/ptpython/key_bindings.py +++ b/ptpython/key_bindings.py @@ -2,7 +2,7 @@ from prompt_toolkit.document import Document from prompt_toolkit.enums import DEFAULT_BUFFER -from prompt_toolkit.filters import HasSelection, IsMultiline, Filter, HasFocus, Condition, InViMode +from prompt_toolkit.filters import HasSelection, IsMultiline, Filter, HasFocus, Condition, ViInsertMode, EmacsInsertMode from prompt_toolkit.key_binding.vi_state import InputMode from prompt_toolkit.keys import Keys @@ -81,7 +81,7 @@ def _(event): event.cli.current_buffer.insert_text(' ') @handle(Keys.ControlJ, filter= ~sidebar_visible & ~has_selection & - ~(vi_mode_enabled & InViMode(InputMode.NAVIGATION)) & + (ViInsertMode() | EmacsInsertMode()) & HasFocus(DEFAULT_BUFFER) & IsMultiline()) def _(event): """ diff --git a/ptpython/python_input.py b/ptpython/python_input.py index f5d587a0..9a2465bd 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -13,7 +13,7 @@ from prompt_toolkit.auto_suggest import AutoSuggestFromHistory, ConditionalAutoSuggest from prompt_toolkit.buffer import Buffer from prompt_toolkit.document import Document -from prompt_toolkit.enums import DEFAULT_BUFFER +from prompt_toolkit.enums import DEFAULT_BUFFER, EditingMode from prompt_toolkit.filters import Condition, Always from prompt_toolkit.history import FileHistory, InMemoryHistory from prompt_toolkit.interface import CommandLineInterface, Application, AcceptAction @@ -520,6 +520,7 @@ def create_application(self): on_exit=self._on_exit, style=DynamicStyle(lambda: self._current_style), get_title=lambda: self.terminal_title, + on_initialize=Callback(self._on_cli_initialize), on_start=self._on_start, on_input_timeout=Callback(self._on_input_timeout)) @@ -549,6 +550,21 @@ def is_buffer_multiline(): return python_buffer + def _on_cli_initialize(self, cli): + """ + Called when a CommandLineInterface has been created. + """ + # Synchronize PythonInput state with the CommandLineInterface. + def synchronize(): + if self.vi_mode: + cli.editing_mode = EditingMode.VI + else: + cli.editing_mode = EditingMode.EMACS + + cli.input_processor.beforeKeyPress += synchronize + cli.input_processor.afterKeyPress += synchronize + synchronize() + def _on_input_timeout(self, cli): """ When there is no input activity, From ae21ebd7dd72d1a08de9b57547c1692c09a8c431 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sun, 1 May 2016 22:16:14 +0200 Subject: [PATCH 142/470] Changes for being compatible with the prompt-toolkit Callback refactoring. --- ptpython/history_browser.py | 9 ++++----- ptpython/python_input.py | 8 ++++---- ptpython/repl.py | 4 ++-- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/ptpython/history_browser.py b/ptpython/history_browser.py index 925262ec..7ca2506f 100644 --- a/ptpython/history_browser.py +++ b/ptpython/history_browser.py @@ -24,7 +24,6 @@ from prompt_toolkit.layout.toolbars import ArgToolbar, SearchToolbar from prompt_toolkit.layout.toolbars import TokenListToolbar from prompt_toolkit.layout.utils import token_list_to_text -from prompt_toolkit.utils import Callback from pygments.lexers import RstLexer from pygments.token import Token @@ -530,7 +529,7 @@ def create_history_application(python_input, original_document): """ history_mapping = HistoryMapping(python_input.history, original_document) - def default_buffer_pos_changed(): + def default_buffer_pos_changed(_): """ When the cursor changes in the default buffer. Synchronize with history buffer. """ # Only when this buffer has the focus. @@ -549,7 +548,7 @@ def default_buffer_pos_changed(): history_buffer.cursor_position = \ history_buffer.document.translate_row_col_to_index(history_lineno, 0) - def history_buffer_pos_changed(): + def history_buffer_pos_changed(_): """ When the cursor changes in the history buffer. Synchronize. """ # Only when this buffer has the focus. if buffer_mapping.focus_stack[-1] == HISTORY_BUFFER: @@ -564,14 +563,14 @@ def history_buffer_pos_changed(): history_buffer = Buffer( initial_document=Document(history_mapping.concatenated_history), - on_cursor_position_changed=Callback(history_buffer_pos_changed), + on_cursor_position_changed=history_buffer_pos_changed, accept_action=AcceptAction( lambda cli, buffer: cli.set_return_value(default_buffer.document)), read_only=True) default_buffer = Buffer( initial_document=history_mapping.get_new_document(), - on_cursor_position_changed=Callback(default_buffer_pos_changed), + on_cursor_position_changed=default_buffer_pos_changed, read_only=True) help_buffer = Buffer( diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 9a2465bd..f624dc92 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -22,7 +22,7 @@ from prompt_toolkit.layout.lexers import PygmentsLexer from prompt_toolkit.shortcuts import create_output from prompt_toolkit.styles import DynamicStyle -from prompt_toolkit.utils import Callback, is_windows +from prompt_toolkit.utils import is_windows from prompt_toolkit.validation import ConditionalValidator from .completer import PythonCompleter @@ -520,9 +520,9 @@ def create_application(self): on_exit=self._on_exit, style=DynamicStyle(lambda: self._current_style), get_title=lambda: self.terminal_title, - on_initialize=Callback(self._on_cli_initialize), + on_initialize=self._on_cli_initialize, on_start=self._on_start, - on_input_timeout=Callback(self._on_input_timeout)) + on_input_timeout=self._on_input_timeout) def _create_buffer(self): """ @@ -555,7 +555,7 @@ def _on_cli_initialize(self, cli): Called when a CommandLineInterface has been created. """ # Synchronize PythonInput state with the CommandLineInterface. - def synchronize(): + def synchronize(_=None): if self.vi_mode: cli.editing_mode = EditingMode.VI else: diff --git a/ptpython/repl.py b/ptpython/repl.py index 451b82ad..8286c905 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -18,7 +18,7 @@ from prompt_toolkit.layout.utils import token_list_width from prompt_toolkit.shortcuts import create_asyncio_eventloop from prompt_toolkit.styles import style_from_pygments -from prompt_toolkit.utils import DummyContext, Callback +from prompt_toolkit.utils import DummyContext from .python_input import PythonInput, PythonCommandLineInterface from .eventloop import create_eventloop @@ -44,7 +44,7 @@ def __init__(self, *a, **kw): kw.update({ '_accept_action': AcceptAction.run_in_terminal( handler=self._process_document, render_cli_done=True), - '_on_start': Callback(self._on_start), + '_on_start': self._on_start, '_on_exit': AbortAction.RETURN_NONE, }) From 48f508cb722b1e39f91f08a7c92eb965cf6d10c0 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Mon, 2 May 2016 21:18:48 +0200 Subject: [PATCH 143/470] Unindent after 'pass'. --- ptpython/key_bindings.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ptpython/key_bindings.py b/ptpython/key_bindings.py index fef08b4d..38186891 100644 --- a/ptpython/key_bindings.py +++ b/ptpython/key_bindings.py @@ -215,8 +215,13 @@ def auto_newline(buffer): current_line = buffer.document.current_line_before_cursor.rstrip() insert_text('\n') + # Unident if the last line ends with 'pass', remove four spaces. + unindent = current_line.rstrip().endswith(' pass') + # Copy whitespace from current line - for c in current_line: + current_line2 = current_line[4:] if unindent else current_line + + for c in current_line2: if c.isspace(): insert_text(c) else: From 34e4ebb5442929ab42bfe3653e578aa093f4894e Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 5 May 2016 16:32:16 +0200 Subject: [PATCH 144/470] Release 0.33 --- CHANGELOG | 10 ++++++++++ setup.py | 4 ++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 102c48fa..3e1d25a4 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,16 @@ CHANGELOG ========= +0.33: 2016-05-05 +--------------- + +Upgrade to prompt_toolkit 1.0.0 + +Improvements: +- Unindent after typing 'pass'. +- Make it configurable whether or not a blank line has to be inserted after the output. + + 0.32: 2016-03-29 --------------- diff --git a/setup.py b/setup.py index edba667b..a4546a88 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ setup( name='ptpython', author='Jonathan Slenders', - version='0.32', + version='0.33', url='https://github.com/jonathanslenders/ptpython', description='Python REPL build on top of prompt_toolkit', long_description=long_description, @@ -21,7 +21,7 @@ install_requires = [ 'docopt', 'jedi>=0.9.0', - 'prompt_toolkit==0.60', + 'prompt_toolkit>=1.0.0,<2.0.0', 'pygments', ], entry_points={ From 3f3125e73bcfbf3c7172dbbfeb59ecc722ca479c Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Fri, 6 May 2016 19:14:01 +0200 Subject: [PATCH 145/470] Bugfix in ptipython: reset buffer in run(). --- ptpython/ipython.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ptpython/ipython.py b/ptpython/ipython.py index d94e7437..a3bbc963 100644 --- a/ptpython/ipython.py +++ b/ptpython/ipython.py @@ -208,7 +208,7 @@ def get_globals(): def raw_input(self, prompt=''): print('') try: - string = self._cli.run().text + string = self._cli.run(reset_current_buffer=True).text # In case of multiline input, make sure to append a newline to the input, # otherwise, IPython will ask again for more input in some cases. From 6a88672b884b8a675b8c1de987b00162a53b1b83 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Fri, 6 May 2016 19:15:10 +0200 Subject: [PATCH 146/470] Release 0.34 --- CHANGELOG | 6 ++++++ setup.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 3e1d25a4..a5eae39e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,12 @@ CHANGELOG ========= +0.34: 2016-05-06 +--------------- + +Bugfix in ptipython: reset input buffer before every read in run(). + + 0.33: 2016-05-05 --------------- diff --git a/setup.py b/setup.py index a4546a88..2644810b 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ setup( name='ptpython', author='Jonathan Slenders', - version='0.33', + version='0.34', url='https://github.com/jonathanslenders/ptpython', description='Python REPL build on top of prompt_toolkit', long_description=long_description, From bdaf4773ba6ac16b7119c511ba9a3926f14d4d41 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sun, 22 May 2016 11:57:42 +0200 Subject: [PATCH 147/470] Added example of binding jj to escape in config.py example. --- examples/ptpython_config/config.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/examples/ptpython_config/config.py b/examples/ptpython_config/config.py index 9f337547..0d8c8eb3 100644 --- a/examples/ptpython_config/config.py +++ b/examples/ptpython_config/config.py @@ -4,8 +4,11 @@ Copy this file to ~/.ptpython/config.py """ from __future__ import unicode_literals +from prompt_toolkit.filters import ViInsertMode +from prompt_toolkit.key_binding.input_processor import KeyPress from prompt_toolkit.keys import Keys from pygments.token import Token + from ptpython.layout import CompletionVisualisation __all__ = ( @@ -124,6 +127,14 @@ def _(event): if b.accept_action.is_returnable: b.accept_action.validate_and_handle(event.cli, b) + + # Typing 'jj' in Vi Insert mode, should send escape. (Go back to navigation + # mode.) + @repl.add_key_binding('j', 'j', filter=ViInsertMode()) + def _(event): + " Map 'jj' to Escape. " + event.cli.input_processor.feed(KeyPress(Keys.Escape)) + """ # Custom key binding for some simple autocorrection while typing. corrections = { From aadb371d22ac869e3a5495402f887b465bcf3818 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 24 May 2016 21:07:42 +0200 Subject: [PATCH 148/470] Bugfix in completer: don't hang when pasting a long string with many backslashes. --- ptpython/completer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ptpython/completer.py b/ptpython/completer.py index 6be64bef..b1400371 100644 --- a/ptpython/completer.py +++ b/ptpython/completer.py @@ -62,9 +62,9 @@ def double_quoted_wrapper(text): grammar = r""" # Text before the current string. ( - [^'"#] | # Not quoted characters. - ''' ([^']|'[^']|''[^']|\\.)* ''' | # Inside single quoted triple strings - "" " ([^"]|"[^"]|""[^"]|\\.)* "" " | # Inside double quoted triple strings + [^'"#] | # Not quoted characters. + ''' ([^'\\]|'(?!')|''(?!')|\\.])* ''' | # Inside single quoted triple strings + "" " ([^"\\]|"(?!")|""(?!^)|\\.])* "" " | # Inside double quoted triple strings \#[^\n]*(\n|$) | # Comment. "(?!"") ([^"\\]|\\.)*" | # Inside double quoted strings. From 873efd4e2eecdb139610ad2b378da977d9c65a72 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Fri, 10 Jun 2016 20:15:00 +0200 Subject: [PATCH 149/470] Handle Python2 bug: crash when filenames contain non-ascii characters. --- ptpython/repl.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ptpython/repl.py b/ptpython/repl.py index 8286c905..f2cd983d 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -159,6 +159,12 @@ def _handle_exception(cls, cli, e): if l: l.insert(0, "Traceback (most recent call last):\n") l.extend(traceback.format_exception_only(t, v)) + + # For Python2: `format_list` and `format_exception_only` return + # non-unicode strings. Ensure that everything is unicode. + if six.PY2: + l = [i.decode('utf-8') if isinstance(i, six.binary_type) else i for i in l] + tb = ''.join(l) # Format exception and write to output. From 7f3efb14c41cdc9fcfda28fa67046eecc18c6f34 Mon Sep 17 00:00:00 2001 From: zlsun Date: Sat, 4 Jun 2016 22:30:28 +0800 Subject: [PATCH 150/470] Add `pt[i]pythonX` and `pt[i]pythonX.X` commands --- setup.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/setup.py b/setup.py index 2644810b..37f9e047 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,6 @@ #!/usr/bin/env python import os +import sys from setuptools import setup, find_packages long_description = open( @@ -28,6 +29,10 @@ 'console_scripts': [ 'ptpython = ptpython.entry_points.run_ptpython:run', 'ptipython = ptpython.entry_points.run_ptipython:run', + 'ptpython%s = ptpython.entry_points.run_ptpython:run' % sys.version_info[0], + 'ptpython%s.%s = ptpython.entry_points.run_ptpython:run' % sys.version_info[:2], + 'ptipython%s = ptpython.entry_points.run_ptipython:run' % sys.version_info[0], + 'ptipython%s.%s = ptpython.entry_points.run_ptipython:run' % sys.version_info[:2], ] }, extra_require={ From 32827385cca65eabefccb06b56e4cf9d2c1e0120 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sun, 12 Jun 2016 19:43:04 +0200 Subject: [PATCH 151/470] Added support for IPython 5.0 in ptipython. --- ptpython/ipython.py | 50 +++++++++++++++++++++++++++++++++++++++++++-- ptpython/style.py | 2 +- 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/ptpython/ipython.py b/ptpython/ipython.py index a3bbc963..d66d09b2 100644 --- a/ptpython/ipython.py +++ b/ptpython/ipython.py @@ -21,6 +21,7 @@ from .python_input import PythonInput, PythonValidator, PythonCompleter from .eventloop import create_eventloop +from .style import default_ui_style from IPython.terminal.embed import InteractiveShellEmbed as _InteractiveShellEmbed from IPython.terminal.ipapp import load_default_config @@ -59,6 +60,23 @@ def out_tokens(self, cli): return [(Token.Out, text)] +class IPython5Prompt(PromptStyle): + """ + Style for IPython >5.0, use the prompt_toolkit tokens directly. + """ + def __init__(self, prompts): + self.prompts = prompts + + def in_tokens(self, cli): + return self.prompts.in_prompt_tokens(cli) + + def in2_tokens(self, cli, width): + return self.prompts.continuation_prompt_tokens(cli) + + def out_tokens(self, cli): + return [] + + class IPythonValidator(PythonValidator): def __init__(self, *args, **kwargs): super(IPythonValidator, self).__init__(*args, **kwargs) @@ -167,8 +185,31 @@ def __init__(self, ipython_shell, *a, **kw): super(IPythonInput, self).__init__(*a, **kw) self.ipython_shell = ipython_shell - self.all_prompt_styles['ipython'] = IPythonPrompt(ipython_shell.prompt_manager) - self.prompt_style = 'ipython' + # Prompt for IPython < 5.0 + if hasattr(ipython_shell, 'prompt_manager'): + self.all_prompt_styles['ipython'] = IPythonPrompt(ipython_shell.prompt_manager) + self.prompt_style = 'ipython' + + # Prompt for IPython >=5.0: + if hasattr(ipython_shell, 'prompts'): + self.all_prompt_styles['ipython'] = IPython5Prompt(ipython_shell.prompts) + self.prompt_style = 'ipython' + + + # UI style for IPython. Add tokens that are used by IPython>5.0 + style_dict = {} + style_dict.update(default_ui_style) + style_dict.update({ + Token.Prompt: '#009900', + Token.PromptNum: '#00ff00 bold', + Token.OutPrompt: '#990000', + Token.OutPromptNum: '#ff0000 bold', + }) + + self.ui_styles = { + 'default': style_dict, + } + self.use_ui_colorscheme('default') class InteractiveShellEmbed(_InteractiveShellEmbed): @@ -200,11 +241,16 @@ def get_globals(): if configure: configure(ipython_input) + ipython_input.prompt_style = 'ipython' # Don't take from config. self._cli = CommandLineInterface( application=ipython_input.create_application(), eventloop=self._eventloop) + def prompt_for_code(self): + # IPython 5.0 calls `prompt_for_code` instead of `raw_input`. + return self.raw_input(self) + def raw_input(self, prompt=''): print('') try: diff --git a/ptpython/style.py b/ptpython/style.py index 43068498..f80754df 100644 --- a/ptpython/style.py +++ b/ptpython/style.py @@ -78,7 +78,7 @@ def generate_style(python_style, ui_style): Token.Prompt: 'bold', Token.Prompt.Dots: 'noinherit', - # (IPython) Prompt: "In [1]:" + # (IPython <5.0) Prompt: "In [1]:" Token.In: 'bold #008800', Token.In.Number: '', From 6362dbd43026a7cc5156f916c9390ed21f3d9f45 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 19 Jul 2016 12:59:55 +0200 Subject: [PATCH 152/470] Release 0.35 --- CHANGELOG | 11 +++++++++++ setup.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index a5eae39e..10fb8152 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,17 @@ CHANGELOG ========= +0.35: 2016-07-19 +---------------- + +Fixes: +- Fix in completer. Don't hang when pasting a long string with many + backslashes. +- Fix Python2 bug: crash when filenames contain non-ascii characters. +- Added `pt[i]pythonX` and `pt[i]pythonX.X` commands. +- Compatibility with IPython 5.0. + + 0.34: 2016-05-06 --------------- diff --git a/setup.py b/setup.py index 37f9e047..3030d8ae 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ setup( name='ptpython', author='Jonathan Slenders', - version='0.34', + version='0.35', url='https://github.com/jonathanslenders/ptpython', description='Python REPL build on top of prompt_toolkit', long_description=long_description, From be3d22e6be1bca5784a2b44395b56d74d0d1b5a5 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sun, 9 Oct 2016 21:51:27 +0200 Subject: [PATCH 153/470] Display multiple cursors, when inserting in Vi Visual block mode. --- ptpython/layout.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ptpython/layout.py b/ptpython/layout.py index b1329f31..5da3cef5 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -12,7 +12,7 @@ from prompt_toolkit.layout.lexers import SimpleLexer from prompt_toolkit.layout.margins import PromptMargin from prompt_toolkit.layout.menus import CompletionsMenu, MultiColumnCompletionsMenu -from prompt_toolkit.layout.processors import ConditionalProcessor, AppendAutoSuggestion, HighlightSearchProcessor, HighlightSelectionProcessor, HighlightMatchingBracketProcessor +from prompt_toolkit.layout.processors import ConditionalProcessor, AppendAutoSuggestion, HighlightSearchProcessor, HighlightSelectionProcessor, HighlightMatchingBracketProcessor, DisplayMultipleCursors from prompt_toolkit.layout.screen import Char from prompt_toolkit.layout.toolbars import CompletionsToolbar, ArgToolbar, SearchToolbar, ValidationToolbar, SystemToolbar, TokenListToolbar from prompt_toolkit.layout.utils import token_list_width @@ -483,6 +483,7 @@ def menu_position(cli): filter=HasFocus(SEARCH_BUFFER), ), HighlightSelectionProcessor(), + DisplayMultipleCursors(DEFAULT_BUFFER), # Show matching parentheses, but only while editing. ConditionalProcessor( processor=HighlightMatchingBracketProcessor(chars='[](){}'), From 0efdd87a466f091abafc9998232a33cd76b1e290 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sun, 16 Oct 2016 21:34:48 +0200 Subject: [PATCH 154/470] Workaround for Jedi crash when generating function signature. See: https://github.com/jonathanslenders/ptpython/issues/136 --- ptpython/layout.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/ptpython/layout.py b/ptpython/layout.py index 5da3cef5..4aa159d8 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -208,7 +208,14 @@ def get_tokens(cli): append((Signature.Operator, '(')) - for i, p in enumerate(sig.params): + try: + enumerated_params = enumerate(sig.params) + except AttributeError: + # Workaround for #136: https://github.com/jonathanslenders/ptpython/issues/136 + # AttributeError: 'Lambda' object has no attribute 'get_subscope_by_name' + return [] + + for i, p in enumerated_params: # Workaround for #47: 'p' is None when we hit the '*' in the signature. # and sig has no 'index' attribute. # See: https://github.com/jonathanslenders/ptpython/issues/47 From decb0f9afd94aaa5b2b667d3dea5c310858c70b1 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sun, 16 Oct 2016 21:38:09 +0200 Subject: [PATCH 155/470] Handle Jedi crash. #91 --- ptpython/utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ptpython/utils.py b/ptpython/utils.py index 08e94f0c..f6f914d0 100644 --- a/ptpython/utils.py +++ b/ptpython/utils.py @@ -65,6 +65,9 @@ def get_jedi_script_from_document(document, locals, globals): except KeyError: # Workaroud for a crash when the input is "u'", the start of a unicode string. return None + except Exception: + # Workaround for: https://github.com/jonathanslenders/ptpython/issues/91 + return None _multiline_string_delims = re.compile('''[']{3}|["]{3}''') From 22f82b15de664da07bbee8bda07b359df3e0e232 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sun, 16 Oct 2016 21:17:58 +0200 Subject: [PATCH 156/470] Release 0.36 --- CHANGELOG | 8 ++++++++ ptpython/layout.py | 16 +++++++++++++++- setup.py | 2 +- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 10fb8152..13716db9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,14 @@ CHANGELOG ========= +0.36: 2016-10-16 +---------------- + +New features: +- Support for editing in Vi block mode. (Only enabled for + prompt_toolkit>=1.0.8.) + + 0.35: 2016-07-19 ---------------- diff --git a/ptpython/layout.py b/ptpython/layout.py index 4aa159d8..24b774c8 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -12,7 +12,7 @@ from prompt_toolkit.layout.lexers import SimpleLexer from prompt_toolkit.layout.margins import PromptMargin from prompt_toolkit.layout.menus import CompletionsMenu, MultiColumnCompletionsMenu -from prompt_toolkit.layout.processors import ConditionalProcessor, AppendAutoSuggestion, HighlightSearchProcessor, HighlightSelectionProcessor, HighlightMatchingBracketProcessor, DisplayMultipleCursors +from prompt_toolkit.layout.processors import ConditionalProcessor, AppendAutoSuggestion, HighlightSearchProcessor, HighlightSelectionProcessor, HighlightMatchingBracketProcessor, Processor, Transformation from prompt_toolkit.layout.screen import Char from prompt_toolkit.layout.toolbars import CompletionsToolbar, ArgToolbar, SearchToolbar, ValidationToolbar, SystemToolbar, TokenListToolbar from prompt_toolkit.layout.utils import token_list_width @@ -34,6 +34,20 @@ ) +# DisplayMultipleCursors: Only for prompt_toolkit>=1.0.8 +try: + from prompt_toolkit.layout.processors import DisplayMultipleCursors +except ImportError: + class DisplayMultipleCursors(Processor): + " Dummy. " + def __init__(self, *a): + pass + + def apply_transformation(self, cli, document, lineno, + source_to_display, tokens): + return Transformation(tokens) + + class CompletionVisualisation: " Visualisation method for the completions. " NONE = 'none' diff --git a/setup.py b/setup.py index 3030d8ae..b0b89a64 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ setup( name='ptpython', author='Jonathan Slenders', - version='0.35', + version='0.36', url='https://github.com/jonathanslenders/ptpython', description='Python REPL build on top of prompt_toolkit', long_description=long_description, From 3bfdaf7e2a9c32316239ebedaceb9830add98bd0 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Mon, 7 Nov 2016 22:34:07 +0100 Subject: [PATCH 157/470] Update changelog (from last release.) --- CHANGELOG | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 13716db9..73febeea 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -8,6 +8,8 @@ New features: - Support for editing in Vi block mode. (Only enabled for prompt_toolkit>=1.0.8.) +Fixes: +- Handle two Jedi crashes. (GitHub ptpython issues #136 and #91.) 0.35: 2016-07-19 ---------------- From d9f35b6ee68f391a2be439b0c5b234da445384f6 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Mon, 7 Nov 2016 22:35:30 +0100 Subject: [PATCH 158/470] Display 'VISUAL BLOCK' and 'INSERT' when we're in these modes. --- ptpython/layout.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ptpython/layout.py b/ptpython/layout.py index 24b774c8..5028974a 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -374,7 +374,10 @@ def toggle_vi_mode(cli, mouse_event): elif cli.current_buffer.selection_state.type == SelectionType.CHARACTERS: append((token.InputMode, 'Vi (VISUAL)', toggle_vi_mode)) append((token, ' ')) - elif mode == InputMode.INSERT: + elif cli.current_buffer.selection_state.type == 'BLOCK': + append((token.InputMode, 'Vi (VISUAL BLOCK)', toggle_vi_mode)) + append((token, ' ')) + elif mode in (InputMode.INSERT, 'vi-insert-multiple'): append((token.InputMode, 'Vi (INSERT)', toggle_vi_mode)) append((token, ' ')) elif mode == InputMode.NAVIGATION: From 45c37bb5b5df5c87c8d937ee1de8d62330df6cca Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 6 Dec 2016 22:13:47 +0100 Subject: [PATCH 159/470] Handle ValueError in PythonValidator. Python2 raises ValueError if the input contains an invalid escape sequence. --- ptpython/validator.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ptpython/validator.py b/ptpython/validator.py index dd0ae73d..103697b5 100644 --- a/ptpython/validator.py +++ b/ptpython/validator.py @@ -42,3 +42,7 @@ def validate(self, document): except TypeError as e: # e.g. "compile() expected string without null bytes" raise ValidationError(0, str(e)) + except ValueError as e: + # In Python 2, compiling "\x9" (an invalid escape sequence) raises + # ValueError instead of SyntaxError. + raise ValidationError(0, 'Syntax Error: %s' % e) From 79bb681bd98b03e8c60030a3b30ca2e2767fcf1d Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sat, 17 Dec 2016 22:09:26 +0100 Subject: [PATCH 160/470] Use load_key_bindings instead of KeyBindingsManager. (For the latest prompt_toolkit.) --- ptpython/history_browser.py | 8 +++---- ptpython/key_bindings.py | 25 ++++++++++++++++----- ptpython/layout.py | 6 ++--- ptpython/python_input.py | 44 ++++++++++++++++++------------------- ptpython/repl.py | 1 - 5 files changed, 47 insertions(+), 37 deletions(-) diff --git a/ptpython/history_browser.py b/ptpython/history_browser.py index 7ca2506f..b6fb8f07 100644 --- a/ptpython/history_browser.py +++ b/ptpython/history_browser.py @@ -12,7 +12,7 @@ from prompt_toolkit.document import Document from prompt_toolkit.enums import DEFAULT_BUFFER from prompt_toolkit.filters import Condition, HasFocus, InFocusStack -from prompt_toolkit.key_binding.manager import KeyBindingManager +from prompt_toolkit.key_binding.defaults import load_key_bindings from prompt_toolkit.keys import Keys from prompt_toolkit.layout.containers import HSplit, VSplit, Window, FloatContainer, Float, ConditionalContainer, Container, ScrollOffsets from prompt_toolkit.layout.controls import BufferControl, FillControl @@ -420,10 +420,10 @@ def create_key_bindings(python_input, history_mapping): """ Key bindings. """ - manager = KeyBindingManager( + registry = load_key_bindings( enable_search=True, enable_extra_page_navigation=True) - handle = manager.registry.add_binding + handle = registry.add_binding @handle(' ', filter=HasFocus(HISTORY_BUFFER)) def _(event): @@ -517,7 +517,7 @@ def _(event): " Suspend to background. " event.cli.suspend_to_background() - return manager.registry + return registry def create_history_application(python_input, original_document): diff --git a/ptpython/key_bindings.py b/ptpython/key_bindings.py index 38186891..7f18551d 100644 --- a/ptpython/key_bindings.py +++ b/ptpython/key_bindings.py @@ -4,6 +4,7 @@ from prompt_toolkit.enums import DEFAULT_BUFFER from prompt_toolkit.filters import HasSelection, IsMultiline, Filter, HasFocus, Condition, ViInsertMode, EmacsInsertMode from prompt_toolkit.key_binding.vi_state import InputMode +from prompt_toolkit.key_binding.registry import Registry from prompt_toolkit.keys import Keys __all__ = ( @@ -29,12 +30,14 @@ def __call__(self, cli): return bool(b.text and (not before_cursor or before_cursor.isspace())) -def load_python_bindings(key_bindings_manager, python_input): +def load_python_bindings(python_input): """ Custom key bindings. """ + registry = Registry() + sidebar_visible = Condition(lambda cli: python_input.show_sidebar) - handle = key_bindings_manager.registry.add_binding + handle = registry.add_binding has_selection = HasSelection() vi_mode_enabled = Condition(lambda cli: python_input.vi_mode) @@ -128,12 +131,16 @@ def _(event): """ python_input.show_exit_confirmation = True + return registry + -def load_sidebar_bindings(key_bindings_manager, python_input): +def load_sidebar_bindings(python_input): """ Load bindings for the navigation in the sidebar. """ - handle = key_bindings_manager.registry.add_binding + registry = Registry() + + handle = registry.add_binding sidebar_visible = Condition(lambda cli: python_input.show_sidebar) @handle(Keys.Up, filter=sidebar_visible) @@ -176,12 +183,16 @@ def _(event): " Hide sidebar. " python_input.show_sidebar = False + return registry -def load_confirm_exit_bindings(key_bindings_manager, python_input): + +def load_confirm_exit_bindings(python_input): """ Handle yes/no key presses when the exit confirmation is shown. """ - handle = key_bindings_manager.registry.add_binding + registry = Registry() + + handle = registry.add_binding confirmation_visible = Condition(lambda cli: python_input.show_exit_confirmation) @handle('y', filter=confirmation_visible) @@ -200,6 +211,8 @@ def _(event): """ python_input.show_exit_confirmation = False + return registry + def auto_newline(buffer): r""" diff --git a/ptpython/layout.py b/ptpython/layout.py index 5028974a..2a403b96 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -290,7 +290,7 @@ def get_continuation_prompt(cli, width): show_numbers=Condition(lambda cli: python_input.show_line_numbers)) -def status_bar(key_bindings_manager, python_input): +def status_bar(python_input): """ Create the `Layout` for the status bar. """ @@ -474,7 +474,7 @@ def extra_condition(cli): filter=visible) -def create_layout(python_input, key_bindings_manager, +def create_layout(python_input, lexer=PythonLexer, extra_body=None, extra_toolbars=None, extra_buffer_processors=None, input_buffer_height=None): @@ -592,7 +592,7 @@ def menu_position(cli): ]), ] + extra_toolbars + [ VSplit([ - status_bar(key_bindings_manager, python_input), + status_bar(python_input), show_sidebar_button_info(python_input), ]) ]) diff --git a/ptpython/python_input.py b/ptpython/python_input.py index f624dc92..c8609bcd 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -17,8 +17,9 @@ from prompt_toolkit.filters import Condition, Always from prompt_toolkit.history import FileHistory, InMemoryHistory from prompt_toolkit.interface import CommandLineInterface, Application, AcceptAction -from prompt_toolkit.key_binding.manager import KeyBindingManager +from prompt_toolkit.key_binding.defaults import load_key_bindings_for_prompt, load_mouse_bindings from prompt_toolkit.key_binding.vi_state import InputMode +from prompt_toolkit.key_binding.registry import MergedRegistry, ConditionalRegistry from prompt_toolkit.layout.lexers import PygmentsLexer from prompt_toolkit.shortcuts import create_output from prompt_toolkit.styles import DynamicStyle @@ -226,22 +227,25 @@ def __init__(self, # Code signatures. (This is set asynchronously after a timeout.) self.signatures = [] - # Use a KeyBindingManager for loading the key bindings. - self.key_bindings_manager = KeyBindingManager( - enable_abort_and_exit_bindings=True, - enable_search=True, - enable_vi_mode=Condition(lambda cli: self.vi_mode), - enable_open_in_editor=Condition(lambda cli: self.enable_open_in_editor), - enable_system_bindings=Condition(lambda cli: self.enable_system_bindings), - enable_auto_suggest_bindings=Condition(lambda cli: self.enable_auto_suggest), - - # Disable all default key bindings when the sidebar or the exit confirmation - # are shown. - enable_all=Condition(lambda cli: not (self.show_sidebar or self.show_exit_confirmation))) - - load_python_bindings(self.key_bindings_manager, self) - load_sidebar_bindings(self.key_bindings_manager, self) - load_confirm_exit_bindings(self.key_bindings_manager, self) + # Create a Registry for the key bindings. + self.key_bindings_registry = MergedRegistry([ + ConditionalRegistry( + registry=load_key_bindings_for_prompt( + enable_abort_and_exit_bindings=True, + enable_search=True, + enable_open_in_editor=Condition(lambda cli: self.enable_open_in_editor), + enable_system_bindings=Condition(lambda cli: self.enable_system_bindings), + enable_auto_suggest_bindings=Condition(lambda cli: self.enable_auto_suggest)), + + # Disable all default key bindings when the sidebar or the exit confirmation + # are shown. + filter=Condition(lambda cli: not (self.show_sidebar or self.show_exit_confirmation)) + ), + load_mouse_bindings(), + load_python_bindings(self), + load_sidebar_bindings(self), + load_confirm_exit_bindings(self), + ]) # Boolean indicating whether we have a signatures thread running. # (Never run more than one at the same time.) @@ -276,10 +280,6 @@ def get_compiler_flags(self): return flags - @property - def key_bindings_registry(self): - return self.key_bindings_manager.registry - @property def add_key_binding(self): """ @@ -505,7 +505,6 @@ def create_application(self): return Application( layout=create_layout( self, - self.key_bindings_manager, lexer=self._lexer, input_buffer_height=self._input_buffer_height, extra_buffer_processors=self._extra_buffer_processors, @@ -635,7 +634,6 @@ def run(): cli.eventloop.run_in_executor(run) def on_reset(self, cli): - self.key_bindings_manager.reset(cli) self.signatures = [] def enter_history(self, cli): diff --git a/ptpython/repl.py b/ptpython/repl.py index f2cd983d..75ef94f5 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -85,7 +85,6 @@ def _process_document(self, cli, buffer): # Append to history and reset. cli.search_state.text = '' cli.buffers[DEFAULT_BUFFER].reset(append_to_history=True) - self.key_bindings_manager.reset(cli) def _execute(self, cli, line): """ From 9fb61ac930904d242ef194e5f32632bb74de19e7 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Wed, 21 Dec 2016 19:06:34 +0100 Subject: [PATCH 161/470] Also accept Ctrl-D for quitting the 'exit confirmation' dialog. --- ptpython/key_bindings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ptpython/key_bindings.py b/ptpython/key_bindings.py index 7f18551d..d0200f38 100644 --- a/ptpython/key_bindings.py +++ b/ptpython/key_bindings.py @@ -198,6 +198,7 @@ def load_confirm_exit_bindings(python_input): @handle('y', filter=confirmation_visible) @handle('Y', filter=confirmation_visible) @handle(Keys.ControlJ, filter=confirmation_visible) + @handle(Keys.ControlD, filter=confirmation_visible) def _(event): """ Really quit. From a451952b71e8b6cc4ece13d0687a65e375075610 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 27 Dec 2016 19:26:50 +0100 Subject: [PATCH 162/470] Set 'reverse_vi_search_direction'. --- ptpython/python_input.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ptpython/python_input.py b/ptpython/python_input.py index c8609bcd..748c52d1 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -519,6 +519,7 @@ def create_application(self): on_exit=self._on_exit, style=DynamicStyle(lambda: self._current_style), get_title=lambda: self.terminal_title, + reverse_vi_search_direction=True, on_initialize=self._on_cli_initialize, on_start=self._on_start, on_input_timeout=self._on_input_timeout) From 910bca9b71039d825849ade2c7d273bc51aa3d13 Mon Sep 17 00:00:00 2001 From: Rob Phoenix Date: Thu, 5 Jan 2017 11:15:24 +0000 Subject: [PATCH 163/470] Check for symlink when creating config dir. Fixes #141 --- ptpython/entry_points/run_ptipython.py | 2 +- ptpython/entry_points/run_ptpython.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ptpython/entry_points/run_ptipython.py b/ptpython/entry_points/run_ptipython.py index 1def5fc2..6ad35606 100644 --- a/ptpython/entry_points/run_ptipython.py +++ b/ptpython/entry_points/run_ptipython.py @@ -27,7 +27,7 @@ def run(): config_dir = os.path.expanduser(a['--config-dir'] or '~/.ptpython/') # Create config directory. - if not os.path.isdir(config_dir): + if not os.path.isdir(config_dir) or not os.path.islink(config_dir): os.mkdir(config_dir) # If IPython is not available, show message and exit here with error status diff --git a/ptpython/entry_points/run_ptpython.py b/ptpython/entry_points/run_ptpython.py index 67e9c072..488e5b1f 100644 --- a/ptpython/entry_points/run_ptpython.py +++ b/ptpython/entry_points/run_ptpython.py @@ -32,7 +32,7 @@ def run(): config_dir = os.path.expanduser(a['--config-dir'] or '~/.ptpython/') # Create config directory. - if not os.path.isdir(config_dir): + if not os.path.isdir(config_dir) or not os.path.islink(config_dir): os.mkdir(config_dir) # Startup path From 68bf87ba791aaed336546341729f571a01f691ec Mon Sep 17 00:00:00 2001 From: Rob Phoenix Date: Thu, 5 Jan 2017 11:22:32 +0000 Subject: [PATCH 164/470] Make default config dir filepath OS independent. --- ptpython/entry_points/run_ptipython.py | 2 +- ptpython/entry_points/run_ptpython.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ptpython/entry_points/run_ptipython.py b/ptpython/entry_points/run_ptipython.py index 6ad35606..1f744dc4 100644 --- a/ptpython/entry_points/run_ptipython.py +++ b/ptpython/entry_points/run_ptipython.py @@ -24,7 +24,7 @@ def run(): a = docopt.docopt(__doc__) vi_mode = bool(a['--vi']) - config_dir = os.path.expanduser(a['--config-dir'] or '~/.ptpython/') + config_dir = os.path.expanduser(a['--config-dir'] or os.path.join('~', '.ptpython') # Create config directory. if not os.path.isdir(config_dir) or not os.path.islink(config_dir): diff --git a/ptpython/entry_points/run_ptpython.py b/ptpython/entry_points/run_ptpython.py index 488e5b1f..85c71c4a 100644 --- a/ptpython/entry_points/run_ptpython.py +++ b/ptpython/entry_points/run_ptpython.py @@ -29,7 +29,7 @@ def run(): a = docopt.docopt(__doc__) vi_mode = bool(a['--vi']) - config_dir = os.path.expanduser(a['--config-dir'] or '~/.ptpython/') + config_dir = os.path.expanduser(a['--config-dir'] or os.path.join('~', '.ptpython')) # Create config directory. if not os.path.isdir(config_dir) or not os.path.islink(config_dir): From 16e4e3155733ad8c90312414cc975315ad8566d3 Mon Sep 17 00:00:00 2001 From: Jonathan Virga Date: Wed, 9 Nov 2016 07:11:58 +0100 Subject: [PATCH 165/470] Remove minor python version in entry point. --- setup.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/setup.py b/setup.py index b0b89a64..eeecaf88 100644 --- a/setup.py +++ b/setup.py @@ -30,9 +30,7 @@ 'ptpython = ptpython.entry_points.run_ptpython:run', 'ptipython = ptpython.entry_points.run_ptipython:run', 'ptpython%s = ptpython.entry_points.run_ptpython:run' % sys.version_info[0], - 'ptpython%s.%s = ptpython.entry_points.run_ptpython:run' % sys.version_info[:2], 'ptipython%s = ptpython.entry_points.run_ptipython:run' % sys.version_info[0], - 'ptipython%s.%s = ptpython.entry_points.run_ptipython:run' % sys.version_info[:2], ] }, extra_require={ From 8d49fb20b49b0d1899a618793eb45450bb751faa Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sat, 28 Jan 2017 12:39:08 +0100 Subject: [PATCH 166/470] Fixed .ptpython directory creation in run_ptpython. --- ptpython/entry_points/run_ptpython.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ptpython/entry_points/run_ptpython.py b/ptpython/entry_points/run_ptpython.py index 85c71c4a..494a7fac 100644 --- a/ptpython/entry_points/run_ptpython.py +++ b/ptpython/entry_points/run_ptpython.py @@ -32,7 +32,7 @@ def run(): config_dir = os.path.expanduser(a['--config-dir'] or os.path.join('~', '.ptpython')) # Create config directory. - if not os.path.isdir(config_dir) or not os.path.islink(config_dir): + if not os.path.isdir(config_dir) and not os.path.islink(config_dir): os.mkdir(config_dir) # Startup path From 7685759ddccc0ab03b46aab6ddc967bb76b8088f Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sun, 26 Mar 2017 20:56:49 +0200 Subject: [PATCH 167/470] Make sure that we start in insert mode in the REPL before typing any code. --- ptpython/repl.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ptpython/repl.py b/ptpython/repl.py index 75ef94f5..7c1230fb 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -15,6 +15,7 @@ from prompt_toolkit.application import AbortAction from prompt_toolkit.enums import DEFAULT_BUFFER from prompt_toolkit.interface import AcceptAction +from prompt_toolkit.key_binding.vi_state import InputMode from prompt_toolkit.layout.utils import token_list_width from prompt_toolkit.shortcuts import create_asyncio_eventloop from prompt_toolkit.styles import style_from_pygments @@ -86,6 +87,11 @@ def _process_document(self, cli, buffer): cli.search_state.text = '' cli.buffers[DEFAULT_BUFFER].reset(append_to_history=True) + # Make sure that we end up in insert mode. + # (Not exactly the right place to check this.) + if cli.vi_state.input_mode == InputMode.NAVIGATION: + cli.vi_state.input_mode = InputMode.INSERT + def _execute(self, cli, line): """ Evaluate the line and print the result. From decfbe6a98801980ea799fc8d29d5c4d6febf130 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sun, 26 Mar 2017 20:47:08 +0200 Subject: [PATCH 168/470] Release 0.37 --- CHANGELOG | 22 ++++++++++++++++++++++ setup.py | 4 ++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 73febeea..f5e6e915 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,28 @@ CHANGELOG ========= +0.37: 2017-03-26 +---------------- + +Fixes: +- Display 'VISUAL BLOCK' and 'INSERT' when we're in these modes. +- Handle ValueError in PythonValidator. Python2 raises ValueError if the input + contains an invalid escape sequence. +- Use load_key_bindings instead of KeyBindingsManager. (For the latest + prompt_toolkit.) +- Set 'reverse_vi_search_direction'. (Search backwards when '/' was pressed in + Vi mode.) +- Check for symlink when creating config dir. +- Make default config dir filepath OS independent. +- Remove minor python version in entry point. +- Fixed .ptpython directory creation in run_ptpython. + +New features: +- Also accept Ctrl-D for quitting the 'exit confirmation' dialog. + +Requires prompt_toolkit 1.0.14. + + 0.36: 2016-10-16 ---------------- diff --git a/setup.py b/setup.py index eeecaf88..60e175ff 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ setup( name='ptpython', author='Jonathan Slenders', - version='0.36', + version='0.37', url='https://github.com/jonathanslenders/ptpython', description='Python REPL build on top of prompt_toolkit', long_description=long_description, @@ -22,7 +22,7 @@ install_requires = [ 'docopt', 'jedi>=0.9.0', - 'prompt_toolkit>=1.0.0,<2.0.0', + 'prompt_toolkit>=1.0.14,<2.0.0', 'pygments', ], entry_points={ From f9de3dd4516eed48ce8f82dbb699af714b84a9b1 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sun, 26 Mar 2017 21:01:47 +0200 Subject: [PATCH 169/470] Fixed syntax error in run_ptipython script. --- ptpython/entry_points/run_ptipython.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ptpython/entry_points/run_ptipython.py b/ptpython/entry_points/run_ptipython.py index 1f744dc4..5225823c 100644 --- a/ptpython/entry_points/run_ptipython.py +++ b/ptpython/entry_points/run_ptipython.py @@ -24,7 +24,7 @@ def run(): a = docopt.docopt(__doc__) vi_mode = bool(a['--vi']) - config_dir = os.path.expanduser(a['--config-dir'] or os.path.join('~', '.ptpython') + config_dir = os.path.expanduser(a['--config-dir'] or os.path.join('~', '.ptpython')) # Create config directory. if not os.path.isdir(config_dir) or not os.path.islink(config_dir): From e6e91b66e4cd1e2d60815d797ccf0a669cfb0caa Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sun, 26 Mar 2017 21:02:30 +0200 Subject: [PATCH 170/470] Release 0.38 --- CHANGELOG | 7 +++++++ setup.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index f5e6e915..d2f36830 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,13 @@ CHANGELOG ========= +0.38: 2017-03-26 +---------------- + +Fixes: +- Fixed syntax error in run_ptipython script. + + 0.37: 2017-03-26 ---------------- diff --git a/setup.py b/setup.py index 60e175ff..bebcb8ae 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ setup( name='ptpython', author='Jonathan Slenders', - version='0.37', + version='0.38', url='https://github.com/jonathanslenders/ptpython', description='Python REPL build on top of prompt_toolkit', long_description=long_description, From 1512b95d21e0f4b1dceb6109e6652d4b51ad99fc Mon Sep 17 00:00:00 2001 From: beader Date: Mon, 27 Mar 2017 18:01:41 +0800 Subject: [PATCH 171/470] Fix logic error when checking existance of config_dir --- ptpython/entry_points/run_ptipython.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ptpython/entry_points/run_ptipython.py b/ptpython/entry_points/run_ptipython.py index 5225823c..51347980 100644 --- a/ptpython/entry_points/run_ptipython.py +++ b/ptpython/entry_points/run_ptipython.py @@ -27,7 +27,7 @@ def run(): config_dir = os.path.expanduser(a['--config-dir'] or os.path.join('~', '.ptpython')) # Create config directory. - if not os.path.isdir(config_dir) or not os.path.islink(config_dir): + if not os.path.isdir(config_dir) and not os.path.islink(config_dir): os.mkdir(config_dir) # If IPython is not available, show message and exit here with error status From dfead6332ef43c761aa58cc337a9f95ae8e635f5 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 28 Mar 2017 23:39:59 +0200 Subject: [PATCH 172/470] Release 0.39 --- CHANGELOG | 8 ++++++++ setup.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index d2f36830..1457f5e0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,14 @@ CHANGELOG ========= +0.38: 2017-03-28 +---------------- + +Fixes: +- Fixed bug in run_ptipython. (It could fail to start if the config directory + already existed.) + + 0.38: 2017-03-26 ---------------- diff --git a/setup.py b/setup.py index bebcb8ae..53f3a106 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ setup( name='ptpython', author='Jonathan Slenders', - version='0.38', + version='0.39', url='https://github.com/jonathanslenders/ptpython', description='Python REPL build on top of prompt_toolkit', long_description=long_description, From 80ad73d494b164ae4cea29d9e1b4c64bfe82700a Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 28 Mar 2017 23:57:14 +0200 Subject: [PATCH 173/470] Catch AssertionError in Jedi completion. --- ptpython/completer.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ptpython/completer.py b/ptpython/completer.py index b1400371..90da6451 100644 --- a/ptpython/completer.py +++ b/ptpython/completer.py @@ -147,6 +147,10 @@ def get_completions(self, document, complete_event): # Jedi issue: "IOError: No such file or directory." # https://github.com/jonathanslenders/ptpython/issues/71 pass + except AssertionError: + # In jedi.parser.__init__.py: 227, in remove_last_newline, + # the assertion "newline.value.endswith('\n')" can fail. + pass else: for c in completions: yield Completion(c.name_with_symbols, len(c.complete) - len(c.name_with_symbols), From af33a310dedf69483715f0999a95221d6ab24043 Mon Sep 17 00:00:00 2001 From: Peter Holloway Date: Mon, 26 Jun 2017 15:03:26 +0100 Subject: [PATCH 174/470] Use __main__.__dict__ for locals when running from command line Fixes issues #148 and #201 --- ptpython/entry_points/run_ptpython.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ptpython/entry_points/run_ptpython.py b/ptpython/entry_points/run_ptpython.py index 494a7fac..e681cc83 100644 --- a/ptpython/entry_points/run_ptpython.py +++ b/ptpython/entry_points/run_ptpython.py @@ -64,9 +64,11 @@ def configure(repl): if os.path.exists(path): run_config(repl, path) + import __main__ embed(vi_mode=vi_mode, history_filename=os.path.join(config_dir, 'history'), configure=configure, + locals=__main__.__dict__, startup_paths=startup_paths, title='Python REPL (ptpython)') From 895aa06451e515cf187766325d4d63dd31ea1bce Mon Sep 17 00:00:00 2001 From: Arthur Noel Date: Tue, 9 May 2017 20:50:20 +0100 Subject: [PATCH 175/470] setup.py: extras_require not extra_require --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 53f3a106..aad4c625 100644 --- a/setup.py +++ b/setup.py @@ -33,7 +33,7 @@ 'ptipython%s = ptpython.entry_points.run_ptipython:run' % sys.version_info[0], ] }, - extra_require={ + extras_require={ 'ptipython': ['ipython'] # For ptipython, we need to have IPython } ) From f77177c2f62fe882193c81b096c2b613a339f83c Mon Sep 17 00:00:00 2001 From: Orivej Desh Date: Wed, 30 Mar 2016 19:06:32 +0000 Subject: [PATCH 176/470] Support user_ns arg for compatibility with start_ipython. --- ptpython/entry_points/run_ptipython.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ptpython/entry_points/run_ptipython.py b/ptpython/entry_points/run_ptipython.py index 51347980..ac53a1f1 100644 --- a/ptpython/entry_points/run_ptipython.py +++ b/ptpython/entry_points/run_ptipython.py @@ -20,7 +20,7 @@ import sys -def run(): +def run(user_ns=None): a = docopt.docopt(__doc__) vi_mode = bool(a['--vi']) @@ -55,7 +55,8 @@ def run(): # Create an empty namespace for this interactive shell. (If we don't do # that, all the variables from this function will become available in # the IPython shell.) - user_ns = {} + if user_ns is None: + user_ns = {} # Startup path startup_paths = [] From 8a5e66e576abf67382bfe03ebfc6ec341b16caac Mon Sep 17 00:00:00 2001 From: Peter Holloway Date: Mon, 3 Jul 2017 13:09:34 +0100 Subject: [PATCH 177/470] Use __main__.__dict__ for globals as well as locals when running ptpython --- ptpython/entry_points/run_ptpython.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ptpython/entry_points/run_ptpython.py b/ptpython/entry_points/run_ptpython.py index e681cc83..c1ec22d1 100644 --- a/ptpython/entry_points/run_ptpython.py +++ b/ptpython/entry_points/run_ptpython.py @@ -69,6 +69,7 @@ def configure(repl): history_filename=os.path.join(config_dir, 'history'), configure=configure, locals=__main__.__dict__, + globals=__main__.__dict__, startup_paths=startup_paths, title='Python REPL (ptpython)') From 86d9677d770b81347b63348eaaae6275fe97e02c Mon Sep 17 00:00:00 2001 From: Israel Blancas Date: Mon, 10 Jul 2017 00:12:56 +0200 Subject: [PATCH 178/470] The Jedi's website was not correct --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index f5752004..9650490b 100644 --- a/README.rst +++ b/README.rst @@ -183,7 +183,7 @@ Special thanks to ***************** - `Pygments `_: Syntax highlighter. -- `Jedi `_: Autocompletion library. +- `Jedi `_: Autocompletion library. - `Docopt `_: Command-line interface description language. - `wcwidth `_: Determine columns needed for a wide characters. - `prompt_toolkit `_ for the interface. From 92d3d08742a733b8fa1d40d589717f974246d829 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 27 Jul 2017 12:19:18 +0200 Subject: [PATCH 179/470] Release 0.40 --- CHANGELOG | 14 +++++++++++++- setup.py | 2 +- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 1457f5e0..872e153b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,7 +1,19 @@ CHANGELOG ========= -0.38: 2017-03-28 +0.40: 2017-07-27 +---------------- + +Fixes: +- Fix in setup.py: `extras_require` instead of `extra_require`. +- Catch AssertionError in Jedi completion. +- Use __main__.__dict__ for globals and locals. + +New features: +- Support `user_ns` arg for compatibility with start_ipython. + + +0.39: 2017-03-28 ---------------- Fixes: diff --git a/setup.py b/setup.py index aad4c625..458cf036 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ setup( name='ptpython', author='Jonathan Slenders', - version='0.39', + version='0.40', url='https://github.com/jonathanslenders/ptpython', description='Python REPL build on top of prompt_toolkit', long_description=long_description, From ebef06eb74ae3523e5842960b100624c5f93a62d Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 27 Jul 2017 12:25:08 +0200 Subject: [PATCH 180/470] Catch Jedi SystemError. --- ptpython/completer.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ptpython/completer.py b/ptpython/completer.py index 90da6451..bba9aec6 100644 --- a/ptpython/completer.py +++ b/ptpython/completer.py @@ -151,6 +151,10 @@ def get_completions(self, document, complete_event): # In jedi.parser.__init__.py: 227, in remove_last_newline, # the assertion "newline.value.endswith('\n')" can fail. pass + except SystemError: + # File "jedi/api/helpers.py", line 140, in get_stack_at_position + # raise SystemError("This really shouldn't happen. There's a bug in Jedi.") + pass else: for c in completions: yield Completion(c.name_with_symbols, len(c.complete) - len(c.name_with_symbols), From da2c5281f60c2d8a92749709219771ffaa84220f Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 27 Jul 2017 12:26:10 +0200 Subject: [PATCH 181/470] Release 0.41 --- CHANGELOG | 7 +++++++ setup.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 872e153b..fdc3b838 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,13 @@ CHANGELOG ========= +0.41: 2017-07-27 +---------------- + +Fixes: +- Catch Jedi SystemError. + + 0.40: 2017-07-27 ---------------- diff --git a/setup.py b/setup.py index 458cf036..351e473c 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ setup( name='ptpython', author='Jonathan Slenders', - version='0.40', + version='0.41', url='https://github.com/jonathanslenders/ptpython', description='Python REPL build on top of prompt_toolkit', long_description=long_description, From fd4f2a9c1a3bd042cd710ad6c8722163f5390701 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Fri, 13 Jan 2017 15:29:14 +0100 Subject: [PATCH 182/470] Prompt_toolkit 2.0 changes. --- examples/python-input.py | 12 +- ptpython/eventloop.py | 6 +- ptpython/history_browser.py | 447 ++++++++++++++++++++---------------- ptpython/ipython.py | 2 +- ptpython/key_bindings.py | 91 +++++--- ptpython/layout.py | 186 ++++++++------- ptpython/prompt_style.py | 2 +- ptpython/python_input.py | 249 +++++++++----------- ptpython/repl.py | 93 ++++---- ptpython/style.py | 50 ++-- ptpython/utils.py | 8 +- ptpython/validator.py | 1 - 12 files changed, 598 insertions(+), 549 deletions(-) diff --git a/examples/python-input.py b/examples/python-input.py index 9acb8d9e..53aaa2dd 100755 --- a/examples/python-input.py +++ b/examples/python-input.py @@ -3,19 +3,19 @@ """ from __future__ import unicode_literals -from prompt_toolkit.shortcuts import create_eventloop -from ptpython.python_input import PythonCommandLineInterface +from prompt_toolkit.eventloop.defaults import create_event_loop +from ptpython.python_input import PythonInput def main(): - eventloop = create_eventloop() + loop = create_event_loop() try: - cli = PythonCommandLineInterface(eventloop) + prompt = PythonInput(loop=loop) - code_obj = cli.run() + code_obj = prompt.app.run() print('You said: ' + code_obj.text) finally: - eventloop.close() + loop.close() if __name__ == '__main__': diff --git a/ptpython/eventloop.py b/ptpython/eventloop.py index b51e877b..3d6103da 100644 --- a/ptpython/eventloop.py +++ b/ptpython/eventloop.py @@ -7,7 +7,7 @@ in readline. ``prompt-toolkit`` doesn't understand that input hook, but this will fix it for Tk.) """ -from prompt_toolkit.shortcuts import create_eventloop as _create_eventloop +from prompt_toolkit.eventloop.defaults import create_event_loop as _create_event_loop import sys import time @@ -71,5 +71,5 @@ def _inputhook(inputhook_context): _inputhook_tk(inputhook_context) -def create_eventloop(): - return _create_eventloop(inputhook=_inputhook) +def create_event_loop(): + return _create_event_loop(inputhook=_inputhook) diff --git a/ptpython/history_browser.py b/ptpython/history_browser.py index b6fb8f07..b9be7488 100644 --- a/ptpython/history_browser.py +++ b/ptpython/history_browser.py @@ -7,25 +7,24 @@ from __future__ import unicode_literals from prompt_toolkit.application import Application -from prompt_toolkit.buffer import Buffer, AcceptAction -from prompt_toolkit.buffer_mapping import BufferMapping +from prompt_toolkit.buffer import Buffer from prompt_toolkit.document import Document from prompt_toolkit.enums import DEFAULT_BUFFER -from prompt_toolkit.filters import Condition, HasFocus, InFocusStack +from prompt_toolkit.filters import Condition, has_focus +from prompt_toolkit.key_binding import KeyBindings, merge_key_bindings from prompt_toolkit.key_binding.defaults import load_key_bindings from prompt_toolkit.keys import Keys -from prompt_toolkit.layout.containers import HSplit, VSplit, Window, FloatContainer, Float, ConditionalContainer, Container, ScrollOffsets -from prompt_toolkit.layout.controls import BufferControl, FillControl -from prompt_toolkit.layout.dimension import LayoutDimension as D +from prompt_toolkit.layout.containers import HSplit, VSplit, Window, FloatContainer, Float, ConditionalContainer, Container, ScrollOffsets, Align +from prompt_toolkit.layout.controls import BufferControl, TokenListControl +from prompt_toolkit.layout.dimension import Dimension as D +from prompt_toolkit.layout.layout import Layout from prompt_toolkit.layout.lexers import PygmentsLexer from prompt_toolkit.layout.margins import Margin, ScrollbarMargin -from prompt_toolkit.layout.processors import Processor, Transformation, HighlightSearchProcessor, HighlightSelectionProcessor -from prompt_toolkit.layout.screen import Char +from prompt_toolkit.layout.processors import Processor, Transformation, HighlightSearchProcessor, HighlightSelectionProcessor, merge_processors from prompt_toolkit.layout.toolbars import ArgToolbar, SearchToolbar -from prompt_toolkit.layout.toolbars import TokenListToolbar from prompt_toolkit.layout.utils import token_list_to_text +from prompt_toolkit.token import Token from pygments.lexers import RstLexer -from pygments.token import Token from .utils import if_mousedown @@ -39,9 +38,6 @@ from pygments.lexers import Python3Lexer as PythonLexer -HISTORY_BUFFER = 'HISTORY_BUFFER' -HELP_BUFFER = 'HELP_BUFFER' - HISTORY_COUNT = 2000 __all__ = ( @@ -110,117 +106,143 @@ def create_popup_window(title, body): return HSplit([ VSplit([ Window(width=D.exact(1), height=D.exact(1), - content=FillControl(BORDER.TOP_LEFT, token=Token.Window.Border)), - TokenListToolbar( - get_tokens=lambda cli: [(Token.Window.Title, ' %s ' % title)], - align_center=True, - default_char=Char(BORDER.HORIZONTAL, Token.Window.Border)), + char=BORDER.TOP_LEFT, + token=Token.Window.Border), + Window( + content=TokenListControl( + get_tokens=lambda app: [(Token.Window.Title, ' %s ' % title)]), + align=Align.CENTER, + char=BORDER.HORIZONTAL, + token=Token.Window.Border), Window(width=D.exact(1), height=D.exact(1), - content=FillControl(BORDER.TOP_RIGHT, token=Token.Window.Border)), + char=BORDER.TOP_RIGHT, + token=Token.Window.Border), ]), VSplit([ Window(width=D.exact(1), - content=FillControl(BORDER.VERTICAL, token=Token.Window.Border)), + char=BORDER.VERTICAL, + token=Token.Window.Border), body, Window(width=D.exact(1), - content=FillControl(BORDER.VERTICAL, token=Token.Window.Border)), + char=BORDER.VERTICAL, + token=Token.Window.Border), ]), VSplit([ Window(width=D.exact(1), height=D.exact(1), - content=FillControl(BORDER.BOTTOM_LEFT, token=Token.Window.Border)), + char=BORDER.BOTTOM_LEFT, + token=Token.Window.Border), Window(height=D.exact(1), - content=FillControl(BORDER.HORIZONTAL, token=Token.Window.Border)), + char=BORDER.HORIZONTAL, + token=Token.Window.Border), Window(width=D.exact(1), height=D.exact(1), - content=FillControl(BORDER.BOTTOM_RIGHT, token=Token.Window.Border)), + char=BORDER.BOTTOM_RIGHT, + token=Token.Window.Border), ]), ]) -def create_layout(python_input, history_mapping): +class HistoryLayout(object): """ Create and return a `Container` instance for the history application. """ - processors = [ - HighlightSearchProcessor(preview_search=True), - HighlightSelectionProcessor()] - - help_window = create_popup_window( - title='History Help', - body=Window( - content=BufferControl( - buffer_name=HELP_BUFFER, - default_char=Char(token=Token), - lexer=PygmentsLexer(RstLexer), - input_processors=processors), - right_margins=[ScrollbarMargin()], - scroll_offsets=ScrollOffsets(top=2, bottom=2))) - - return HSplit([ - # Top title bar. - TokenListToolbar( - get_tokens=_get_top_toolbar_tokens, - align_center=True, - default_char=Char(' ', Token.Toolbar.Status)), - FloatContainer( - content=VSplit([ - # Left side: history. - Window( - content=BufferControl( - buffer_name=HISTORY_BUFFER, - lexer=PygmentsLexer(PythonLexer), - input_processors=processors), - wrap_lines=False, - left_margins=[HistoryMargin(history_mapping)], - scroll_offsets=ScrollOffsets(top=2, bottom=2)), - # Separator. - Window(width=D.exact(1), - content=FillControl(BORDER.LIGHT_VERTICAL, token=Token.Separator)), - # Right side: result. - Window( - content=BufferControl( - buffer_name=DEFAULT_BUFFER, - input_processors=processors + [GrayExistingText(history_mapping)], - lexer=PygmentsLexer(PythonLexer)), - wrap_lines=False, - left_margins=[ResultMargin(history_mapping)], - scroll_offsets=ScrollOffsets(top=2, bottom=2)), - ]), - floats=[ - # Help text as a float. - Float(width=60, top=3, bottom=2, - content=ConditionalContainer( - # (We use InFocusStack, because it's possible to search - # through the help text as well, and at that point the search - # buffer has the focus.) - content=help_window, filter=InFocusStack(HELP_BUFFER))), - ] - ), - # Bottom toolbars. - ArgToolbar(), - SearchToolbar(), - TokenListToolbar( - get_tokens=partial(_get_bottom_toolbar_tokens, python_input=python_input), - default_char=Char(' ', Token.Toolbar.Status)), - ]) - - -def _get_top_toolbar_tokens(cli): + def __init__(self, history): + default_processors = [ + HighlightSearchProcessor(preview_search=True), + HighlightSelectionProcessor() + ] + + self.help_buffer_control = BufferControl( + buffer=history.help_buffer, + lexer=PygmentsLexer(RstLexer), + input_processor=merge_processors(default_processors)) + + help_window = create_popup_window( + title='History Help', + body=Window( + content=self.help_buffer_control, + right_margins=[ScrollbarMargin()], + scroll_offsets=ScrollOffsets(top=2, bottom=2), + transparent=False)) + + self.default_buffer_control = BufferControl( + buffer=history.default_buffer, + input_processor=merge_processors( + default_processors + [GrayExistingText(history.history_mapping)]), + lexer=PygmentsLexer(PythonLexer)) + + self.history_buffer_control = BufferControl( + buffer=history.history_buffer, + lexer=PygmentsLexer(PythonLexer), + input_processor=merge_processors(default_processors)) + + history_window = Window( + content=self.history_buffer_control, + wrap_lines=False, + left_margins=[HistoryMargin(history)], + scroll_offsets=ScrollOffsets(top=2, bottom=2)) + + self.root_container = HSplit([ + # Top title bar. + Window( + content=TokenListControl(get_tokens=_get_top_toolbar_tokens), + align=Align.CENTER, + token=Token.Toolbar.Status), + FloatContainer( + content=VSplit([ + # Left side: history. + history_window, + # Separator. + Window(width=D.exact(1), + char=BORDER.LIGHT_VERTICAL, + token=Token.Separator), + # Right side: result. + Window( + content=self.default_buffer_control, + wrap_lines=False, + left_margins=[ResultMargin(history)], + scroll_offsets=ScrollOffsets(top=2, bottom=2)), + ]), + floats=[ + # Help text as a float. + Float(width=60, top=3, bottom=2, + content=ConditionalContainer( + # XXXX XXX + # (We use InFocusStack, because it's possible to search + # through the help text as well, and at that point the search + # buffer has the focus.) + content=help_window, filter=has_focus(history.help_buffer))), # XXX + ] + ), + # Bottom toolbars. + ArgToolbar(), + # SearchToolbar(), # XXX + Window( + content=TokenListControl( + get_tokens=partial(_get_bottom_toolbar_tokens, history=history)), + token=Token.Toolbar.Status), + ]) + + self.layout = Layout(self.root_container, history_window) + + +def _get_top_toolbar_tokens(app): return [(Token.Toolbar.Status.Title, 'History browser - Insert from history')] -def _get_bottom_toolbar_tokens(cli, python_input): +def _get_bottom_toolbar_tokens(app, history): + python_input = history.python_input @if_mousedown - def f1(cli, mouse_event): - _toggle_help(cli) + def f1(app, mouse_event): + _toggle_help(history) @if_mousedown - def tab(cli, mouse_event): - _select_other_window(cli) + def tab(app, mouse_event): + _select_other_window(history) return [ (Token.Toolbar.Status, ' ') - ] + get_inputmode_tokens(cli, python_input) + [ + ] + get_inputmode_tokens(app, python_input) + [ (Token.Toolbar.Status, ' '), (Token.Toolbar.Status.Key, '[Space]'), (Token.Toolbar.Status, ' Toggle '), @@ -238,14 +260,15 @@ class HistoryMargin(Margin): Margin for the history buffer. This displays a green bar for the selected entries. """ - def __init__(self, history_mapping): - self.history_mapping = history_mapping + def __init__(self, history): + self.history_buffer = history.history_buffer + self.history_mapping = history.history_mapping - def get_width(self, cli, ui_content): + def get_width(self, app, ui_content): return 2 - def create_margin(self, cli, window_render_info, width, height): - document = cli.buffers[HISTORY_BUFFER].document + def create_margin(self, app, window_render_info, width, height): + document = self.history_buffer.document lines_starting_new_entries = self.history_mapping.lines_starting_new_entries selected_lines = self.history_mapping.selected_lines @@ -283,14 +306,15 @@ class ResultMargin(Margin): """ The margin to be shown in the result pane. """ - def __init__(self, history_mapping): - self.history_mapping = history_mapping + def __init__(self, history): + self.history_mapping = history.history_mapping + self.history_buffer = history.history_buffer - def get_width(self, cli, ui_content): + def get_width(self, app, ui_content): return 2 - def create_margin(self, cli, window_render_info, width, height): - document = cli.buffers[DEFAULT_BUFFER].document + def create_margin(self, app, window_render_info, width, height): + document = self.history_buffer.document current_lineno = document.cursor_position_row offset = self.history_mapping.result_line_offset #original_document.cursor_position_row @@ -315,7 +339,7 @@ def create_margin(self, cli, window_render_info, width, height): return result - def invalidation_hash(self, cli, document): + def invalidation_hash(self, app, document): return document.cursor_position_row @@ -327,7 +351,11 @@ def __init__(self, history_mapping): self.history_mapping = history_mapping self._lines_before = len(history_mapping.original_document.text_before_cursor.splitlines()) - def apply_transformation(self, cli, document, lineno, source_to_display, tokens): + def apply_transformation(self, transformation_input): + app = transformation_input.app + lineno = transformation_input.lineno + tokens = transformation_input.tokens + if (lineno < self._lines_before or lineno >= self._lines_before + len(self.history_mapping.selected_lines)): text = token_list_to_text(tokens) @@ -340,7 +368,8 @@ class HistoryMapping(object): """ Keep a list of all the lines from the history and the selected lines. """ - def __init__(self, python_history, original_document): + def __init__(self, history, python_history, original_document): + self.history = history self.python_history = python_history self.original_document = original_document @@ -392,40 +421,43 @@ def get_new_document(self, cursor_pos=None): cursor_pos = len(text) return Document(text, cursor_pos) - def update_default_buffer(self, cli): - b = cli.buffers[DEFAULT_BUFFER] + def update_default_buffer(self, app): + b = self.history.default_buffer b.set_document( self.get_new_document(b.cursor_position), bypass_readonly=True) -def _toggle_help(cli): +def _toggle_help(history): " Display/hide help. " - if cli.current_buffer_name == HELP_BUFFER: - cli.pop_focus() + help_buffer_control = history.history_layout.help_buffer_control + + if history.app.layout.current_control == help_buffer_control: + history.app.layout.pop_focus() else: - cli.push_focus(HELP_BUFFER) + history.app.layout.current_control = help_buffer_control -def _select_other_window(cli): +def _select_other_window(history): " Toggle focus between left/right window. " - if cli.current_buffer_name == HISTORY_BUFFER: - cli.focus(DEFAULT_BUFFER) + current_buffer = history.app.current_buffer + layout = history.history_layout.layout + + if current_buffer == history.history_buffer: + layout.current_control = history.history_layout.default_buffer_control - elif cli.current_buffer_name == DEFAULT_BUFFER: - cli.focus(HISTORY_BUFFER) + elif current_buffer == history.default_buffer: + layout.current_control = history.history_layout.history_buffer_control -def create_key_bindings(python_input, history_mapping): +def create_key_bindings(history, python_input, history_mapping): """ Key bindings. """ - registry = load_key_bindings( - enable_search=True, - enable_extra_page_navigation=True) - handle = registry.add_binding + bindings = KeyBindings() + handle = bindings.add - @handle(' ', filter=HasFocus(HISTORY_BUFFER)) + @handle(' ', filter=has_focus(history.history_buffer)) def _(event): """ Space: select/deselect line from history pane. @@ -436,14 +468,14 @@ def _(event): if line_no in history_mapping.selected_lines: # Remove line. history_mapping.selected_lines.remove(line_no) - history_mapping.update_default_buffer(event.cli) + history_mapping.update_default_buffer(event.app) else: # Add line. history_mapping.selected_lines.add(line_no) - history_mapping.update_default_buffer(event.cli) + history_mapping.update_default_buffer(event.app) # Update cursor position - default_buffer = event.cli.buffers[DEFAULT_BUFFER] + default_buffer = history.default_buffer default_lineno = sorted(history_mapping.selected_lines).index(line_no) + \ history_mapping.result_line_offset default_buffer.cursor_position = \ @@ -453,9 +485,9 @@ def _(event): # space to select a region.) b.cursor_position = b.document.translate_row_col_to_index(line_no + 1, 0) - @handle(' ', filter=HasFocus(DEFAULT_BUFFER)) - @handle(Keys.Delete, filter=HasFocus(DEFAULT_BUFFER)) - @handle(Keys.ControlH, filter=HasFocus(DEFAULT_BUFFER)) + @handle(' ', filter=has_focus(DEFAULT_BUFFER)) + @handle(Keys.Delete, filter=has_focus(DEFAULT_BUFFER)) + @handle(Keys.ControlH, filter=has_focus(DEFAULT_BUFFER)) def _(event): """ Space: remove line from default pane. @@ -471,10 +503,10 @@ def _(event): else: history_mapping.selected_lines.remove(history_lineno) - history_mapping.update_default_buffer(event.cli) + history_mapping.update_default_buffer(event.app) - help_focussed = HasFocus(HELP_BUFFER) - main_buffer_focussed = HasFocus(HISTORY_BUFFER) | HasFocus(DEFAULT_BUFFER) + help_focussed = has_focus(history.help_buffer) + main_buffer_focussed = has_focus(history.history_buffer) | has_focus(history.default_buffer) @handle(Keys.Tab, filter=main_buffer_focussed) @handle(Keys.ControlX, filter=main_buffer_focussed, eager=True) @@ -482,7 +514,7 @@ def _(event): @handle(Keys.ControlW, filter=main_buffer_focussed) def _(event): " Select other window. " - _select_other_window(event.cli) + _select_other_window(history) @handle(Keys.F4) def _(event): @@ -492,15 +524,15 @@ def _(event): @handle(Keys.F1) def _(event): " Display/hide help. " - _toggle_help(event.cli) + _toggle_help(history) - @handle(Keys.ControlJ, filter=help_focussed) + @handle(Keys.Enter, filter=help_focussed) @handle(Keys.ControlC, filter=help_focussed) @handle(Keys.ControlG, filter=help_focussed) @handle(Keys.Escape, filter=help_focussed) def _(event): " Leave help. " - event.cli.pop_focus() + event.app.layout.pop_focus() @handle('q', filter=main_buffer_focussed) @handle(Keys.F3, filter=main_buffer_focussed) @@ -508,89 +540,102 @@ def _(event): @handle(Keys.ControlG, filter=main_buffer_focussed) def _(event): " Cancel and go back. " - event.cli.set_return_value(None) + event.app.set_return_value(None) - enable_system_bindings = Condition(lambda cli: python_input.enable_system_bindings) + @handle(Keys.Enter, filter=main_buffer_focussed) + def _(event): + " Accept input. " + event.app.set_return_value(history.default_buffer.document) + + enable_system_bindings = Condition(lambda app: python_input.enable_system_bindings) @handle(Keys.ControlZ, filter=enable_system_bindings) def _(event): " Suspend to background. " - event.cli.suspend_to_background() + event.app.suspend_to_background() - return registry + return merge_key_bindings([ + load_key_bindings( + enable_search=True, + enable_extra_page_navigation=True), + bindings + ]) -def create_history_application(python_input, original_document): - """ - Create an `Application` for the history screen. - This has to be run as a sub application of `python_input`. +class History(object): + def __init__(self, python_input, original_document): + """ + Create an `Application` for the history screen. + This has to be run as a sub application of `python_input`. - When this application runs and returns, it retuns the selected lines. - """ - history_mapping = HistoryMapping(python_input.history, original_document) + When this application runs and returns, it retuns the selected lines. + """ + self.python_input = python_input - def default_buffer_pos_changed(_): + history_mapping = HistoryMapping(self, python_input.history, original_document) + self.history_mapping = history_mapping + + self.history_buffer = Buffer( + loop=python_input.loop, + document=Document(history_mapping.concatenated_history), + on_cursor_position_changed=self._history_buffer_pos_changed, + accept_handler=( + lambda app, buffer: app.set_return_value(self.default_buffer.text)), + read_only=True) + + self.default_buffer = Buffer( + loop=python_input.loop, + name=DEFAULT_BUFFER, + document=history_mapping.get_new_document(), + on_cursor_position_changed=self._default_buffer_pos_changed, + read_only=True) + + self.help_buffer = Buffer( + loop=python_input.loop, + document=Document(HELP_TEXT, 0), + read_only=True + ) + + self.history_layout = HistoryLayout(self) + + self.app = Application( + loop=python_input.loop, + layout=self.history_layout.layout, + use_alternate_screen=True, + style=python_input._current_style, + mouse_support=Condition(lambda app: python_input.enable_mouse_support), + key_bindings=create_key_bindings(self, python_input, history_mapping) + ) + + def _default_buffer_pos_changed(self, _): """ When the cursor changes in the default buffer. Synchronize with history buffer. """ # Only when this buffer has the focus. - if buffer_mapping.focus_stack[-1] == DEFAULT_BUFFER: + if self.app.current_buffer == self.default_buffer: try: - line_no = default_buffer.document.cursor_position_row - \ - history_mapping.result_line_offset + line_no = self.default_buffer.document.cursor_position_row - \ + self.history_mapping.result_line_offset if line_no < 0: # When the cursor is above the inserted region. raise IndexError - history_lineno = sorted(history_mapping.selected_lines)[line_no] + history_lineno = sorted(self.history_mapping.selected_lines)[line_no] except IndexError: pass else: - history_buffer.cursor_position = \ - history_buffer.document.translate_row_col_to_index(history_lineno, 0) + self.history_buffer.cursor_position = \ + self.history_buffer.document.translate_row_col_to_index(history_lineno, 0) - def history_buffer_pos_changed(_): + def _history_buffer_pos_changed(self, _): """ When the cursor changes in the history buffer. Synchronize. """ # Only when this buffer has the focus. - if buffer_mapping.focus_stack[-1] == HISTORY_BUFFER: - line_no = history_buffer.document.cursor_position_row - - if line_no in history_mapping.selected_lines: - default_lineno = sorted(history_mapping.selected_lines).index(line_no) + \ - history_mapping.result_line_offset - - default_buffer.cursor_position = \ - default_buffer.document.translate_row_col_to_index(default_lineno, 0) - - history_buffer = Buffer( - initial_document=Document(history_mapping.concatenated_history), - on_cursor_position_changed=history_buffer_pos_changed, - accept_action=AcceptAction( - lambda cli, buffer: cli.set_return_value(default_buffer.document)), - read_only=True) - - default_buffer = Buffer( - initial_document=history_mapping.get_new_document(), - on_cursor_position_changed=default_buffer_pos_changed, - read_only=True) - - help_buffer = Buffer( - initial_document=Document(HELP_TEXT, 0), - accept_action=AcceptAction.IGNORE, - read_only=True - ) - - buffer_mapping = BufferMapping({ - HISTORY_BUFFER: history_buffer, - DEFAULT_BUFFER: default_buffer, - HELP_BUFFER: help_buffer, - }, initial=HISTORY_BUFFER) - - application = Application( - layout=create_layout(python_input, history_mapping), - use_alternate_screen=True, - buffers=buffer_mapping, - style=python_input._current_style, - mouse_support=Condition(lambda cli: python_input.enable_mouse_support), - key_bindings_registry=create_key_bindings(python_input, history_mapping) - ) - return application + if self.app.current_buffer == self.history_buffer: + line_no = self.history_buffer.document.cursor_position_row + + if line_no in self.history_mapping.selected_lines: + default_lineno = sorted(self.history_mapping.selected_lines).index(line_no) + \ + self.history_mapping.result_line_offset + + self.default_buffer.cursor_position = \ + self.default_buffer.document.translate_row_col_to_index(default_lineno, 0) + diff --git a/ptpython/ipython.py b/ptpython/ipython.py index d66d09b2..957ad95a 100644 --- a/ptpython/ipython.py +++ b/ptpython/ipython.py @@ -18,6 +18,7 @@ from prompt_toolkit.document import Document from prompt_toolkit.interface import CommandLineInterface from prompt_toolkit.layout.lexers import PygmentsLexer, SimpleLexer +from prompt_toolkit.token import Token from .python_input import PythonInput, PythonValidator, PythonCompleter from .eventloop import create_eventloop @@ -29,7 +30,6 @@ from IPython.core.inputsplitter import IPythonInputSplitter from pygments.lexers import PythonLexer, BashLexer -from pygments.token import Token from ptpython.prompt_style import PromptStyle __all__ = ( diff --git a/ptpython/key_bindings.py b/ptpython/key_bindings.py index d0200f38..43cce9f2 100644 --- a/ptpython/key_bindings.py +++ b/ptpython/key_bindings.py @@ -2,10 +2,10 @@ from prompt_toolkit.document import Document from prompt_toolkit.enums import DEFAULT_BUFFER -from prompt_toolkit.filters import HasSelection, IsMultiline, Filter, HasFocus, Condition, ViInsertMode, EmacsInsertMode -from prompt_toolkit.key_binding.vi_state import InputMode -from prompt_toolkit.key_binding.registry import Registry +from prompt_toolkit.filters import HasSelection, HasFocus, Condition, ViInsertMode, EmacsInsertMode, EmacsMode +from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.keys import Keys +from .utils import document_is_multiline_python __all__ = ( 'load_python_bindings', @@ -14,7 +14,8 @@ ) -class TabShouldInsertWhitespaceFilter(Filter): +@Condition +def tab_should_insert_whitespace(app): """ When the 'tab' key is pressed with only whitespace character before the cursor, do autocompletion. Otherwise, insert indentation. @@ -23,30 +24,28 @@ class TabShouldInsertWhitespaceFilter(Filter): completion. It doesn't make sense to start the first line with indentation. """ - def __call__(self, cli): - b = cli.current_buffer - before_cursor = b.document.current_line_before_cursor + b = app.current_buffer + before_cursor = b.document.current_line_before_cursor - return bool(b.text and (not before_cursor or before_cursor.isspace())) + return bool(b.text and (not before_cursor or before_cursor.isspace())) def load_python_bindings(python_input): """ Custom key bindings. """ - registry = Registry() + bindings = KeyBindings() - sidebar_visible = Condition(lambda cli: python_input.show_sidebar) - handle = registry.add_binding + sidebar_visible = Condition(lambda app: python_input.show_sidebar) + handle = bindings.add has_selection = HasSelection() - vi_mode_enabled = Condition(lambda cli: python_input.vi_mode) @handle(Keys.ControlL) def _(event): """ Clear whole screen and render again -- also when the sidebar is visible. """ - event.cli.renderer.clear() + event.app.renderer.clear() @handle(Keys.F2) def _(event): @@ -60,7 +59,7 @@ def _(event): """ Select from the history. """ - python_input.enter_history(event.cli) + python_input.enter_history(event.app) @handle(Keys.F4) def _(event): @@ -76,16 +75,40 @@ def _(event): """ python_input.paste_mode = not python_input.paste_mode - @handle(Keys.Tab, filter= ~sidebar_visible & ~has_selection & TabShouldInsertWhitespaceFilter()) + @handle(Keys.Tab, filter= ~sidebar_visible & ~has_selection & tab_should_insert_whitespace) def _(event): """ When tab should insert whitespace, do that instead of completion. """ - event.cli.current_buffer.insert_text(' ') + event.app.current_buffer.insert_text(' ') - @handle(Keys.ControlJ, filter= ~sidebar_visible & ~has_selection & + @Condition + def is_multiline(app): + return document_is_multiline_python(python_input.default_buffer.document) + + @handle(Keys.Enter, filter= ~sidebar_visible & ~has_selection & + (ViInsertMode() | EmacsInsertMode()) & + HasFocus(DEFAULT_BUFFER) & ~is_multiline) + @handle(Keys.Escape, Keys.Enter, filter= ~sidebar_visible & EmacsMode()) + def _(event): + """ + Accept input (for single line input). + """ + b = event.current_buffer + + if b.validate(): + # When the cursor is at the end, and we have an empty line: + # drop the empty lines, but return the value. + + b.document = Document( + text=b.text.rstrip(), + cursor_position=len(b.text.rstrip())) + + b.validate_and_handle(event.app) + + @handle(Keys.Enter, filter= ~sidebar_visible & ~has_selection & (ViInsertMode() | EmacsInsertMode()) & - HasFocus(DEFAULT_BUFFER) & IsMultiline()) + HasFocus(DEFAULT_BUFFER) & is_multiline) def _(event): """ Behaviour of the Enter key. @@ -115,33 +138,33 @@ def at_the_end(b): text=b.text.rstrip(), cursor_position=len(b.text.rstrip())) - b.accept_action.validate_and_handle(event.cli, b) + b.validate_and_handle(event.app) else: auto_newline(b) - @handle(Keys.ControlD, filter=~sidebar_visible & Condition(lambda cli: + @handle(Keys.ControlD, filter=~sidebar_visible & Condition(lambda app: # Only when the `confirm_exit` flag is set. python_input.confirm_exit and # And the current buffer is empty. - cli.current_buffer_name == DEFAULT_BUFFER and - not cli.current_buffer.text)) + app.current_buffer == python_input.default_buffer and + not app.current_buffer.text)) def _(event): """ Override Control-D exit, to ask for confirmation. """ python_input.show_exit_confirmation = True - return registry + return bindings def load_sidebar_bindings(python_input): """ Load bindings for the navigation in the sidebar. """ - registry = Registry() + bindings = KeyBindings() - handle = registry.add_binding - sidebar_visible = Condition(lambda cli: python_input.show_sidebar) + handle = bindings.add + sidebar_visible = Condition(lambda app: python_input.show_sidebar) @handle(Keys.Up, filter=sidebar_visible) @handle(Keys.ControlP, filter=sidebar_visible) @@ -177,33 +200,33 @@ def _(event): @handle(Keys.ControlC, filter=sidebar_visible) @handle(Keys.ControlG, filter=sidebar_visible) @handle(Keys.ControlD, filter=sidebar_visible) - @handle(Keys.ControlJ, filter=sidebar_visible) + @handle(Keys.Enter, filter=sidebar_visible) @handle(Keys.Escape, filter=sidebar_visible) def _(event): " Hide sidebar. " python_input.show_sidebar = False - return registry + return bindings def load_confirm_exit_bindings(python_input): """ Handle yes/no key presses when the exit confirmation is shown. """ - registry = Registry() + bindings = KeyBindings() - handle = registry.add_binding - confirmation_visible = Condition(lambda cli: python_input.show_exit_confirmation) + handle = bindings.add + confirmation_visible = Condition(lambda app: python_input.show_exit_confirmation) @handle('y', filter=confirmation_visible) @handle('Y', filter=confirmation_visible) - @handle(Keys.ControlJ, filter=confirmation_visible) + @handle(Keys.Enter, filter=confirmation_visible) @handle(Keys.ControlD, filter=confirmation_visible) def _(event): """ Really quit. """ - event.cli.exit() + event.app.exit() @handle(Keys.Any, filter=confirmation_visible) def _(event): @@ -212,7 +235,7 @@ def _(event): """ python_input.show_exit_confirmation = False - return registry + return bindings def auto_newline(buffer): diff --git a/ptpython/layout.py b/ptpython/layout.py index 2a403b96..00d4c1a4 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -7,23 +7,24 @@ from prompt_toolkit.filters import IsDone, HasCompletions, RendererHeightIsKnown, HasFocus, Condition from prompt_toolkit.key_binding.vi_state import InputMode from prompt_toolkit.layout.containers import Window, HSplit, VSplit, FloatContainer, Float, ConditionalContainer, ScrollOffsets -from prompt_toolkit.layout.controls import BufferControl, TokenListControl, FillControl -from prompt_toolkit.layout.dimension import LayoutDimension +from prompt_toolkit.layout.controls import BufferControl, TokenListControl +from prompt_toolkit.layout.dimension import Dimension +from prompt_toolkit.layout.layout import Layout from prompt_toolkit.layout.lexers import SimpleLexer from prompt_toolkit.layout.margins import PromptMargin from prompt_toolkit.layout.menus import CompletionsMenu, MultiColumnCompletionsMenu from prompt_toolkit.layout.processors import ConditionalProcessor, AppendAutoSuggestion, HighlightSearchProcessor, HighlightSelectionProcessor, HighlightMatchingBracketProcessor, Processor, Transformation -from prompt_toolkit.layout.screen import Char -from prompt_toolkit.layout.toolbars import CompletionsToolbar, ArgToolbar, SearchToolbar, ValidationToolbar, SystemToolbar, TokenListToolbar +from prompt_toolkit.layout.processors import merge_processors +from prompt_toolkit.layout.toolbars import CompletionsToolbar, ArgToolbar, SearchToolbar, ValidationToolbar, SystemToolbar from prompt_toolkit.layout.utils import token_list_width from prompt_toolkit.reactive import Integer from prompt_toolkit.selection import SelectionType +from prompt_toolkit.token import Token from .filters import HasSignature, ShowSidebar, ShowSignature, ShowDocstring from .utils import if_mousedown from pygments.lexers import PythonLexer -from pygments.token import Token import platform import sys @@ -43,7 +44,7 @@ class DisplayMultipleCursors(Processor): def __init__(self, *a): pass - def apply_transformation(self, cli, document, lineno, + def apply_transformation(self, app, document, lineno, source_to_display, tokens): return Transformation(tokens) @@ -57,22 +58,22 @@ class CompletionVisualisation: def show_completions_toolbar(python_input): - return Condition(lambda cli: python_input.completion_visualisation == CompletionVisualisation.TOOLBAR) + return Condition(lambda app: python_input.completion_visualisation == CompletionVisualisation.TOOLBAR) def show_completions_menu(python_input): - return Condition(lambda cli: python_input.completion_visualisation == CompletionVisualisation.POP_UP) + return Condition(lambda app: python_input.completion_visualisation == CompletionVisualisation.POP_UP) def show_multi_column_completions_menu(python_input): - return Condition(lambda cli: python_input.completion_visualisation == CompletionVisualisation.MULTI_COLUMN) + return Condition(lambda app: python_input.completion_visualisation == CompletionVisualisation.MULTI_COLUMN) def python_sidebar(python_input): """ Create the `Layout` for the sidebar with the configurable options. """ - def get_tokens(cli): + def get_tokens(app): tokens = [] T = Token.Sidebar @@ -87,11 +88,11 @@ def append(index, label, status): selected = index == python_input.selected_option_index @if_mousedown - def select_item(cli, mouse_event): + def select_item(app, mouse_event): python_input.selected_option_index = index @if_mousedown - def goto_next(cli, mouse_event): + def goto_next(app, mouse_event): " Select item and go to next value. " python_input.selected_option_index = index option = python_input.selected_option @@ -124,18 +125,19 @@ def goto_next(cli, mouse_event): return tokens class Control(TokenListControl): - def move_cursor_down(self, cli): + def move_cursor_down(self, app): python_input.selected_option_index += 1 - def move_cursor_up(self, cli): + def move_cursor_up(self, app): python_input.selected_option_index -= 1 return ConditionalContainer( content=Window( - Control(get_tokens, Char(token=Token.Sidebar), - has_focus=ShowSidebar(python_input) & ~IsDone()), - width=LayoutDimension.exact(43), - height=LayoutDimension(min=3), + Control(get_tokens), + # has_focus=ShowSidebar(python_input) & ~IsDone()), + token=Token.Sidebar, + width=Dimension.exact(43), + height=Dimension(min=3), scroll_offsets=ScrollOffsets(top=1, bottom=1)), filter=ShowSidebar(python_input) & ~IsDone()) @@ -144,7 +146,7 @@ def python_sidebar_navigation(python_input): """ Create the `Layout` showing the navigation information for the sidebar. """ - def get_tokens(cli): + def get_tokens(app): tokens = [] T = Token.Sidebar @@ -165,9 +167,10 @@ def get_tokens(cli): return ConditionalContainer( content=Window( - TokenListControl(get_tokens, Char(token=Token.Sidebar)), - width=LayoutDimension.exact(43), - height=LayoutDimension.exact(2)), + TokenListControl(get_tokens), + token=Token.Sidebar, + width=Dimension.exact(43), + height=Dimension.exact(2)), filter=ShowSidebar(python_input) & ~IsDone()) @@ -189,22 +192,23 @@ def get_current_description(): i += 1 return '' - def get_tokens(cli): + def get_help_tokens(app): return [(token, get_current_description())] return ConditionalContainer( content=Window( - TokenListControl(get_tokens, Char(token=token)), - height=LayoutDimension(min=3)), + TokenListControl(get_help_tokens), + token=token, + height=Dimension(min=3)), filter=ShowSidebar(python_input) & - Condition(lambda cli: python_input.show_sidebar_help) & ~IsDone()) + Condition(lambda app: python_input.show_sidebar_help) & ~IsDone()) def signature_toolbar(python_input): """ Return the `Layout` for the signature. """ - def get_tokens(cli): + def get_tokens(app): result = [] append = result.append Signature = Token.Toolbar.Signature @@ -256,7 +260,7 @@ def get_tokens(cli): return ConditionalContainer( content=Window( TokenListControl(get_tokens), - height=LayoutDimension.exact(1)), + height=Dimension.exact(1)), filter= # Show only when there is a signature HasSignature(python_input) & @@ -280,14 +284,14 @@ def __init__(self, python_input): def get_prompt_style(): return python_input.all_prompt_styles[python_input.prompt_style] - def get_prompt(cli): - return get_prompt_style().in_tokens(cli) + def get_prompt(app): + return get_prompt_style().in_tokens(app) - def get_continuation_prompt(cli, width): - return get_prompt_style().in2_tokens(cli, width) + def get_continuation_prompt(app, width): + return get_prompt_style().in2_tokens(app, width) super(PythonPromptMargin, self).__init__(get_prompt, get_continuation_prompt, - show_numbers=Condition(lambda cli: python_input.show_line_numbers)) + show_numbers=Condition(lambda app: python_input.show_line_numbers)) def status_bar(python_input): @@ -297,21 +301,21 @@ def status_bar(python_input): TB = Token.Toolbar.Status @if_mousedown - def toggle_paste_mode(cli, mouse_event): + def toggle_paste_mode(app, mouse_event): python_input.paste_mode = not python_input.paste_mode @if_mousedown - def enter_history(cli, mouse_event): - python_input.enter_history(cli) + def enter_history(app, mouse_event): + python_input.enter_history(app) - def get_tokens(cli): - python_buffer = cli.buffers[DEFAULT_BUFFER] + def get_tokens(app): + python_buffer = python_input.default_buffer result = [] append = result.append append((TB, ' ')) - result.extend(get_inputmode_tokens(cli, python_input)) + result.extend(get_inputmode_tokens(app, python_input)) append((TB, ' ')) # Position in history. @@ -319,9 +323,9 @@ def get_tokens(cli): len(python_buffer._working_lines)))) # Shortcuts. - if not python_input.vi_mode and cli.current_buffer_name == SEARCH_BUFFER: + if not python_input.vi_mode and app.current_buffer == python_input.search_buffer: append((TB, '[Ctrl-G] Cancel search [Enter] Go to this position.')) - elif bool(cli.current_buffer.selection_state) and not python_input.vi_mode: + elif bool(app.current_buffer.selection_state) and not python_input.vi_mode: # Emacs cut/copy keys. append((TB, '[Ctrl-W] Cut [Meta-W] Copy [Ctrl-Y] Paste [Ctrl-G] Cancel')) else: @@ -339,28 +343,27 @@ def get_tokens(cli): return result - return TokenListToolbar( - get_tokens, - default_char=Char(token=TB), - filter=~IsDone() & RendererHeightIsKnown() & - Condition(lambda cli: python_input.show_status_bar and - not python_input.show_exit_confirmation)) + return ConditionalContainer( + content=Window(content=TokenListControl(get_tokens), token=TB), + filter=~IsDone() & RendererHeightIsKnown() & + Condition(lambda app: python_input.show_status_bar and + not python_input.show_exit_confirmation)) -def get_inputmode_tokens(cli, python_input): +def get_inputmode_tokens(app, python_input): """ Return current input mode as a list of (token, text) tuples for use in a toolbar. - :param cli: `CommandLineInterface` instance. + :param app: `CommandLineInterface` instance. """ @if_mousedown - def toggle_vi_mode(cli, mouse_event): + def toggle_vi_mode(app, mouse_event): python_input.vi_mode = not python_input.vi_mode token = Token.Toolbar.Status - mode = cli.vi_state.input_mode + mode = app.vi_state.input_mode result = [] append = result.append @@ -368,13 +371,13 @@ def toggle_vi_mode(cli, mouse_event): # InputMode if python_input.vi_mode: - if bool(cli.current_buffer.selection_state): - if cli.current_buffer.selection_state.type == SelectionType.LINES: + if bool(app.current_buffer.selection_state): + if app.current_buffer.selection_state.type == SelectionType.LINES: append((token.InputMode, 'Vi (VISUAL LINE)', toggle_vi_mode)) - elif cli.current_buffer.selection_state.type == SelectionType.CHARACTERS: + elif app.current_buffer.selection_state.type == SelectionType.CHARACTERS: append((token.InputMode, 'Vi (VISUAL)', toggle_vi_mode)) append((token, ' ')) - elif cli.current_buffer.selection_state.type == 'BLOCK': + elif app.current_buffer.selection_state.type == 'BLOCK': append((token.InputMode, 'Vi (VISUAL BLOCK)', toggle_vi_mode)) append((token, ' ')) elif mode in (InputMode.INSERT, 'vi-insert-multiple'): @@ -399,7 +402,7 @@ def show_sidebar_button_info(python_input): (The right part of the status bar.) """ @if_mousedown - def toggle_sidebar(cli, mouse_event): + def toggle_sidebar(app, mouse_event): " Click handler for the menu. " python_input.show_sidebar = not python_input.show_sidebar @@ -416,17 +419,18 @@ def toggle_sidebar(cli, mouse_event): ] width = token_list_width(tokens) - def get_tokens(cli): + def get_tokens(app): # Python version return tokens return ConditionalContainer( content=Window( - TokenListControl(get_tokens, default_char=Char(token=token)), - height=LayoutDimension.exact(1), - width=LayoutDimension.exact(width)), + TokenListControl(get_tokens), + token=token, + height=Dimension.exact(1), + width=Dimension.exact(width)), filter=~IsDone() & RendererHeightIsKnown() & - Condition(lambda cli: python_input.show_status_bar and + Condition(lambda app: python_input.show_status_bar and not python_input.show_exit_confirmation)) @@ -434,7 +438,7 @@ def exit_confirmation(python_input, token=Token.ExitConfirmation): """ Create `Layout` for the exit message. """ - def get_tokens(cli): + def get_tokens(app): # Show "Do you really want to exit?" return [ (token, '\n %s ([y]/n)' % python_input.exit_message), @@ -442,11 +446,10 @@ def get_tokens(cli): (token, ' \n'), ] - visible = ~IsDone() & Condition(lambda cli: python_input.show_exit_confirmation) + visible = ~IsDone() & Condition(lambda app: python_input.show_exit_confirmation) return ConditionalContainer( - content=Window(TokenListControl( - get_tokens, default_char=Char(token=token), has_focus=visible)), + content=Window(TokenListControl(get_tokens), token=token), # , has_focus=visible)), filter=visible) @@ -454,18 +457,18 @@ def meta_enter_message(python_input): """ Create the `Layout` for the 'Meta+Enter` message. """ - def get_tokens(cli): + def get_tokens(app): return [(Token.AcceptMessage, ' [Meta+Enter] Execute ')] - def extra_condition(cli): + def extra_condition(app): " Only show when... " - b = cli.buffers[DEFAULT_BUFFER] + b = python_input.default_buffer return ( python_input.show_meta_enter_message and (not b.document.is_cursor_at_the_end or python_input.accept_input_on_enter is None) and - b.is_multiline()) + '\n' in b.text) visible = ~IsDone() & HasFocus(DEFAULT_BUFFER) & Condition(extra_condition) @@ -478,19 +481,21 @@ def create_layout(python_input, lexer=PythonLexer, extra_body=None, extra_toolbars=None, extra_buffer_processors=None, input_buffer_height=None): - D = LayoutDimension + D = Dimension extra_body = [extra_body] if extra_body else [] extra_toolbars = extra_toolbars or [] extra_buffer_processors = extra_buffer_processors or [] input_buffer_height = input_buffer_height or D(min=6) + search_toolbar = SearchToolbar(python_input.search_buffer) + def create_python_input_window(): - def menu_position(cli): + def menu_position(app): """ When there is no autocompletion menu to be shown, and we have a signature, set the pop-up position at `bracket_start`. """ - b = cli.buffers[DEFAULT_BUFFER] + b = python_input.default_buffer if b.complete_state is None and python_input.signatures: row, col = python_input.signatures[0].bracket_start @@ -499,24 +504,25 @@ def menu_position(cli): return Window( BufferControl( - buffer_name=DEFAULT_BUFFER, + buffer=python_input.default_buffer, + search_buffer_control=search_toolbar.control, lexer=lexer, - input_processors=[ + input_processor=merge_processors([ ConditionalProcessor( processor=HighlightSearchProcessor(preview_search=True), - filter=HasFocus(SEARCH_BUFFER), + filter=HasFocus(SEARCH_BUFFER) | HasFocus(search_toolbar.control), ), HighlightSelectionProcessor(), - DisplayMultipleCursors(DEFAULT_BUFFER), + DisplayMultipleCursors(), # Show matching parentheses, but only while editing. ConditionalProcessor( processor=HighlightMatchingBracketProcessor(chars='[](){}'), filter=HasFocus(DEFAULT_BUFFER) & ~IsDone() & - Condition(lambda cli: python_input.highlight_matching_parenthesis)), + Condition(lambda app: python_input.highlight_matching_parenthesis)), ConditionalProcessor( processor=AppendAutoSuggestion(), filter=~IsDone()) - ] + extra_buffer_processors, + ] + extra_buffer_processors), menu_position=menu_position, # Make sure that we always see the result of an reverse-i-search: @@ -527,13 +533,13 @@ def menu_position(cli): # cursor is never below the "Press [Meta+Enter]" message which is a float. scroll_offsets=ScrollOffsets(bottom=1, left=4, right=4), # As long as we're editing, prefer a minimal height of 6. - get_height=(lambda cli: ( - None if cli.is_done or python_input.show_exit_confirmation + get_height=(lambda app: ( + None if app.is_done or python_input.show_exit_confirmation else input_buffer_height)), - wrap_lines=Condition(lambda cli: python_input.wrap_lines), + wrap_lines=Condition(lambda app: python_input.wrap_lines), ) - return HSplit([ + root_container = HSplit([ VSplit([ HSplit([ FloatContainer( @@ -564,21 +570,23 @@ def menu_position(cli): Float(bottom=1, left=1, right=0, content=python_sidebar_help(python_input)), ]), ArgToolbar(), - SearchToolbar(), - SystemToolbar(), + search_toolbar, + SystemToolbar(python_input.loop), ValidationToolbar(), CompletionsToolbar(extra_filter=show_completions_toolbar(python_input)), # Docstring region. ConditionalContainer( - content=Window(height=D.exact(1), - content=FillControl('\u2500', token=Token.Separator)), + content=Window( + height=D.exact(1), + char='\u2500', + token=Token.Separator), filter=HasSignature(python_input) & ShowDocstring(python_input) & ~IsDone()), ConditionalContainer( content=Window( BufferControl( - buffer_name='docstring', - lexer=SimpleLexer(default_token=Token.Docstring), + buffer=python_input.docstring_buffer, + lexer=SimpleLexer(token=Token.Docstring), #lexer=PythonLexer, ), height=D(max=12)), @@ -596,3 +604,5 @@ def menu_position(cli): show_sidebar_button_info(python_input), ]) ]) + + return Layout(root_container) diff --git a/ptpython/prompt_style.py b/ptpython/prompt_style.py index 5a78d19c..5a2c3660 100644 --- a/ptpython/prompt_style.py +++ b/ptpython/prompt_style.py @@ -1,7 +1,7 @@ from __future__ import unicode_literals from abc import ABCMeta, abstractmethod +from prompt_toolkit.token import Token from six import with_metaclass -from pygments.token import Token __all__ = ( 'PromptStyle', diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 748c52d1..05f17d0a 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -1,42 +1,38 @@ """ -CommandLineInterface for reading Python input. +Application for reading Python input. This can be used for creation of Python REPLs. - -:: - - cli = PythonCommandLineInterface() - cli.run() """ from __future__ import unicode_literals -from prompt_toolkit import AbortAction +from prompt_toolkit.application import Application from prompt_toolkit.auto_suggest import AutoSuggestFromHistory, ConditionalAutoSuggest from prompt_toolkit.buffer import Buffer from prompt_toolkit.document import Document from prompt_toolkit.enums import DEFAULT_BUFFER, EditingMode -from prompt_toolkit.filters import Condition, Always +from prompt_toolkit.filters import Condition from prompt_toolkit.history import FileHistory, InMemoryHistory -from prompt_toolkit.interface import CommandLineInterface, Application, AcceptAction -from prompt_toolkit.key_binding.defaults import load_key_bindings_for_prompt, load_mouse_bindings +from prompt_toolkit.input.defaults import create_input +from prompt_toolkit.key_binding import merge_key_bindings, ConditionalKeyBindings, KeyBindings +from prompt_toolkit.key_binding.defaults import load_key_bindings from prompt_toolkit.key_binding.vi_state import InputMode -from prompt_toolkit.key_binding.registry import MergedRegistry, ConditionalRegistry from prompt_toolkit.layout.lexers import PygmentsLexer -from prompt_toolkit.shortcuts import create_output +from prompt_toolkit.output.defaults import create_output from prompt_toolkit.styles import DynamicStyle from prompt_toolkit.utils import is_windows from prompt_toolkit.validation import ConditionalValidator from .completer import PythonCompleter -from .history_browser import create_history_application +from .history_browser import History from .key_bindings import load_python_bindings, load_sidebar_bindings, load_confirm_exit_bindings from .layout import create_layout, CompletionVisualisation from .prompt_style import IPythonPrompt, ClassicPrompt from .style import get_all_code_styles, get_all_ui_styles, generate_style -from .utils import get_jedi_script_from_document, document_is_multiline_python +from .utils import get_jedi_script_from_document from .validator import PythonValidator from functools import partial +import sys import six import __future__ @@ -47,7 +43,6 @@ __all__ = ( 'PythonInput', - 'PythonCommandLineInterface', ) @@ -123,40 +118,42 @@ class PythonInput(object): :: python_input = PythonInput(...) - application = python_input.create_application() - cli = PythonCommandLineInterface(application=application) - python_code = cli.run() + python_code = python_input.run() """ def __init__(self, - get_globals=None, get_locals=None, history_filename=None, + loop, get_globals=None, get_locals=None, history_filename=None, vi_mode=False, + input=None, + output=None, + true_color=False, + # For internal use. + extra_key_bindings=None, _completer=None, _validator=None, - _lexer=None, _extra_buffers=None, _extra_buffer_processors=None, - _on_start=None, + _lexer=None, _extra_buffer_processors=None, _extra_layout_body=None, _extra_toolbars=None, - _input_buffer_height=None, - _accept_action=AcceptAction.RETURN_DOCUMENT, - _on_exit=AbortAction.RAISE_EXCEPTION): + _input_buffer_height=None): + self.loop = loop self.get_globals = get_globals or (lambda: {}) self.get_locals = get_locals or self.get_globals + self.output = output or create_output(true_color=Condition(lambda: self.true_color)) + self.input = input or create_input(sys.stdin) + self._completer = _completer or PythonCompleter(self.get_globals, self.get_locals) self._validator = _validator or PythonValidator(self.get_compiler_flags) self.history = FileHistory(history_filename) if history_filename else InMemoryHistory() self._lexer = _lexer or PygmentsLexer(PythonLexer) - self._extra_buffers = _extra_buffers - self._accept_action = _accept_action - self._on_exit = _on_exit - self._on_start = _on_start self._input_buffer_height = _input_buffer_height self._extra_layout_body = _extra_layout_body or [] self._extra_toolbars = _extra_toolbars or [] self._extra_buffer_processors = _extra_buffer_processors or [] + self.extra_key_bindings = extra_key_bindings or KeyBindings() + # Settings. self.show_signature = False self.show_docstring = False @@ -168,7 +165,6 @@ def __init__(self, self.show_status_bar = True self.wrap_lines = True self.complete_while_typing = True - self.vi_mode = vi_mode self.paste_mode = False # When True, don't insert whitespace after newline. self.confirm_exit = True # Ask for confirmation when Control-D is pressed. self.accept_input_on_enter = 2 # Accept when pressing Enter 'n' times. @@ -191,6 +187,11 @@ def __init__(self, self.exit_message = 'Do you really want to exit?' self.insert_blank_line_after_output = True # (For the REPL.) + # The buffers. + self.default_buffer = self._create_buffer() + self.search_buffer = Buffer(loop=loop) + self.docstring_buffer = Buffer(loop=loop, read_only=True) + # Tokens to be shown at the prompt. self.prompt_style = 'classic' # The currently active style. @@ -199,11 +200,11 @@ def __init__(self, 'classic': ClassicPrompt(), } - self.get_input_prompt_tokens = lambda cli: \ - self.all_prompt_styles[self.prompt_style].in_tokens(cli) + self.get_input_prompt_tokens = lambda app: \ + self.all_prompt_styles[self.prompt_style].in_tokens(app) - self.get_output_prompt_tokens = lambda cli: \ - self.all_prompt_styles[self.prompt_style].out_tokens(cli) + self.get_output_prompt_tokens = lambda app: \ + self.all_prompt_styles[self.prompt_style].out_tokens(app) #: Load styles. self.code_styles = get_all_code_styles() @@ -215,7 +216,7 @@ def __init__(self, self._current_code_style_name = 'win32' self._current_style = self._generate_style() - self.true_color = False + self.true_color = true_color # Options to be configurable from the sidebar. self.options = self._create_options() @@ -227,30 +228,19 @@ def __init__(self, # Code signatures. (This is set asynchronously after a timeout.) self.signatures = [] - # Create a Registry for the key bindings. - self.key_bindings_registry = MergedRegistry([ - ConditionalRegistry( - registry=load_key_bindings_for_prompt( - enable_abort_and_exit_bindings=True, - enable_search=True, - enable_open_in_editor=Condition(lambda cli: self.enable_open_in_editor), - enable_system_bindings=Condition(lambda cli: self.enable_system_bindings), - enable_auto_suggest_bindings=Condition(lambda cli: self.enable_auto_suggest)), - - # Disable all default key bindings when the sidebar or the exit confirmation - # are shown. - filter=Condition(lambda cli: not (self.show_sidebar or self.show_exit_confirmation)) - ), - load_mouse_bindings(), - load_python_bindings(self), - load_sidebar_bindings(self), - load_confirm_exit_bindings(self), - ]) - # Boolean indicating whether we have a signatures thread running. # (Never run more than one at the same time.) self._get_signatures_thread_running = False + self.app = self._create_application() + + if vi_mode: + self.app.editing_mode = EditingMode.VI + + def _accept_handler(self, app, buff): + app.set_return_value(buff.text) + app.pre_run_callables.append(buff.reset) + @property def option_count(self): " Return the total amount of options. (In all categories together.) " @@ -293,15 +283,8 @@ def add_key_binding(self): def handler(event): ... """ - # Extra key bindings should not be active when the sidebar is visible. - sidebar_visible = Condition(lambda cli: self.show_sidebar) - - def add_binding_decorator(*keys, **kw): - # Pop default filter keyword argument. - filter = kw.pop('filter', Always()) - assert not kw - - return self.key_bindings_registry.add_binding(*keys, filter=filter & ~sidebar_visible) + def add_binding_decorator(*k, **kw): + return self.extra_key_bindings.add(*k, **kw) return add_binding_decorator def install_code_colorscheme(self, name, style_dict): @@ -382,10 +365,10 @@ def get_values(): return [ OptionCategory('Input', [ - simple_option(title='Input mode', + simple_option(title='Editing mode', description='Vi or emacs key bindings.', field_name='vi_mode', - values=['emacs', 'vi']), + values=[EditingMode.EMACS, EditingMode.VI]), simple_option(title='Paste mode', description="When enabled, don't indent automatically.", field_name='paste_mode'), @@ -493,16 +476,14 @@ def get_values(): ]), ] - def create_application(self): + def _create_application(self): """ - Create an `Application` instance for use in a `CommandLineInterface`. + Create an `Application` instance. """ - buffers = { - 'docstring': Buffer(read_only=True), - } - buffers.update(self._extra_buffers or {}) - return Application( + loop=self.loop, + input=self.input, + output=self.output, layout=create_layout( self, lexer=self._lexer, @@ -510,31 +491,40 @@ def create_application(self): extra_buffer_processors=self._extra_buffer_processors, extra_body=self._extra_layout_body, extra_toolbars=self._extra_toolbars), - buffer=self._create_buffer(), - buffers=buffers, - key_bindings_registry=self.key_bindings_registry, - paste_mode=Condition(lambda cli: self.paste_mode), - mouse_support=Condition(lambda cli: self.enable_mouse_support), - on_abort=AbortAction.RETRY, - on_exit=self._on_exit, + key_bindings=merge_key_bindings([ + ConditionalKeyBindings( + key_bindings=load_key_bindings( + enable_abort_and_exit_bindings=True, + enable_search=True, + enable_open_in_editor=Condition(lambda app: self.enable_open_in_editor), + enable_system_bindings=Condition(lambda app: self.enable_system_bindings), + enable_auto_suggest_bindings=Condition(lambda app: self.enable_auto_suggest)), + + # Disable all default key bindings when the sidebar or the exit confirmation + # are shown. + filter=Condition(lambda app: not (self.show_sidebar or self.show_exit_confirmation)) + ), + load_python_bindings(self), + load_sidebar_bindings(self), + load_confirm_exit_bindings(self), + # Extra key bindings should not be active when the sidebar is visible. + ConditionalKeyBindings( + self.extra_key_bindings, + Condition(lambda app: not self.show_sidebar)) + ]), + paste_mode=Condition(lambda app: self.paste_mode), + mouse_support=Condition(lambda app: self.enable_mouse_support), style=DynamicStyle(lambda: self._current_style), get_title=lambda: self.terminal_title, reverse_vi_search_direction=True, - on_initialize=self._on_cli_initialize, - on_start=self._on_start, on_input_timeout=self._on_input_timeout) def _create_buffer(self): """ Create the `Buffer` for the Python input. """ - def is_buffer_multiline(): - return (self.paste_mode or - self.accept_input_on_enter is None or - document_is_multiline_python(python_buffer.document)) - python_buffer = Buffer( - is_multiline=Condition(is_buffer_multiline), + loop=self.loop, name=DEFAULT_BUFFER, complete_while_typing=Condition(lambda: self.complete_while_typing), enable_history_search=Condition(lambda: self.enable_history_search), tempfile_suffix='.py', @@ -545,32 +535,36 @@ def is_buffer_multiline(): Condition(lambda: self.enable_input_validation)), auto_suggest=ConditionalAutoSuggest( AutoSuggestFromHistory(), - Condition(lambda cli: self.enable_auto_suggest)), - accept_action=self._accept_action) + Condition(lambda app: self.enable_auto_suggest)), + accept_handler=self._accept_handler) return python_buffer - def _on_cli_initialize(self, cli): - """ - Called when a CommandLineInterface has been created. - """ - # Synchronize PythonInput state with the CommandLineInterface. - def synchronize(_=None): - if self.vi_mode: - cli.editing_mode = EditingMode.VI - else: - cli.editing_mode = EditingMode.EMACS + @property + def editing_mode(self): + return self.app.editing_mode + + @editing_mode.setter + def editing_mode(self, value): + self.app.editing_mode = value - cli.input_processor.beforeKeyPress += synchronize - cli.input_processor.afterKeyPress += synchronize - synchronize() + @property + def vi_mode(self): + return self.editing_mode == EditingMode.VI + + @vi_mode.setter + def vi_mode(self, value): + if value: + self.editing_mode = EditingMode.VI + else: + self.editing_mode = EditingMode.EMACS - def _on_input_timeout(self, cli): + def _on_input_timeout(self, app): """ When there is no input activity, in another thread, get the signature of the current code. """ - if cli.current_buffer_name != DEFAULT_BUFFER: + if app.current_buffer_name != DEFAULT_BUFFER: return # Never run multiple get-signature threads. @@ -578,7 +572,7 @@ def _on_input_timeout(self, cli): return self._get_signatures_thread_running = True - buffer = cli.current_buffer + buffer = app.current_buffer document = buffer.document def run(): @@ -623,49 +617,34 @@ def run(): string = signatures[0].docstring() if not isinstance(string, six.text_type): string = string.decode('utf-8') - cli.buffers['docstring'].reset( + app.buffers['docstring'].reset( initial_document=Document(string, cursor_position=0)) else: - cli.buffers['docstring'].reset() + app.buffers['docstring'].reset() - cli.request_redraw() + app.request_redraw() else: - self._on_input_timeout(cli) + self._on_input_timeout(app) - cli.eventloop.run_in_executor(run) + app.eventloop.run_in_executor(run) - def on_reset(self, cli): + def on_reset(self, app): self.signatures = [] - def enter_history(self, cli): + def enter_history(self, app): """ Display the history. """ - cli.vi_state.input_mode = InputMode.NAVIGATION + app.vi_state.input_mode = InputMode.NAVIGATION - def done(result): + def done(f): + result = f.result() if result is not None: - cli.buffers[DEFAULT_BUFFER].document = result - - cli.vi_state.input_mode = InputMode.INSERT - - cli.run_sub_application(create_history_application( - self, cli.buffers[DEFAULT_BUFFER].document), done) - - -class PythonCommandLineInterface(CommandLineInterface): - def __init__(self, eventloop=None, python_input=None, input=None, output=None): - assert python_input is None or isinstance(python_input, PythonInput) + self.default_buffer.document = result - python_input = python_input or PythonInput() + app.vi_state.input_mode = InputMode.INSERT - # Make sure that the prompt_toolkit 'renderer' knows about the - # 'true_color' property of PythonInput. - if output is None: - output=create_output(true_color=Condition(lambda: python_input.true_color)) + history = History(self, self.default_buffer.document) - super(PythonCommandLineInterface, self).__init__( - application=python_input.create_application(), - eventloop=eventloop, - input=input, - output=output) + future = app.run_sub_application(history.app) + future.add_done_callback(done) diff --git a/ptpython/repl.py b/ptpython/repl.py index 75ef94f5..5214eafa 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -12,16 +12,15 @@ from pygments.lexers import PythonTracebackLexer, PythonLexer from pygments.styles.default import DefaultStyle -from prompt_toolkit.application import AbortAction +from prompt_toolkit.document import Document from prompt_toolkit.enums import DEFAULT_BUFFER -from prompt_toolkit.interface import AcceptAction +from prompt_toolkit.eventloop.defaults import create_asyncio_event_loop from prompt_toolkit.layout.utils import token_list_width -from prompt_toolkit.shortcuts import create_asyncio_eventloop from prompt_toolkit.styles import style_from_pygments from prompt_toolkit.utils import DummyContext -from .python_input import PythonInput, PythonCommandLineInterface -from .eventloop import create_eventloop +from .python_input import PythonInput +from .eventloop import create_event_loop import os import six @@ -40,20 +39,11 @@ class PythonRepl(PythonInput): def __init__(self, *a, **kw): self._startup_paths = kw.pop('startup_paths', None) - - kw.update({ - '_accept_action': AcceptAction.run_in_terminal( - handler=self._process_document, render_cli_done=True), - '_on_start': self._on_start, - '_on_exit': AbortAction.RETURN_NONE, - }) - super(PythonRepl, self).__init__(*a, **kw) + self._load_start_paths() - def _on_start(self, cli): - """ - Start the Read-Eval-Print Loop. - """ + def _load_start_paths(self): + " Start the Read-Eval-Print Loop. " if self._startup_paths: for path in self._startup_paths: if os.path.exists(path): @@ -61,36 +51,45 @@ def _on_start(self, cli): code = compile(f.read(), path, 'exec') six.exec_(code, self.get_globals(), self.get_locals()) else: - output = cli.output + output = self.app.output output.write('WARNING | File not found: {}\n\n'.format(path)) - def _process_document(self, cli, buffer): - line = buffer.text + def run(self): + while True: + # Run the UI. + try: + text = self.app.run() + except EOFError: + return + except KeyboardInterrupt: + # Abort - try again. + self.default_buffer.document = Document() + else: + self._process_text(text) + + def _process_text(self, text): + line = self.default_buffer.text if line and not line.isspace(): try: # Eval and print. - self._execute(cli, line) + self._execute(line) except KeyboardInterrupt as e: # KeyboardInterrupt doesn't inherit from Exception. - self._handle_keyboard_interrupt(cli, e) + self._handle_keyboard_interrupt(e) except Exception as e: - self._handle_exception(cli, e) + self._handle_exception(e) if self.insert_blank_line_after_output: - cli.output.write('\n') + self.app.output.write('\n') self.current_statement_index += 1 self.signatures = [] - # Append to history and reset. - cli.search_state.text = '' - cli.buffers[DEFAULT_BUFFER].reset(append_to_history=True) - - def _execute(self, cli, line): + def _execute(self, line): """ Evaluate the line and print the result. """ - output = cli.output + output = self.app.output def compile_with_flags(code, mode): " Compile code with the right compiler flags. " @@ -100,7 +99,7 @@ def compile_with_flags(code, mode): if line.lstrip().startswith('\x1a'): # When the input starts with Ctrl-Z, quit the REPL. - cli.exit() + self.app.exit() elif line.lstrip().startswith('!'): # Run as shell command @@ -115,7 +114,7 @@ def compile_with_flags(code, mode): locals['_'] = locals['_%i' % self.current_statement_index] = result if result is not None: - out_tokens = self.get_output_prompt_tokens(cli) + out_tokens = self.get_output_prompt_tokens(self.app) try: result_str = '%r\n' % (result, ) @@ -132,7 +131,7 @@ def compile_with_flags(code, mode): # Write output tokens. out_tokens.extend(_lex_python_result(result_str)) - cli.print_tokens(out_tokens) + self.app.print_tokens(out_tokens) # If not a valid `eval` expression, run using `exec` instead. except SyntaxError: code = compile_with_flags(line, 'exec') @@ -140,9 +139,8 @@ def compile_with_flags(code, mode): output.flush() - @classmethod - def _handle_exception(cls, cli, e): - output = cli.output + def _handle_exception(self, e): + output = self.app.output # Instead of just calling ``traceback.format_exc``, we take the # traceback and skip the bottom calls of this framework. @@ -170,14 +168,13 @@ def _handle_exception(cls, cli, e): # (We use the default style. Most other styles result # in unreadable colors for the traceback.) tokens = _lex_python_traceback(tb) - cli.print_tokens(tokens, style=style_from_pygments(DefaultStyle)) + self.app.print_tokens(tokens, style=style_from_pygments(DefaultStyle)) output.write('%s\n' % e) output.flush() - @classmethod - def _handle_keyboard_interrupt(cls, cli, e): - output = cli.output + def _handle_keyboard_interrupt(self, e): + output = self.app.output output.write('\rKeyboardInterrupt\n\n') output.flush() @@ -283,12 +280,12 @@ def get_locals(): # Create eventloop. if return_asyncio_coroutine: - eventloop = create_asyncio_eventloop() + loop = create_asyncio_event_loop() else: - eventloop = create_eventloop() + loop = create_event_loop() # Create REPL. - repl = PythonRepl(get_globals, get_locals, vi_mode=vi_mode, + repl = PythonRepl(loop=loop, get_globals=get_globals, get_locals=get_locals, vi_mode=vi_mode, history_filename=history_filename, startup_paths=startup_paths) @@ -298,17 +295,17 @@ def get_locals(): if configure: configure(repl) - cli = PythonCommandLineInterface(python_input=repl, eventloop=eventloop) + app = repl.app # Start repl. - patch_context = cli.patch_stdout_context() if patch_stdout else DummyContext() + patch_context = app.patch_stdout_context() if patch_stdout else DummyContext() - if return_asyncio_coroutine: + if return_asyncio_coroutine: # XXX def coroutine(): with patch_context: - for future in cli.run_async(): + for future in app.run_async(): yield future return coroutine() else: with patch_context: - cli.run() + repl.run() diff --git a/ptpython/style.py b/ptpython/style.py index f80754df..2a614b10 100644 --- a/ptpython/style.py +++ b/ptpython/style.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals -from pygments.token import Token, Keyword, Name, Comment, String, Operator, Number +from prompt_toolkit.token import Token from pygments.styles import get_style_by_name, get_all_styles from prompt_toolkit.styles import DEFAULT_STYLE_EXTENSIONS, style_from_dict from prompt_toolkit.utils import is_windows, is_conemu_ansi @@ -50,26 +50,26 @@ def generate_style(python_style, ui_style): # Code style for Windows consoles. They support only 16 colors, # so we choose a combination that displays nicely. win32_code_style = { - Comment: "#00ff00", - Keyword: '#44ff44', - Number: '', - Operator: '', - String: '#ff44ff', - - Name: '', - Name.Decorator: '#ff4444', - Name.Class: '#ff4444', - Name.Function: '#ff4444', - Name.Builtin: '#ff4444', - - Name.Attribute: '', - Name.Constant: '', - Name.Entity: '', - Name.Exception: '', - Name.Label: '', - Name.Namespace: '', - Name.Tag: '', - Name.Variable: '', + Token.Comment: "#00ff00", + Token.Keyword: '#44ff44', + Token.Number: '', + Token.Operator: '', + Token.String: '#ff44ff', + + Token.Name: '', + Token.Name.Decorator: '#ff4444', + Token.Name.Class: '#ff4444', + Token.Name.Function: '#ff4444', + Token.Name.Builtin: '#ff4444', + + Token.Name.Attribute: '', + Token.Name.Constant: '', + Token.Name.Entity: '', + Token.Name.Exception: '', + Token.Name.Label: '', + Token.Name.Namespace: '', + Token.Name.Tag: '', + Token.Name.Variable: '', } @@ -89,10 +89,6 @@ def generate_style(python_style, ui_style): # Separator between windows. (Used above docstring.) Token.Separator: '#bbbbbb', - # Search toolbar. - Token.Toolbar.Search: '#22aaaa noinherit', - Token.Toolbar.Search.Text: 'noinherit', - # System toolbar Token.Toolbar.System: '#22aaaa noinherit', @@ -119,7 +115,7 @@ def generate_style(python_style, ui_style): Token.Toolbar.Status.PythonVersion: 'bg:#222222 #ffffff bold', # When Control-C has been pressed. Grayed. - Token.Aborted: '#888888', + Token.Aborted: '#888888 bg:default important', # The options sidebar. Token.Sidebar: 'bg:#bbbbbb #000000', @@ -142,7 +138,7 @@ def generate_style(python_style, ui_style): Token.History.ExistingInput: '#888888', # Help Window. - Token.Window.Border: '#bbbbbb', + Token.Window.Border: '#aaaaaa', Token.Window.Title: 'bg:#bbbbbb #000000', # Meta-enter message. diff --git a/ptpython/utils.py b/ptpython/utils.py index f6f914d0..d53b7422 100644 --- a/ptpython/utils.py +++ b/ptpython/utils.py @@ -3,7 +3,7 @@ """ from __future__ import unicode_literals -from prompt_toolkit.mouse_events import MouseEventTypes +from prompt_toolkit.mouse_events import MouseEventType import re __all__ = ( @@ -119,9 +119,9 @@ def if_mousedown(handler): (When applied to a token list. Scroll events will bubble up and are handled by the Window.) """ - def handle_if_mouse_down(cli, mouse_event): - if mouse_event.event_type == MouseEventTypes.MOUSE_DOWN: - return handler(cli, mouse_event) + def handle_if_mouse_down(app, mouse_event): + if mouse_event.event_type == MouseEventType.MOUSE_DOWN: + return handler(app, mouse_event) else: return NotImplemented return handle_if_mouse_down diff --git a/ptpython/validator.py b/ptpython/validator.py index 103697b5..80cc3fb1 100644 --- a/ptpython/validator.py +++ b/ptpython/validator.py @@ -6,7 +6,6 @@ 'PythonValidator', ) - class PythonValidator(Validator): """ Validation of Python input. From 7945bb40c696e43e77be36bdef87f81dbd01f5ba Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 28 Mar 2017 23:56:56 +0200 Subject: [PATCH 183/470] Catch AssertionError in Jedi completion. --- ptpython/completer.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ptpython/completer.py b/ptpython/completer.py index b1400371..90da6451 100644 --- a/ptpython/completer.py +++ b/ptpython/completer.py @@ -147,6 +147,10 @@ def get_completions(self, document, complete_event): # Jedi issue: "IOError: No such file or directory." # https://github.com/jonathanslenders/ptpython/issues/71 pass + except AssertionError: + # In jedi.parser.__init__.py: 227, in remove_last_newline, + # the assertion "newline.value.endswith('\n')" can fail. + pass else: for c in completions: yield Completion(c.name_with_symbols, len(c.complete) - len(c.name_with_symbols), From 063fddee6832246aff05c3cc8591031bca00b1b7 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sun, 2 Apr 2017 13:37:32 +0200 Subject: [PATCH 184/470] Prompt_toolkit 2.0 changes. --- ptpython/history_browser.py | 99 ++++++------- ptpython/ipython.py | 21 ++- ptpython/layout.py | 191 ++++++++++++------------ ptpython/prompt_style.py | 21 ++- ptpython/python_input.py | 29 ++-- ptpython/repl.py | 14 +- ptpython/style.py | 284 ++++++++++++++++++------------------ 7 files changed, 322 insertions(+), 337 deletions(-) diff --git a/ptpython/history_browser.py b/ptpython/history_browser.py index b9be7488..bc24ee36 100644 --- a/ptpython/history_browser.py +++ b/ptpython/history_browser.py @@ -15,20 +15,19 @@ from prompt_toolkit.key_binding.defaults import load_key_bindings from prompt_toolkit.keys import Keys from prompt_toolkit.layout.containers import HSplit, VSplit, Window, FloatContainer, Float, ConditionalContainer, Container, ScrollOffsets, Align -from prompt_toolkit.layout.controls import BufferControl, TokenListControl +from prompt_toolkit.layout.controls import BufferControl, FormattedTextControl from prompt_toolkit.layout.dimension import Dimension as D from prompt_toolkit.layout.layout import Layout from prompt_toolkit.layout.lexers import PygmentsLexer from prompt_toolkit.layout.margins import Margin, ScrollbarMargin from prompt_toolkit.layout.processors import Processor, Transformation, HighlightSearchProcessor, HighlightSelectionProcessor, merge_processors from prompt_toolkit.layout.toolbars import ArgToolbar, SearchToolbar -from prompt_toolkit.layout.utils import token_list_to_text -from prompt_toolkit.token import Token +from prompt_toolkit.layout.utils import fragment_list_to_text from pygments.lexers import RstLexer from .utils import if_mousedown -from ptpython.layout import get_inputmode_tokens +from ptpython.layout import get_inputmode_fragments from functools import partial import six @@ -41,7 +40,7 @@ HISTORY_COUNT = 2000 __all__ = ( - 'create_history_application', + 'HistoryLayout', ) HELP_TEXT = """ @@ -95,7 +94,7 @@ class BORDER: LIGHT_VERTICAL = '\u2502' -def create_popup_window(title, body): +def _create_popup_window(title, body): """ Return the layout for a pop-up window. It consists of a title bar showing the `title` text, and a body layout. The window is surrounded by borders. @@ -107,36 +106,36 @@ def create_popup_window(title, body): VSplit([ Window(width=D.exact(1), height=D.exact(1), char=BORDER.TOP_LEFT, - token=Token.Window.Border), + style='class:window.border'), Window( - content=TokenListControl( - get_tokens=lambda app: [(Token.Window.Title, ' %s ' % title)]), + content=FormattedTextControl( + lambda app: [('class:window.title', ' %s ' % title)]), align=Align.CENTER, char=BORDER.HORIZONTAL, - token=Token.Window.Border), + style='class:window.border'), Window(width=D.exact(1), height=D.exact(1), char=BORDER.TOP_RIGHT, - token=Token.Window.Border), + style='class:window.border'), ]), VSplit([ Window(width=D.exact(1), char=BORDER.VERTICAL, - token=Token.Window.Border), + style='class:window.border'), body, Window(width=D.exact(1), char=BORDER.VERTICAL, - token=Token.Window.Border), + style='class:window.border'), ]), VSplit([ Window(width=D.exact(1), height=D.exact(1), char=BORDER.BOTTOM_LEFT, - token=Token.Window.Border), + style='class:window.border'), Window(height=D.exact(1), char=BORDER.HORIZONTAL, - token=Token.Window.Border), + style='class:window.border'), Window(width=D.exact(1), height=D.exact(1), char=BORDER.BOTTOM_RIGHT, - token=Token.Window.Border), + style='class:window.border'), ]), ]) @@ -157,7 +156,7 @@ def __init__(self, history): lexer=PygmentsLexer(RstLexer), input_processor=merge_processors(default_processors)) - help_window = create_popup_window( + help_window = _create_popup_window( title='History Help', body=Window( content=self.help_buffer_control, @@ -185,9 +184,9 @@ def __init__(self, history): self.root_container = HSplit([ # Top title bar. Window( - content=TokenListControl(get_tokens=_get_top_toolbar_tokens), + content=FormattedTextControl(_get_top_toolbar_fragments), align=Align.CENTER, - token=Token.Toolbar.Status), + style='class:status-toolbar'), FloatContainer( content=VSplit([ # Left side: history. @@ -195,7 +194,7 @@ def __init__(self, history): # Separator. Window(width=D.exact(1), char=BORDER.LIGHT_VERTICAL, - token=Token.Separator), + style='class:separator'), # Right side: result. Window( content=self.default_buffer_control, @@ -218,19 +217,19 @@ def __init__(self, history): ArgToolbar(), # SearchToolbar(), # XXX Window( - content=TokenListControl( - get_tokens=partial(_get_bottom_toolbar_tokens, history=history)), - token=Token.Toolbar.Status), + content=FormattedTextControl( + partial(_get_bottom_toolbar_fragments, history=history)), + style='class:status-toolbar'), ]) self.layout = Layout(self.root_container, history_window) -def _get_top_toolbar_tokens(app): - return [(Token.Toolbar.Status.Title, 'History browser - Insert from history')] +def _get_top_toolbar_fragments(app): + return [('class:status-bar.title', 'History browser - Insert from history')] -def _get_bottom_toolbar_tokens(app, history): +def _get_bottom_toolbar_fragments(app, history): python_input = history.python_input @if_mousedown def f1(app, mouse_event): @@ -241,17 +240,16 @@ def tab(app, mouse_event): _select_other_window(history) return [ - (Token.Toolbar.Status, ' ') - ] + get_inputmode_tokens(app, python_input) + [ - (Token.Toolbar.Status, ' '), - (Token.Toolbar.Status.Key, '[Space]'), - (Token.Toolbar.Status, ' Toggle '), - (Token.Toolbar.Status.Key, '[Tab]', tab), - (Token.Toolbar.Status, ' Focus ', tab), - (Token.Toolbar.Status.Key, '[Enter]'), - (Token.Toolbar.Status, ' Accept '), - (Token.Toolbar.Status.Key, '[F1]', f1), - (Token.Toolbar.Status, ' Help ', f1), + ('class:status-toolbar', ' ') ] + get_inputmode_fragments(app, python_input) + [ + ('class:status-toolbar', ' '), + ('class:status-toolbar.key', '[Space]'), + ('class:status-toolbar', ' Toggle '), + ('class:status-toolbar.key', '[Tab]', tab), + ('class:status-toolbar', ' Focus ', tab), + ('class:status-toolbar.key', '[Enter]'), + ('class:status-toolbar', ' Accept '), + ('class:status-toolbar.key', '[F1]', f1), + ('class:status-toolbar', ' Help ', f1), ] @@ -289,15 +287,15 @@ def create_margin(self, app, window_render_info, width, height): char = ' ' if line_number in selected_lines: - t = Token.History.Line.Selected + t = 'class:history-line,selected' else: - t = Token.History.Line + t = 'class:history-line' if line_number == current_lineno: - t = t.Current + t = t + ',current' result.append((t, char)) - result.append((Token, '\n')) + result.append(('', '\n')) return result @@ -328,14 +326,14 @@ def create_margin(self, app, window_render_info, width, height): if (line_number is None or line_number < offset or line_number >= offset + len(self.history_mapping.selected_lines)): - t = Token + t = '' elif line_number == current_lineno: - t = Token.History.Line.Selected.Current + t = 'class:history-line,selected,current' else: - t = Token.History.Line.Selected + t = 'class:history-line,selected' result.append((t, ' ')) - result.append((Token, '\n')) + result.append(('', '\n')) return result @@ -352,16 +350,15 @@ def __init__(self, history_mapping): self._lines_before = len(history_mapping.original_document.text_before_cursor.splitlines()) def apply_transformation(self, transformation_input): - app = transformation_input.app lineno = transformation_input.lineno - tokens = transformation_input.tokens + fragments = transformation_input.fragments if (lineno < self._lines_before or lineno >= self._lines_before + len(self.history_mapping.selected_lines)): - text = token_list_to_text(tokens) - return Transformation(tokens=[(Token.History.ExistingInput, text)]) + text = fragment_list_to_text(fragments) + return Transformation(fragments=[('class:history.existing-input', text)]) else: - return Transformation(tokens=tokens) + return Transformation(fragments=fragments) class HistoryMapping(object): @@ -601,7 +598,7 @@ def __init__(self, python_input, original_document): self.app = Application( loop=python_input.loop, layout=self.history_layout.layout, - use_alternate_screen=True, + full_screen=True, style=python_input._current_style, mouse_support=Condition(lambda app: python_input.enable_mouse_support), key_bindings=create_key_bindings(self, python_input, history_mapping) diff --git a/ptpython/ipython.py b/ptpython/ipython.py index 957ad95a..086e2848 100644 --- a/ptpython/ipython.py +++ b/ptpython/ipython.py @@ -18,7 +18,6 @@ from prompt_toolkit.document import Document from prompt_toolkit.interface import CommandLineInterface from prompt_toolkit.layout.lexers import PygmentsLexer, SimpleLexer -from prompt_toolkit.token import Token from .python_input import PythonInput, PythonValidator, PythonCompleter from .eventloop import create_eventloop @@ -47,17 +46,17 @@ def __init__(self, prompt_manager): def in_tokens(self, cli): text = self.prompt_manager.render('in', color=False, just=False) - return [(Token.In, text)] + return [('class:in', text)] def in2_tokens(self, cli, width): text = self.prompt_manager.render('in2', color=False, just=False) - return [(Token.In, text.rjust(width))] + return [('class:in', text.rjust(width))] def out_tokens(self, cli): # This function is currently not used by IPython. But for completeness, # it would look like this. text = self.prompt_manager.render('out', color=False, just=False) - return [(Token.Out, text)] + return [('class:out', text)] class IPython5Prompt(PromptStyle): @@ -135,9 +134,9 @@ def create_lexer(): return GrammarLexer( g, lexers={ - 'percent': SimpleLexer(Token.Operator), - 'magic': SimpleLexer(Token.Keyword), - 'filename': SimpleLexer(Token.Name), + 'percent': SimpleLexer('class:pygments.operator'), + 'magic': SimpleLexer('class:pygments.keyword'), + 'filename': SimpleLexer('class:pygments.name'), 'python': PygmentsLexer(PythonLexer), 'system': PygmentsLexer(BashLexer), }) @@ -200,10 +199,10 @@ def __init__(self, ipython_shell, *a, **kw): style_dict = {} style_dict.update(default_ui_style) style_dict.update({ - Token.Prompt: '#009900', - Token.PromptNum: '#00ff00 bold', - Token.OutPrompt: '#990000', - Token.OutPromptNum: '#ff0000 bold', + 'prompt': '#009900', + 'prompt-num': '#00ff00 bold', + 'out-prompt': '#990000', + 'out-prompt-num': '#ff0000 bold', }) self.ui_styles = { diff --git a/ptpython/layout.py b/ptpython/layout.py index 00d4c1a4..b3c0366e 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -7,7 +7,7 @@ from prompt_toolkit.filters import IsDone, HasCompletions, RendererHeightIsKnown, HasFocus, Condition from prompt_toolkit.key_binding.vi_state import InputMode from prompt_toolkit.layout.containers import Window, HSplit, VSplit, FloatContainer, Float, ConditionalContainer, ScrollOffsets -from prompt_toolkit.layout.controls import BufferControl, TokenListControl +from prompt_toolkit.layout.controls import BufferControl, FormattedTextControl from prompt_toolkit.layout.dimension import Dimension from prompt_toolkit.layout.layout import Layout from prompt_toolkit.layout.lexers import SimpleLexer @@ -16,10 +16,9 @@ from prompt_toolkit.layout.processors import ConditionalProcessor, AppendAutoSuggestion, HighlightSearchProcessor, HighlightSelectionProcessor, HighlightMatchingBracketProcessor, Processor, Transformation from prompt_toolkit.layout.processors import merge_processors from prompt_toolkit.layout.toolbars import CompletionsToolbar, ArgToolbar, SearchToolbar, ValidationToolbar, SystemToolbar -from prompt_toolkit.layout.utils import token_list_width +from prompt_toolkit.layout.utils import fragment_list_width from prompt_toolkit.reactive import Integer from prompt_toolkit.selection import SelectionType -from prompt_toolkit.token import Token from .filters import HasSignature, ShowSidebar, ShowSignature, ShowDocstring from .utils import if_mousedown @@ -73,15 +72,14 @@ def python_sidebar(python_input): """ Create the `Layout` for the sidebar with the configurable options. """ - def get_tokens(app): + def get_text_fragments(app): tokens = [] - T = Token.Sidebar def append_category(category): tokens.extend([ - (T, ' '), - (T.Title, ' %-36s' % category.title), - (T, '\n'), + ('class:sidebar', ' '), + ('class:sidebar.title', ' %-36s' % category.title), + ('class:sidebar', '\n'), ]) def append(index, label, status): @@ -98,19 +96,19 @@ def goto_next(app, mouse_event): option = python_input.selected_option option.activate_next() - token = T.Selected if selected else T + sel = ',selected' if selected else '' - tokens.append((T, ' >' if selected else ' ')) - tokens.append((token.Label, '%-24s' % label, select_item)) - tokens.append((token.Status, ' ', select_item)) - tokens.append((token.Status, '%s' % status, goto_next)) + tokens.append(('class:sidebar' + sel, ' >' if selected else ' ')) + tokens.append(('class:sidebar.label' + sel, '%-24s' % label, select_item)) + tokens.append(('class:sidebar.status' + sel, ' ', select_item)) + tokens.append(('class:sidebar.status' + sel, '%s' % status, goto_next)) if selected: - tokens.append((Token.SetCursorPosition, '')) + tokens.append(('[SetCursorPosition]', '')) - tokens.append((token.Status, ' ' * (13 - len(status)), goto_next)) - tokens.append((T, '<' if selected else '')) - tokens.append((T, '\n')) + tokens.append(('class:sidebar.status' + sel, ' ' * (13 - len(status)), goto_next)) + tokens.append(('class:sidebar', '<' if selected else '')) + tokens.append(('class:sidebar', '\n')) i = 0 for category in python_input.options: @@ -124,61 +122,54 @@ def goto_next(app, mouse_event): return tokens - class Control(TokenListControl): + class Control(FormattedTextControl): def move_cursor_down(self, app): python_input.selected_option_index += 1 def move_cursor_up(self, app): python_input.selected_option_index -= 1 - return ConditionalContainer( - content=Window( - Control(get_tokens), - # has_focus=ShowSidebar(python_input) & ~IsDone()), - token=Token.Sidebar, - width=Dimension.exact(43), - height=Dimension(min=3), - scroll_offsets=ScrollOffsets(top=1, bottom=1)), - filter=ShowSidebar(python_input) & ~IsDone()) + return Window( + Control(get_text_fragments), + style='class:sidebar', + width=Dimension.exact(43), + height=Dimension(min=3), + scroll_offsets=ScrollOffsets(top=1, bottom=1)) def python_sidebar_navigation(python_input): """ Create the `Layout` showing the navigation information for the sidebar. """ - def get_tokens(app): + def get_text_fragments(app): tokens = [] - T = Token.Sidebar # Show navigation info. tokens.extend([ - (T.Separator, ' ' * 43 + '\n'), - (T, ' '), - (T.Key, '[Arrows]'), - (T, ' '), - (T.Key.Description, 'Navigate'), - (T, ' '), - (T.Key, '[Enter]'), - (T, ' '), - (T.Key.Description, 'Hide menu'), + ('class:sidebar', ' '), + ('class:sidebar.key', '[Arrows]'), + ('class:sidebar', ' '), + ('class:sidebar.description', 'Navigate'), + ('class:sidebar', ' '), + ('class:sidebar.key', '[Enter]'), + ('class:sidebar', ' '), + ('class:sidebar.description', 'Hide menu'), ]) return tokens - return ConditionalContainer( - content=Window( - TokenListControl(get_tokens), - token=Token.Sidebar, - width=Dimension.exact(43), - height=Dimension.exact(2)), - filter=ShowSidebar(python_input) & ~IsDone()) + return Window( + FormattedTextControl(get_text_fragments), + style='class:sidebar', + width=Dimension.exact(43), + height=Dimension.exact(1)) def python_sidebar_help(python_input): """ Create the `Layout` for the help text for the current item in the sidebar. """ - token = Token.Sidebar.HelpText + token = 'class:sidebar.helptext' def get_current_description(): """ @@ -192,13 +183,13 @@ def get_current_description(): i += 1 return '' - def get_help_tokens(app): + def get_help_text(app): return [(token, get_current_description())] return ConditionalContainer( content=Window( - TokenListControl(get_help_tokens), - token=token, + FormattedTextControl(get_help_text), + style=token, height=Dimension(min=3)), filter=ShowSidebar(python_input) & Condition(lambda app: python_input.show_sidebar_help) & ~IsDone()) @@ -208,10 +199,10 @@ def signature_toolbar(python_input): """ Return the `Layout` for the signature. """ - def get_tokens(app): + def get_text_fragments(app): result = [] append = result.append - Signature = Token.Toolbar.Signature + Signature = 'class:signature-toolbar' if python_input.signatures: sig = python_input.signatures[0] # Always take the first one. @@ -224,7 +215,7 @@ def get_tokens(app): # See also: https://github.com/davidhalter/jedi/issues/490 return [] - append((Signature.Operator, '(')) + append((Signature + ',operator', '(')) try: enumerated_params = enumerate(sig.params) @@ -244,22 +235,22 @@ def get_tokens(app): if i == sig_index: # Note: we use `_Param.description` instead of # `_Param.name`, that way we also get the '*' before args. - append((Signature.CurrentName, str(description))) + append((Signature + ',current-name', str(description))) else: append((Signature, str(description))) - append((Signature.Operator, ', ')) + append((Signature + ',operator', ', ')) if sig.params: # Pop last comma result.pop() - append((Signature.Operator, ')')) + append((Signature + ',operator', ')')) append((Signature, ' ')) return result return ConditionalContainer( content=Window( - TokenListControl(get_tokens), + FormattedTextControl(get_text_fragments), height=Dimension.exact(1)), filter= # Show only when there is a signature @@ -298,7 +289,7 @@ def status_bar(python_input): """ Create the `Layout` for the status bar. """ - TB = Token.Toolbar.Status + TB = 'class:status-toolbar' @if_mousedown def toggle_paste_mode(app, mouse_event): @@ -308,14 +299,14 @@ def toggle_paste_mode(app, mouse_event): def enter_history(app, mouse_event): python_input.enter_history(app) - def get_tokens(app): + def get_text_fragments(app): python_buffer = python_input.default_buffer result = [] append = result.append append((TB, ' ')) - result.extend(get_inputmode_tokens(app, python_input)) + result.extend(get_inputmode_fragments(app, python_input)) append((TB, ' ')) # Position in history. @@ -330,27 +321,27 @@ def get_tokens(app): append((TB, '[Ctrl-W] Cut [Meta-W] Copy [Ctrl-Y] Paste [Ctrl-G] Cancel')) else: result.extend([ - (TB.Key, '[F3]', enter_history), + (TB + ' class:key', '[F3]', enter_history), (TB, ' History ', enter_history), - (TB.Key, '[F6]', toggle_paste_mode), + (TB + ' class:key', '[F6]', toggle_paste_mode), (TB, ' ', toggle_paste_mode), ]) if python_input.paste_mode: - append((TB.PasteModeOn, 'Paste mode (on)', toggle_paste_mode)) + append((TB + ' class:paste-mode-on', 'Paste mode (on)', toggle_paste_mode)) else: append((TB, 'Paste mode', toggle_paste_mode)) return result return ConditionalContainer( - content=Window(content=TokenListControl(get_tokens), token=TB), + content=Window(content=FormattedTextControl(get_text_fragments), style=TB), filter=~IsDone() & RendererHeightIsKnown() & Condition(lambda app: python_input.show_status_bar and not python_input.show_exit_confirmation)) -def get_inputmode_tokens(app, python_input): +def get_inputmode_fragments(app, python_input): """ Return current input mode as a list of (token, text) tuples for use in a toolbar. @@ -361,36 +352,37 @@ def get_inputmode_tokens(app, python_input): def toggle_vi_mode(app, mouse_event): python_input.vi_mode = not python_input.vi_mode - token = Token.Toolbar.Status + token = 'class:status-toolbar' + input_mode_t = 'class:status-toolbar,input-mode' mode = app.vi_state.input_mode result = [] append = result.append - append((token.InputMode, '[F4] ', toggle_vi_mode)) + append((token + 'class:input-mode', '[F4] ', toggle_vi_mode)) # InputMode if python_input.vi_mode: if bool(app.current_buffer.selection_state): if app.current_buffer.selection_state.type == SelectionType.LINES: - append((token.InputMode, 'Vi (VISUAL LINE)', toggle_vi_mode)) + append((input_mode_t, 'Vi (VISUAL LINE)', toggle_vi_mode)) elif app.current_buffer.selection_state.type == SelectionType.CHARACTERS: - append((token.InputMode, 'Vi (VISUAL)', toggle_vi_mode)) + append((input_mode_t, 'Vi (VISUAL)', toggle_vi_mode)) append((token, ' ')) elif app.current_buffer.selection_state.type == 'BLOCK': - append((token.InputMode, 'Vi (VISUAL BLOCK)', toggle_vi_mode)) + append((input_mode_t, 'Vi (VISUAL BLOCK)', toggle_vi_mode)) append((token, ' ')) elif mode in (InputMode.INSERT, 'vi-insert-multiple'): - append((token.InputMode, 'Vi (INSERT)', toggle_vi_mode)) + append((input_mode_t, 'Vi (INSERT)', toggle_vi_mode)) append((token, ' ')) elif mode == InputMode.NAVIGATION: - append((token.InputMode, 'Vi (NAV)', toggle_vi_mode)) + append((input_mode_t, 'Vi (NAV)', toggle_vi_mode)) append((token, ' ')) elif mode == InputMode.REPLACE: - append((token.InputMode, 'Vi (REPLACE)', toggle_vi_mode)) + append((input_mode_t, 'Vi (REPLACE)', toggle_vi_mode)) append((token, ' ')) else: - append((token.InputMode, 'Emacs', toggle_vi_mode)) + append((input_mode_t, 'Emacs', toggle_vi_mode)) append((token, ' ')) return result @@ -406,27 +398,25 @@ def toggle_sidebar(app, mouse_event): " Click handler for the menu. " python_input.show_sidebar = not python_input.show_sidebar - token = Token.Toolbar.Status - version = sys.version_info tokens = [ - (token.Key, '[F2]', toggle_sidebar), - (token, ' Menu', toggle_sidebar), - (token, ' - '), - (token.PythonVersion, '%s %i.%i.%i' % (platform.python_implementation(), + ('class:status-toolbar.key', '[F2]', toggle_sidebar), + ('class:status-toolbar', ' Menu', toggle_sidebar), + ('class:status-toolbar', ' - '), + ('class:status-toolbar.python-version', '%s %i.%i.%i' % (platform.python_implementation(), version[0], version[1], version[2])), - (token, ' '), + ('class:status-toolbar', ' '), ] - width = token_list_width(tokens) + width = fragment_list_width(tokens) - def get_tokens(app): + def get_text_fragments(app): # Python version return tokens return ConditionalContainer( content=Window( - TokenListControl(get_tokens), - token=token, + FormattedTextControl(get_text_fragments), + style='class:status-toolbar', height=Dimension.exact(1), width=Dimension.exact(width)), filter=~IsDone() & RendererHeightIsKnown() & @@ -434,22 +424,22 @@ def get_tokens(app): not python_input.show_exit_confirmation)) -def exit_confirmation(python_input, token=Token.ExitConfirmation): +def exit_confirmation(python_input, style='class:exit-confirmation'): """ Create `Layout` for the exit message. """ - def get_tokens(app): + def get_text_fragments(app): # Show "Do you really want to exit?" return [ - (token, '\n %s ([y]/n)' % python_input.exit_message), - (Token.SetCursorPosition, ''), - (token, ' \n'), + (style, '\n %s ([y]/n)' % python_input.exit_message), + ('[SetCursorPosition]', ''), + (style, ' \n'), ] visible = ~IsDone() & Condition(lambda app: python_input.show_exit_confirmation) return ConditionalContainer( - content=Window(TokenListControl(get_tokens), token=token), # , has_focus=visible)), + content=Window(FormattedTextControl(get_text_fragments), style=style), # , has_focus=visible)), filter=visible) @@ -457,8 +447,8 @@ def meta_enter_message(python_input): """ Create the `Layout` for the 'Meta+Enter` message. """ - def get_tokens(app): - return [(Token.AcceptMessage, ' [Meta+Enter] Execute ')] + def get_text_fragments(app): + return [('class:accept-message', ' [Meta+Enter] Execute ')] def extra_condition(app): " Only show when... " @@ -473,7 +463,7 @@ def extra_condition(app): visible = ~IsDone() & HasFocus(DEFAULT_BUFFER) & Condition(extra_condition) return ConditionalContainer( - content=Window(TokenListControl(get_tokens)), + content=Window(FormattedTextControl(get_text_fragments)), filter=visible) @@ -580,23 +570,26 @@ def menu_position(app): content=Window( height=D.exact(1), char='\u2500', - token=Token.Separator), + style='class:separator'), filter=HasSignature(python_input) & ShowDocstring(python_input) & ~IsDone()), ConditionalContainer( content=Window( BufferControl( buffer=python_input.docstring_buffer, - lexer=SimpleLexer(token=Token.Docstring), + lexer=SimpleLexer(style='class:docstring'), #lexer=PythonLexer, ), height=D(max=12)), filter=HasSignature(python_input) & ShowDocstring(python_input) & ~IsDone(), ), ]), - HSplit([ - python_sidebar(python_input), - python_sidebar_navigation(python_input), - ]) + ConditionalContainer( + content=HSplit([ + python_sidebar(python_input), + Window(style='class:sidebar,separator', height=1), + python_sidebar_navigation(python_input), + ]), + filter=ShowSidebar(python_input) & ~IsDone()) ]), ] + extra_toolbars + [ VSplit([ diff --git a/ptpython/prompt_style.py b/ptpython/prompt_style.py index 5a2c3660..beb02855 100644 --- a/ptpython/prompt_style.py +++ b/ptpython/prompt_style.py @@ -1,6 +1,5 @@ from __future__ import unicode_literals from abc import ABCMeta, abstractmethod -from prompt_toolkit.token import Token from six import with_metaclass __all__ = ( @@ -44,22 +43,22 @@ def __init__(self, python_input): def in_tokens(self, cli): return [ - (Token.In, 'In ['), - (Token.In.Number, '%s' % self.python_input.current_statement_index), - (Token.In, ']: '), + ('class:in', 'In ['), + ('class:in.number', '%s' % self.python_input.current_statement_index), + ('class:in', ']: '), ] def in2_tokens(self, cli, width): return [ - (Token.In, '...: '.rjust(width)), + ('class:in', '...: '.rjust(width)), ] def out_tokens(self, cli): return [ - (Token.Out, 'Out['), - (Token.Out.Number, '%s' % self.python_input.current_statement_index), - (Token.Out, ']:'), - (Token, ' '), + ('class:out', 'Out['), + ('class:out.number', '%s' % self.python_input.current_statement_index), + ('class:out', ']:'), + ('', ' '), ] @@ -68,10 +67,10 @@ class ClassicPrompt(PromptStyle): The classic Python prompt. """ def in_tokens(self, cli): - return [(Token.Prompt, '>>> ')] + return [('class:prompt', '>>> ')] def in2_tokens(self, cli, width): - return [(Token.Prompt.Dots, '...')] + return [('class:prompt.dots', '...')] def out_tokens(self, cli): return [] diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 05f17d0a..1dc5ab06 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -516,8 +516,7 @@ def _create_application(self): mouse_support=Condition(lambda app: self.enable_mouse_support), style=DynamicStyle(lambda: self._current_style), get_title=lambda: self.terminal_title, - reverse_vi_search_direction=True, - on_input_timeout=self._on_input_timeout) + reverse_vi_search_direction=True) def _create_buffer(self): """ @@ -536,7 +535,8 @@ def _create_buffer(self): auto_suggest=ConditionalAutoSuggest( AutoSuggestFromHistory(), Condition(lambda app: self.enable_auto_suggest)), - accept_handler=self._accept_handler) + accept_handler=self._accept_handler, + on_text_changed=self._on_input_timeout) return python_buffer @@ -559,21 +559,20 @@ def vi_mode(self, value): else: self.editing_mode = EditingMode.EMACS - def _on_input_timeout(self, app): + def _on_input_timeout(self, buff): """ When there is no input activity, in another thread, get the signature of the current code. """ - if app.current_buffer_name != DEFAULT_BUFFER: - return + assert isinstance(buff, Buffer) + app = self.app # Never run multiple get-signature threads. if self._get_signatures_thread_running: return self._get_signatures_thread_running = True - buffer = app.current_buffer - document = buffer.document + document = buff.document def run(): script = get_jedi_script_from_document(document, self.get_locals(), self.get_globals()) @@ -609,7 +608,7 @@ def run(): # Set signatures and redraw if the text didn't change in the # meantime. Otherwise request new signatures. - if buffer.text == document.text: + if buff.text == document.text: self.signatures = signatures # Set docstring in docstring buffer. @@ -617,16 +616,16 @@ def run(): string = signatures[0].docstring() if not isinstance(string, six.text_type): string = string.decode('utf-8') - app.buffers['docstring'].reset( - initial_document=Document(string, cursor_position=0)) + self.docstring_buffer.reset( + document=Document(string, cursor_position=0)) else: - app.buffers['docstring'].reset() + self.docstring_buffer.reset() - app.request_redraw() + app.invalidate() else: - self._on_input_timeout(app) + self._on_input_timeout(buff) - app.eventloop.run_in_executor(run) + app.loop.run_in_executor(run) def on_reset(self, app): self.signatures = [] diff --git a/ptpython/repl.py b/ptpython/repl.py index 5214eafa..8baed20e 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -13,10 +13,9 @@ from pygments.styles.default import DefaultStyle from prompt_toolkit.document import Document -from prompt_toolkit.enums import DEFAULT_BUFFER from prompt_toolkit.eventloop.defaults import create_asyncio_event_loop -from prompt_toolkit.layout.utils import token_list_width -from prompt_toolkit.styles import style_from_pygments +from prompt_toolkit.layout.utils import fragment_list_width +from prompt_toolkit.styles import style_from_pygments, token_list_to_formatted_text from prompt_toolkit.utils import DummyContext from .python_input import PythonInput @@ -126,12 +125,13 @@ def compile_with_flags(code, mode): result_str = '%s\n' % repr(result).decode('utf-8') # Align every line to the first one. - line_sep = '\n' + ' ' * token_list_width(out_tokens) + line_sep = '\n' + ' ' * fragment_list_width(out_tokens) result_str = line_sep.join(result_str.splitlines()) + '\n' # Write output tokens. out_tokens.extend(_lex_python_result(result_str)) - self.app.print_tokens(out_tokens) + self.app.print_formatted_text( + token_list_to_formatted_text(out_tokens)) # If not a valid `eval` expression, run using `exec` instead. except SyntaxError: code = compile_with_flags(line, 'exec') @@ -168,7 +168,9 @@ def _handle_exception(self, e): # (We use the default style. Most other styles result # in unreadable colors for the traceback.) tokens = _lex_python_traceback(tb) - self.app.print_tokens(tokens, style=style_from_pygments(DefaultStyle)) + self.app.print_formatted_text( + token_list_to_formatted_text(tokens), + style=style_from_pygments(DefaultStyle)) output.write('%s\n' % e) output.flush() diff --git a/ptpython/style.py b/ptpython/style.py index 2a614b10..3b9bbfe9 100644 --- a/ptpython/style.py +++ b/ptpython/style.py @@ -1,9 +1,9 @@ from __future__ import unicode_literals -from prompt_toolkit.token import Token +from prompt_toolkit.styles import Style, merge_styles, default_style +from prompt_toolkit.styles.pygments import style_from_pygments +#from prompt_toolkit.utils import is_windows, is_conemu_ansi from pygments.styles import get_style_by_name, get_all_styles -from prompt_toolkit.styles import DEFAULT_STYLE_EXTENSIONS, style_from_dict -from prompt_toolkit.utils import is_windows, is_conemu_ansi __all__ = ( 'get_all_code_styles', @@ -16,8 +16,8 @@ def get_all_code_styles(): """ Return a mapping from style names to their classes. """ - result = dict((name, get_style_by_name(name).styles) for name in get_all_styles()) - result['win32'] = win32_code_style + result = dict((name, style_from_pygments(get_style_by_name(name))) for name in get_all_styles()) + result['win32'] = Style.from_dict(win32_code_style) return result @@ -26,8 +26,8 @@ def get_all_ui_styles(): Return a dict mapping {ui_style_name -> style_dict}. """ return { - 'default': default_ui_style, - 'blue': blue_ui_style, + 'default': Style.from_dict(default_ui_style), + 'blue': Style.from_dict(blue_ui_style), } @@ -36,156 +36,152 @@ def generate_style(python_style, ui_style): Generate Pygments Style class from two dictionaries containing style rules. """ - assert isinstance(python_style, dict) - assert isinstance(ui_style, dict) - - styles = {} - styles.update(DEFAULT_STYLE_EXTENSIONS) - styles.update(python_style) - styles.update(ui_style) - - return style_from_dict(styles) + return merge_styles([ + default_style(), + python_style, + ui_style + ]) # Code style for Windows consoles. They support only 16 colors, # so we choose a combination that displays nicely. win32_code_style = { - Token.Comment: "#00ff00", - Token.Keyword: '#44ff44', - Token.Number: '', - Token.Operator: '', - Token.String: '#ff44ff', - - Token.Name: '', - Token.Name.Decorator: '#ff4444', - Token.Name.Class: '#ff4444', - Token.Name.Function: '#ff4444', - Token.Name.Builtin: '#ff4444', - - Token.Name.Attribute: '', - Token.Name.Constant: '', - Token.Name.Entity: '', - Token.Name.Exception: '', - Token.Name.Label: '', - Token.Name.Namespace: '', - Token.Name.Tag: '', - Token.Name.Variable: '', + 'pygments.comment': "#00ff00", + 'pygments.keyword': '#44ff44', + 'pygments.number': '', + 'pygments.operator': '', + 'pygments.string': '#ff44ff', + + 'pygments.name': '', + 'pygments.name.decorator': '#ff4444', + 'pygments.name.class': '#ff4444', + 'pygments.name.function': '#ff4444', + 'pygments.name.builtin': '#ff4444', + + 'pygments.name.attribute': '', + 'pygments.name.constant': '', + 'pygments.name.entity': '', + 'pygments.name.exception': '', + 'pygments.name.label': '', + 'pygments.name.namespace': '', + 'pygments.name.tag': '', + 'pygments.name.variable': '', } default_ui_style = { - # Classic prompt. - Token.Prompt: 'bold', - Token.Prompt.Dots: 'noinherit', - - # (IPython <5.0) Prompt: "In [1]:" - Token.In: 'bold #008800', - Token.In.Number: '', - - # Return value. - Token.Out: '#ff0000', - Token.Out.Number: '#ff0000', - - # Separator between windows. (Used above docstring.) - Token.Separator: '#bbbbbb', - - # System toolbar - Token.Toolbar.System: '#22aaaa noinherit', - - # "arg" toolbar. - Token.Toolbar.Arg: '#22aaaa noinherit', - Token.Toolbar.Arg.Text: 'noinherit', - - # Signature toolbar. - Token.Toolbar.Signature: 'bg:#44bbbb #000000', - Token.Toolbar.Signature.CurrentName: 'bg:#008888 #ffffff bold', - Token.Toolbar.Signature.Operator: '#000000 bold', - - Token.Docstring: '#888888', - - # Validation toolbar. - Token.Toolbar.Validation: 'bg:#440000 #aaaaaa', - - # Status toolbar. - Token.Toolbar.Status: 'bg:#222222 #aaaaaa', - Token.Toolbar.Status.Title: 'underline', - Token.Toolbar.Status.InputMode: 'bg:#222222 #ffffaa', - Token.Toolbar.Status.Key: 'bg:#000000 #888888', - Token.Toolbar.Status.PasteModeOn: 'bg:#aa4444 #ffffff', - Token.Toolbar.Status.PythonVersion: 'bg:#222222 #ffffff bold', - - # When Control-C has been pressed. Grayed. - Token.Aborted: '#888888 bg:default important', - - # The options sidebar. - Token.Sidebar: 'bg:#bbbbbb #000000', - Token.Sidebar.Title: 'bg:#668866 #ffffff', - Token.Sidebar.Label: 'bg:#bbbbbb #222222', - Token.Sidebar.Status: 'bg:#dddddd #000011', - Token.Sidebar.Selected.Label: 'bg:#222222 #eeeeee', - Token.Sidebar.Selected.Status: 'bg:#444444 #ffffff bold', - - Token.Sidebar.Separator: 'bg:#bbbbbb #ffffff underline', - Token.Sidebar.Key: 'bg:#bbddbb #000000 bold', - Token.Sidebar.Key.Description: 'bg:#bbbbbb #000000', - Token.Sidebar.HelpText: 'bg:#fdf6e3 #000011', - - # Styling for the history layout. - Token.History.Line: '', - Token.History.Line.Selected: 'bg:#008800 #000000', - Token.History.Line.Current: 'bg:#ffffff #000000', - Token.History.Line.Selected.Current: 'bg:#88ff88 #000000', - Token.History.ExistingInput: '#888888', - - # Help Window. - Token.Window.Border: '#aaaaaa', - Token.Window.Title: 'bg:#bbbbbb #000000', - - # Meta-enter message. - Token.AcceptMessage: 'bg:#ffff88 #444444', - - # Exit confirmation. - Token.ExitConfirmation: 'bg:#884444 #ffffff', + # Classic prompt. + 'prompt': 'bold', + 'prompt.dots': 'noinherit', + + # (IPython <5.0) Prompt: "In [1]:" + 'in': 'bold #008800', + 'in.number': '', + + # Return value. + 'out': '#ff0000', + 'out.number': '#ff0000', + + # Separator between windows. (Used above docstring.) + 'separator': '#bbbbbb', + + # System toolbar + 'system-toolbar': '#22aaaa noinherit', + + # "arg" toolbar. + 'arg-toolbar': '#22aaaa noinherit', + 'arg-toolbar.text': 'noinherit', + + # Signature toolbar. + 'signature-toolbar': 'bg:#44bbbb #000000', + 'signature-toolbar.currentname': 'bg:#008888 #ffffff bold', + 'signature-toolbar.operator': '#000000 bold', + + 'docstring': '#888888', + + # Validation toolbar. + 'validation-toolbar': 'bg:#440000 #aaaaaa', + + # Status toolbar. + 'status-toolbar': 'bg:#222222 #aaaaaa', + 'status-toolbar.title': 'underline', + 'status-toolbar.inputmode': 'bg:#222222 #ffffaa', + 'status-toolbar.key': 'bg:#000000 #888888', + 'status-toolbar.pastemodeon': 'bg:#aa4444 #ffffff', + 'status-toolbar.pythonversion': 'bg:#222222 #ffffff bold', + + # When Control-C has been pressed. Grayed. +# 'aborted': '#888888 bg:default important', + + # The options sidebar. + 'sidebar': 'bg:#bbbbbb #000000', + 'sidebar.title': 'bg:#668866 #ffffff', + 'sidebar.label': 'bg:#bbbbbb #222222', + 'sidebar.status': 'bg:#dddddd #000011', + 'sidebar.label selected': 'bg:#222222 #eeeeee', + 'sidebar.status selected': 'bg:#444444 #ffffff bold', + + 'sidebar.separator': 'underline', + 'sidebar.key': 'bg:#bbddbb #000000 bold', + 'sidebar.key.description': 'bg:#bbbbbb #000000', + 'sidebar.helptext': 'bg:#fdf6e3 #000011', + +# # Styling for the history layout. +# history.line: '', +# history.line.selected: 'bg:#008800 #000000', +# history.line.current: 'bg:#ffffff #000000', +# history.line.selected.current: 'bg:#88ff88 #000000', +# history.existinginput: '#888888', + + # Help Window. + 'window-border': '#aaaaaa', + 'window-title': 'bg:#bbbbbb #000000', + + # Meta-enter message. + 'accept-message': 'bg:#ffff88 #444444', + + # Exit confirmation. + 'exit-confirmation': 'bg:#884444 #ffffff', } -# Some changes to get a bit more contrast on Windows consoles. -# (They only support 16 colors.) -if is_windows() and not is_conemu_ansi(): - default_ui_style.update({ - Token.Sidebar.Title: 'bg:#00ff00 #ffffff', - Token.ExitConfirmation: 'bg:#ff4444 #ffffff', - Token.Toolbar.Validation: 'bg:#ff4444 #ffffff', - - Token.Menu.Completions.Completion: 'bg:#ffffff #000000', - Token.Menu.Completions.Completion.Current: 'bg:#aaaaaa #000000', - }) +# # Some changes to get a bit more contrast on Windows consoles. +# # (They only support 16 colors.) +# if is_windows() and not is_conemu_ansi(): +# default_ui_style.update({ +# Token.Sidebar.Title: 'bg:#00ff00 #ffffff', +# Token.ExitConfirmation: 'bg:#ff4444 #ffffff', +# Token.Toolbar.Validation: 'bg:#ff4444 #ffffff', +# +# Token.Menu.Completions.Completion: 'bg:#ffffff #000000', +# Token.Menu.Completions.Completion.Current: 'bg:#aaaaaa #000000', +# }) blue_ui_style = {} blue_ui_style.update(default_ui_style) -blue_ui_style.update({ - # Line numbers. - Token.LineNumber: '#aa6666', - - # Highlighting of search matches in document. - Token.SearchMatch: '#ffffff bg:#4444aa', - Token.SearchMatch.Current: '#ffffff bg:#44aa44', - - # Highlighting of select text in document. - Token.SelectedText: '#ffffff bg:#6666aa', - - # Completer toolbar. - Token.Toolbar.Completions: 'bg:#44bbbb #000000', - Token.Toolbar.Completions.Arrow: 'bg:#44bbbb #000000 bold', - Token.Toolbar.Completions.Completion: 'bg:#44bbbb #000000', - Token.Toolbar.Completions.Completion.Current: 'bg:#008888 #ffffff', - - # Completer menu. - Token.Menu.Completions.Completion: 'bg:#44bbbb #000000', - Token.Menu.Completions.Completion.Current: 'bg:#008888 #ffffff', - Token.Menu.Completions.Meta: 'bg:#449999 #000000', - Token.Menu.Completions.Meta.Current: 'bg:#00aaaa #000000', - Token.Menu.Completions.ProgressBar: 'bg:#aaaaaa', - Token.Menu.Completions.ProgressButton: 'bg:#000000', -}) +#blue_ui_style.update({ +# # Line numbers. +# Token.LineNumber: '#aa6666', +# +# # Highlighting of search matches in document. +# Token.SearchMatch: '#ffffff bg:#4444aa', +# Token.SearchMatch.Current: '#ffffff bg:#44aa44', +# +# # Highlighting of select text in document. +# Token.SelectedText: '#ffffff bg:#6666aa', +# +# # Completer toolbar. +# Token.Toolbar.Completions: 'bg:#44bbbb #000000', +# Token.Toolbar.Completions.Arrow: 'bg:#44bbbb #000000 bold', +# Token.Toolbar.Completions.Completion: 'bg:#44bbbb #000000', +# Token.Toolbar.Completions.Completion.Current: 'bg:#008888 #ffffff', +# +# # Completer menu. +# Token.Menu.Completions.Completion: 'bg:#44bbbb #000000', +# Token.Menu.Completions.Completion.Current: 'bg:#008888 #ffffff', +# Token.Menu.Completions.Meta: 'bg:#449999 #000000', +# Token.Menu.Completions.Meta.Current: 'bg:#00aaaa #000000', +# Token.Menu.Completions.ProgressBar: 'bg:#aaaaaa', +# Token.Menu.Completions.ProgressButton: 'bg:#000000', +#}) From efd4bf63ff2939ac02dda5a483d48302476ebf56 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 9 May 2017 20:23:34 -0700 Subject: [PATCH 185/470] Added enable_syntax_highlighting option. --- examples/ptpython_config/config.py | 3 +++ ptpython/python_input.py | 9 +++++++-- ptpython/repl.py | 16 ++++++++++------ 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/examples/ptpython_config/config.py b/examples/ptpython_config/config.py index 0d8c8eb3..9f15cafc 100644 --- a/examples/ptpython_config/config.py +++ b/examples/ptpython_config/config.py @@ -107,6 +107,9 @@ def configure(repl): # $TERM before changing.) repl.true_color = False + # Syntax. + repl.enable_syntax_highlighting = True + # Install custom colorscheme named 'my-colorscheme' and use it. """ repl.install_ui_colorscheme('my-colorscheme', _custom_ui_colorscheme) diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 1dc5ab06..d22e47c5 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -15,7 +15,7 @@ from prompt_toolkit.key_binding import merge_key_bindings, ConditionalKeyBindings, KeyBindings from prompt_toolkit.key_binding.defaults import load_key_bindings from prompt_toolkit.key_binding.vi_state import InputMode -from prompt_toolkit.layout.lexers import PygmentsLexer +from prompt_toolkit.layout.lexers import PygmentsLexer, DynamicLexer, SimpleLexer from prompt_toolkit.output.defaults import create_output from prompt_toolkit.styles import DynamicStyle from prompt_toolkit.utils import is_windows @@ -179,6 +179,7 @@ def __init__(self, # history on the records starting # with the current input. + self.enable_syntax_highlighting = True self.highlight_matching_parenthesis = False self.show_sidebar = False # Currently show the sidebar. self.show_sidebar_help = True # When the sidebar is visible, also show the help text. @@ -458,6 +459,9 @@ def get_values(): field_name='highlight_matching_parenthesis'), ]), OptionCategory('Colors', [ + simple_option(title='Syntax highlighting', + description='Use colors for syntax highligthing', + field_name='enable_syntax_highlighting'), Option(title='Code', description='Color scheme to use for the Python code.', get_current_value=lambda: self._current_code_style_name, @@ -486,7 +490,8 @@ def _create_application(self): output=self.output, layout=create_layout( self, - lexer=self._lexer, + lexer=DynamicLexer( + lambda: self._lexer if self.enable_syntax_highlighting else SimpleLexer()), input_buffer_height=self._input_buffer_height, extra_buffer_processors=self._extra_buffer_processors, extra_body=self._extra_layout_body, diff --git a/ptpython/repl.py b/ptpython/repl.py index 8baed20e..af6bfcf2 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -10,12 +10,11 @@ from __future__ import unicode_literals from pygments.lexers import PythonTracebackLexer, PythonLexer -from pygments.styles.default import DefaultStyle from prompt_toolkit.document import Document from prompt_toolkit.eventloop.defaults import create_asyncio_event_loop from prompt_toolkit.layout.utils import fragment_list_width -from prompt_toolkit.styles import style_from_pygments, token_list_to_formatted_text +from prompt_toolkit.styles import token_list_to_formatted_text from prompt_toolkit.utils import DummyContext from .python_input import PythonInput @@ -129,7 +128,10 @@ def compile_with_flags(code, mode): result_str = line_sep.join(result_str.splitlines()) + '\n' # Write output tokens. - out_tokens.extend(_lex_python_result(result_str)) + if self.enable_syntax_highlighting: + out_tokens.extend(_lex_python_result(result_str)) + else: + out_tokens.append(('', result_str)) self.app.print_formatted_text( token_list_to_formatted_text(out_tokens)) # If not a valid `eval` expression, run using `exec` instead. @@ -167,10 +169,12 @@ def _handle_exception(self, e): # Format exception and write to output. # (We use the default style. Most other styles result # in unreadable colors for the traceback.) - tokens = _lex_python_traceback(tb) + if self.enable_syntax_highlighting: + tokens = _lex_python_traceback(tb) + else: + tokens = [('', tb)] self.app.print_formatted_text( - token_list_to_formatted_text(tokens), - style=style_from_pygments(DefaultStyle)) + token_list_to_formatted_text(tokens)) output.write('%s\n' % e) output.flush() From fe15a6504f5cbcdabf535d24e96be4aee22e4152 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 9 May 2017 20:23:52 -0700 Subject: [PATCH 186/470] Config example fixes. --- examples/ptpython_config/config.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/examples/ptpython_config/config.py b/examples/ptpython_config/config.py index 9f15cafc..273977ef 100644 --- a/examples/ptpython_config/config.py +++ b/examples/ptpython_config/config.py @@ -5,7 +5,7 @@ """ from __future__ import unicode_literals from prompt_toolkit.filters import ViInsertMode -from prompt_toolkit.key_binding.input_processor import KeyPress +from prompt_toolkit.key_binding.key_processor import KeyPress from prompt_toolkit.keys import Keys from pygments.token import Token @@ -40,7 +40,7 @@ def configure(repl): repl.completion_menu_scroll_offset = 0 # Show line numbers (when the input contains multiple lines.) - repl.show_line_numbers = True + repl.show_line_numbers = False # Show status bar. repl.show_status_bar = True @@ -117,29 +117,35 @@ def configure(repl): """ # Add custom key binding for PDB. + """ @repl.add_key_binding(Keys.ControlB) def _(event): ' Pressing Control-B will insert "pdb.set_trace()" ' event.cli.current_buffer.insert_text('\nimport pdb; pdb.set_trace()\n') + """ # Typing ControlE twice should also execute the current command. # (Alternative for Meta-Enter.) + """ @repl.add_key_binding(Keys.ControlE, Keys.ControlE) def _(event): b = event.current_buffer if b.accept_action.is_returnable: b.accept_action.validate_and_handle(event.cli, b) + """ # Typing 'jj' in Vi Insert mode, should send escape. (Go back to navigation # mode.) + """ @repl.add_key_binding('j', 'j', filter=ViInsertMode()) def _(event): " Map 'jj' to Escape. " event.cli.input_processor.feed(KeyPress(Keys.Escape)) - """ + # Custom key binding for some simple autocorrection while typing. + """ corrections = { 'impotr': 'import', 'pritn': 'print', From 70fcb866d21c3aebf4c52e9d42b0d433b33b6bac Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 23 May 2017 11:16:31 +0200 Subject: [PATCH 187/470] Prompt_toolkit 2.0 changes. --- ptpython/eventloop.py | 5 +- ptpython/filters.py | 10 +-- ptpython/history_browser.py | 136 +++++++++++++----------------------- ptpython/ipython.py | 16 ++--- ptpython/key_bindings.py | 51 ++++++++------ ptpython/layout.py | 85 +++++++++++----------- ptpython/prompt_style.py | 18 ++--- ptpython/python_input.py | 66 ++++++++--------- ptpython/repl.py | 2 +- ptpython/utils.py | 4 +- 10 files changed, 182 insertions(+), 211 deletions(-) diff --git a/ptpython/eventloop.py b/ptpython/eventloop.py index 3d6103da..4794988d 100644 --- a/ptpython/eventloop.py +++ b/ptpython/eventloop.py @@ -8,6 +8,7 @@ will fix it for Tk.) """ from prompt_toolkit.eventloop.defaults import create_event_loop as _create_event_loop +from prompt_toolkit.eventloop.defaults import set_event_loop import sys import time @@ -72,4 +73,6 @@ def _inputhook(inputhook_context): def create_event_loop(): - return _create_event_loop(inputhook=_inputhook) + loop = _create_event_loop(inputhook=_inputhook) + set_event_loop(loop) + return loop diff --git a/ptpython/filters.py b/ptpython/filters.py index 1180af13..ddf7cf5c 100644 --- a/ptpython/filters.py +++ b/ptpython/filters.py @@ -13,25 +13,25 @@ class PythonInputFilter(Filter): def __init__(self, python_input): self.python_input = python_input - def __call__(self, cli): + def __call__(self): raise NotImplementedError class HasSignature(PythonInputFilter): - def __call__(self, cli): + def __call__(self): return bool(self.python_input.signatures) class ShowSidebar(PythonInputFilter): - def __call__(self, cli): + def __call__(self): return self.python_input.show_sidebar class ShowSignature(PythonInputFilter): - def __call__(self, cli): + def __call__(self): return self.python_input.show_signature class ShowDocstring(PythonInputFilter): - def __call__(self, cli): + def __call__(self): return self.python_input.show_docstring diff --git a/ptpython/history_browser.py b/ptpython/history_browser.py index bc24ee36..5149359a 100644 --- a/ptpython/history_browser.py +++ b/ptpython/history_browser.py @@ -7,13 +7,12 @@ from __future__ import unicode_literals from prompt_toolkit.application import Application +from prompt_toolkit.application.current import get_app from prompt_toolkit.buffer import Buffer from prompt_toolkit.document import Document from prompt_toolkit.enums import DEFAULT_BUFFER from prompt_toolkit.filters import Condition, has_focus -from prompt_toolkit.key_binding import KeyBindings, merge_key_bindings -from prompt_toolkit.key_binding.defaults import load_key_bindings -from prompt_toolkit.keys import Keys +from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.layout.containers import HSplit, VSplit, Window, FloatContainer, Float, ConditionalContainer, Container, ScrollOffsets, Align from prompt_toolkit.layout.controls import BufferControl, FormattedTextControl from prompt_toolkit.layout.dimension import Dimension as D @@ -21,8 +20,9 @@ from prompt_toolkit.layout.lexers import PygmentsLexer from prompt_toolkit.layout.margins import Margin, ScrollbarMargin from prompt_toolkit.layout.processors import Processor, Transformation, HighlightSearchProcessor, HighlightSelectionProcessor, merge_processors -from prompt_toolkit.layout.toolbars import ArgToolbar, SearchToolbar +from prompt_toolkit.layout.widgets.toolbars import ArgToolbar, SearchToolbar from prompt_toolkit.layout.utils import fragment_list_to_text +from prompt_toolkit.layout.widgets import Frame from pygments.lexers import RstLexer from .utils import if_mousedown @@ -101,43 +101,7 @@ def _create_popup_window(title, body): """ assert isinstance(title, six.text_type) assert isinstance(body, Container) - - return HSplit([ - VSplit([ - Window(width=D.exact(1), height=D.exact(1), - char=BORDER.TOP_LEFT, - style='class:window.border'), - Window( - content=FormattedTextControl( - lambda app: [('class:window.title', ' %s ' % title)]), - align=Align.CENTER, - char=BORDER.HORIZONTAL, - style='class:window.border'), - Window(width=D.exact(1), height=D.exact(1), - char=BORDER.TOP_RIGHT, - style='class:window.border'), - ]), - VSplit([ - Window(width=D.exact(1), - char=BORDER.VERTICAL, - style='class:window.border'), - body, - Window(width=D.exact(1), - char=BORDER.VERTICAL, - style='class:window.border'), - ]), - VSplit([ - Window(width=D.exact(1), height=D.exact(1), - char=BORDER.BOTTOM_LEFT, - style='class:window.border'), - Window(height=D.exact(1), - char=BORDER.HORIZONTAL, - style='class:window.border'), - Window(width=D.exact(1), height=D.exact(1), - char=BORDER.BOTTOM_RIGHT, - style='class:window.border'), - ]), - ]) + return Frame(body=body, title=title) class HistoryLayout(object): @@ -160,7 +124,7 @@ def __init__(self, history): title='History Help', body=Window( content=self.help_buffer_control, - right_margins=[ScrollbarMargin()], + right_margins=[ScrollbarMargin(display_arrows=True)], scroll_offsets=ScrollOffsets(top=2, bottom=2), transparent=False)) @@ -225,22 +189,22 @@ def __init__(self, history): self.layout = Layout(self.root_container, history_window) -def _get_top_toolbar_fragments(app): +def _get_top_toolbar_fragments(): return [('class:status-bar.title', 'History browser - Insert from history')] -def _get_bottom_toolbar_fragments(app, history): +def _get_bottom_toolbar_fragments(history): python_input = history.python_input @if_mousedown - def f1(app, mouse_event): + def f1(mouse_event): _toggle_help(history) @if_mousedown - def tab(app, mouse_event): + def tab(mouse_event): _select_other_window(history) return [ - ('class:status-toolbar', ' ') ] + get_inputmode_fragments(app, python_input) + [ + ('class:status-toolbar', ' ') ] + get_inputmode_fragments(python_input) + [ ('class:status-toolbar', ' '), ('class:status-toolbar.key', '[Space]'), ('class:status-toolbar', ' Toggle '), @@ -262,10 +226,10 @@ def __init__(self, history): self.history_buffer = history.history_buffer self.history_mapping = history.history_mapping - def get_width(self, app, ui_content): + def get_width(self, ui_content): return 2 - def create_margin(self, app, window_render_info, width, height): + def create_margin(self, window_render_info, width, height): document = self.history_buffer.document lines_starting_new_entries = self.history_mapping.lines_starting_new_entries @@ -308,10 +272,10 @@ def __init__(self, history): self.history_mapping = history.history_mapping self.history_buffer = history.history_buffer - def get_width(self, app, ui_content): + def get_width(self, ui_content): return 2 - def create_margin(self, app, window_render_info, width, height): + def create_margin(self, window_render_info, width, height): document = self.history_buffer.document current_lineno = document.cursor_position_row @@ -337,7 +301,7 @@ def create_margin(self, app, window_render_info, width, height): return result - def invalidation_hash(self, app, document): + def invalidation_hash(self, document): return document.cursor_position_row @@ -418,7 +382,7 @@ def get_new_document(self, cursor_pos=None): cursor_pos = len(text) return Document(text, cursor_pos) - def update_default_buffer(self, app): + def update_default_buffer(self): b = self.history.default_buffer b.set_document( @@ -430,7 +394,7 @@ def _toggle_help(history): help_buffer_control = history.history_layout.help_buffer_control if history.app.layout.current_control == help_buffer_control: - history.app.layout.pop_focus() + history.app.layout.focus_previous() else: history.app.layout.current_control = help_buffer_control @@ -465,11 +429,11 @@ def _(event): if line_no in history_mapping.selected_lines: # Remove line. history_mapping.selected_lines.remove(line_no) - history_mapping.update_default_buffer(event.app) + history_mapping.update_default_buffer() else: # Add line. history_mapping.selected_lines.add(line_no) - history_mapping.update_default_buffer(event.app) + history_mapping.update_default_buffer() # Update cursor position default_buffer = history.default_buffer @@ -483,8 +447,8 @@ def _(event): b.cursor_position = b.document.translate_row_col_to_index(line_no + 1, 0) @handle(' ', filter=has_focus(DEFAULT_BUFFER)) - @handle(Keys.Delete, filter=has_focus(DEFAULT_BUFFER)) - @handle(Keys.ControlH, filter=has_focus(DEFAULT_BUFFER)) + @handle('delete', filter=has_focus(DEFAULT_BUFFER)) + @handle('c-h', filter=has_focus(DEFAULT_BUFFER)) def _(event): """ Space: remove line from default pane. @@ -500,63 +464,58 @@ def _(event): else: history_mapping.selected_lines.remove(history_lineno) - history_mapping.update_default_buffer(event.app) + history_mapping.update_default_buffer() help_focussed = has_focus(history.help_buffer) main_buffer_focussed = has_focus(history.history_buffer) | has_focus(history.default_buffer) - @handle(Keys.Tab, filter=main_buffer_focussed) - @handle(Keys.ControlX, filter=main_buffer_focussed, eager=True) + @handle('tab', filter=main_buffer_focussed) + @handle('c-x', filter=main_buffer_focussed, eager=True) # Eager: ignore the Emacs [Ctrl-X Ctrl-X] binding. - @handle(Keys.ControlW, filter=main_buffer_focussed) + @handle('c-w', filter=main_buffer_focussed) def _(event): " Select other window. " _select_other_window(history) - @handle(Keys.F4) + @handle('f4') def _(event): " Switch between Emacs/Vi mode. " python_input.vi_mode = not python_input.vi_mode - @handle(Keys.F1) + @handle('f1') def _(event): " Display/hide help. " _toggle_help(history) - @handle(Keys.Enter, filter=help_focussed) - @handle(Keys.ControlC, filter=help_focussed) - @handle(Keys.ControlG, filter=help_focussed) - @handle(Keys.Escape, filter=help_focussed) + @handle('enter', filter=help_focussed) + @handle('c-c', filter=help_focussed) + @handle('c-g', filter=help_focussed) + @handle('escape', filter=help_focussed) def _(event): " Leave help. " - event.app.layout.pop_focus() + event.app.layout.focus_previous() @handle('q', filter=main_buffer_focussed) - @handle(Keys.F3, filter=main_buffer_focussed) - @handle(Keys.ControlC, filter=main_buffer_focussed) - @handle(Keys.ControlG, filter=main_buffer_focussed) + @handle('f3', filter=main_buffer_focussed) + @handle('c-c', filter=main_buffer_focussed) + @handle('c-g', filter=main_buffer_focussed) def _(event): " Cancel and go back. " event.app.set_return_value(None) - @handle(Keys.Enter, filter=main_buffer_focussed) + @handle('enter', filter=main_buffer_focussed) def _(event): " Accept input. " event.app.set_return_value(history.default_buffer.document) - enable_system_bindings = Condition(lambda app: python_input.enable_system_bindings) + enable_system_bindings = Condition(lambda: python_input.enable_system_bindings) - @handle(Keys.ControlZ, filter=enable_system_bindings) + @handle('c-z', filter=enable_system_bindings) def _(event): " Suspend to background. " event.app.suspend_to_background() - return merge_key_bindings([ - load_key_bindings( - enable_search=True, - enable_extra_page_navigation=True), - bindings - ]) + return bindings class History(object): @@ -572,23 +531,25 @@ def __init__(self, python_input, original_document): history_mapping = HistoryMapping(self, python_input.history, original_document) self.history_mapping = history_mapping + document = Document(history_mapping.concatenated_history) + document = Document( + document.text, + cursor_position=document.cursor_position + document.get_start_of_line_position()) + self.history_buffer = Buffer( - loop=python_input.loop, - document=Document(history_mapping.concatenated_history), + document=document, on_cursor_position_changed=self._history_buffer_pos_changed, accept_handler=( - lambda app, buffer: app.set_return_value(self.default_buffer.text)), + lambda buff: get_app().set_return_value(self.default_buffer.text)), read_only=True) self.default_buffer = Buffer( - loop=python_input.loop, name=DEFAULT_BUFFER, document=history_mapping.get_new_document(), on_cursor_position_changed=self._default_buffer_pos_changed, read_only=True) self.help_buffer = Buffer( - loop=python_input.loop, document=Document(HELP_TEXT, 0), read_only=True ) @@ -596,11 +557,10 @@ def __init__(self, python_input, original_document): self.history_layout = HistoryLayout(self) self.app = Application( - loop=python_input.loop, layout=self.history_layout.layout, full_screen=True, style=python_input._current_style, - mouse_support=Condition(lambda app: python_input.enable_mouse_support), + mouse_support=Condition(lambda: python_input.enable_mouse_support), key_bindings=create_key_bindings(self, python_input, history_mapping) ) diff --git a/ptpython/ipython.py b/ptpython/ipython.py index 086e2848..c541b371 100644 --- a/ptpython/ipython.py +++ b/ptpython/ipython.py @@ -44,15 +44,15 @@ class IPythonPrompt(PromptStyle): def __init__(self, prompt_manager): self.prompt_manager = prompt_manager - def in_tokens(self, cli): + def in_tokens(self): text = self.prompt_manager.render('in', color=False, just=False) return [('class:in', text)] - def in2_tokens(self, cli, width): + def in2_tokens(self, width): text = self.prompt_manager.render('in2', color=False, just=False) return [('class:in', text.rjust(width))] - def out_tokens(self, cli): + def out_tokens(self): # This function is currently not used by IPython. But for completeness, # it would look like this. text = self.prompt_manager.render('out', color=False, just=False) @@ -66,13 +66,13 @@ class IPython5Prompt(PromptStyle): def __init__(self, prompts): self.prompts = prompts - def in_tokens(self, cli): - return self.prompts.in_prompt_tokens(cli) + def in_tokens(self): + return self.prompts.in_prompt_tokens() - def in2_tokens(self, cli, width): - return self.prompts.continuation_prompt_tokens(cli) + def in2_tokens(self, width): + return self.prompts.continuation_prompt_tokens() - def out_tokens(self, cli): + def out_tokens(self): return [] diff --git a/ptpython/key_bindings.py b/ptpython/key_bindings.py index 43cce9f2..58e4962b 100644 --- a/ptpython/key_bindings.py +++ b/ptpython/key_bindings.py @@ -5,6 +5,7 @@ from prompt_toolkit.filters import HasSelection, HasFocus, Condition, ViInsertMode, EmacsInsertMode, EmacsMode from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.keys import Keys +from prompt_toolkit.application import get_app from .utils import document_is_multiline_python __all__ = ( @@ -15,7 +16,7 @@ @Condition -def tab_should_insert_whitespace(app): +def tab_should_insert_whitespace(): """ When the 'tab' key is pressed with only whitespace character before the cursor, do autocompletion. Otherwise, insert indentation. @@ -24,7 +25,7 @@ def tab_should_insert_whitespace(app): completion. It doesn't make sense to start the first line with indentation. """ - b = app.current_buffer + b = get_app().current_buffer before_cursor = b.document.current_line_before_cursor return bool(b.text and (not before_cursor or before_cursor.isspace())) @@ -36,46 +37,46 @@ def load_python_bindings(python_input): """ bindings = KeyBindings() - sidebar_visible = Condition(lambda app: python_input.show_sidebar) + sidebar_visible = Condition(lambda: python_input.show_sidebar) handle = bindings.add has_selection = HasSelection() - @handle(Keys.ControlL) + @handle('c-l') def _(event): """ Clear whole screen and render again -- also when the sidebar is visible. """ event.app.renderer.clear() - @handle(Keys.F2) + @handle('f2') def _(event): """ Show/hide sidebar. """ python_input.show_sidebar = not python_input.show_sidebar - @handle(Keys.F3) + @handle('f3') def _(event): """ Select from the history. """ - python_input.enter_history(event.app) + python_input.enter_history() - @handle(Keys.F4) + @handle('f4') def _(event): """ Toggle between Vi and Emacs mode. """ python_input.vi_mode = not python_input.vi_mode - @handle(Keys.F6) + @handle('f6') def _(event): """ Enable/Disable paste mode. """ python_input.paste_mode = not python_input.paste_mode - @handle(Keys.Tab, filter= ~sidebar_visible & ~has_selection & tab_should_insert_whitespace) + @handle('tab', filter= ~sidebar_visible & ~has_selection & tab_should_insert_whitespace) def _(event): """ When tab should insert whitespace, do that instead of completion. @@ -83,10 +84,10 @@ def _(event): event.app.current_buffer.insert_text(' ') @Condition - def is_multiline(app): + def is_multiline(): return document_is_multiline_python(python_input.default_buffer.document) - @handle(Keys.Enter, filter= ~sidebar_visible & ~has_selection & + @handle('enter', filter= ~sidebar_visible & ~has_selection & (ViInsertMode() | EmacsInsertMode()) & HasFocus(DEFAULT_BUFFER) & ~is_multiline) @handle(Keys.Escape, Keys.Enter, filter= ~sidebar_visible & EmacsMode()) @@ -99,14 +100,13 @@ def _(event): if b.validate(): # When the cursor is at the end, and we have an empty line: # drop the empty lines, but return the value. - b.document = Document( text=b.text.rstrip(), cursor_position=len(b.text.rstrip())) - b.validate_and_handle(event.app) + b.validate_and_handle() - @handle(Keys.Enter, filter= ~sidebar_visible & ~has_selection & + @handle('enter', filter= ~sidebar_visible & ~has_selection & (ViInsertMode() | EmacsInsertMode()) & HasFocus(DEFAULT_BUFFER) & is_multiline) def _(event): @@ -131,29 +131,34 @@ def at_the_end(b): elif at_the_end(b) and b.document.text.replace(' ', '').endswith( '\n' * (empty_lines_required - 1)): + # When the cursor is at the end, and we have an empty line: + # drop the empty lines, but return the value. if b.validate(): - # When the cursor is at the end, and we have an empty line: - # drop the empty lines, but return the value. b.document = Document( text=b.text.rstrip(), cursor_position=len(b.text.rstrip())) - b.validate_and_handle(event.app) + b.validate_and_handle() else: auto_newline(b) - @handle(Keys.ControlD, filter=~sidebar_visible & Condition(lambda app: + @handle('c-d', filter=~sidebar_visible & Condition(lambda: # Only when the `confirm_exit` flag is set. python_input.confirm_exit and # And the current buffer is empty. - app.current_buffer == python_input.default_buffer and - not app.current_buffer.text)) + get_app().current_buffer == python_input.default_buffer and + not get_app().current_buffer.text)) def _(event): """ Override Control-D exit, to ask for confirmation. """ python_input.show_exit_confirmation = True + @handle('c-c') + def _(event): + " Abort when Control-C has been pressed. " + event.app.abort() + return bindings @@ -164,7 +169,7 @@ def load_sidebar_bindings(python_input): bindings = KeyBindings() handle = bindings.add - sidebar_visible = Condition(lambda app: python_input.show_sidebar) + sidebar_visible = Condition(lambda: python_input.show_sidebar) @handle(Keys.Up, filter=sidebar_visible) @handle(Keys.ControlP, filter=sidebar_visible) @@ -216,7 +221,7 @@ def load_confirm_exit_bindings(python_input): bindings = KeyBindings() handle = bindings.add - confirmation_visible = Condition(lambda app: python_input.show_exit_confirmation) + confirmation_visible = Condition(lambda: python_input.show_exit_confirmation) @handle('y', filter=confirmation_visible) @handle('Y', filter=confirmation_visible) diff --git a/ptpython/layout.py b/ptpython/layout.py index b3c0366e..7674b97d 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -3,6 +3,7 @@ """ from __future__ import unicode_literals +from prompt_toolkit.application import get_app from prompt_toolkit.enums import DEFAULT_BUFFER, SEARCH_BUFFER from prompt_toolkit.filters import IsDone, HasCompletions, RendererHeightIsKnown, HasFocus, Condition from prompt_toolkit.key_binding.vi_state import InputMode @@ -15,7 +16,7 @@ from prompt_toolkit.layout.menus import CompletionsMenu, MultiColumnCompletionsMenu from prompt_toolkit.layout.processors import ConditionalProcessor, AppendAutoSuggestion, HighlightSearchProcessor, HighlightSelectionProcessor, HighlightMatchingBracketProcessor, Processor, Transformation from prompt_toolkit.layout.processors import merge_processors -from prompt_toolkit.layout.toolbars import CompletionsToolbar, ArgToolbar, SearchToolbar, ValidationToolbar, SystemToolbar +from prompt_toolkit.layout.widgets.toolbars import CompletionsToolbar, ArgToolbar, SearchToolbar, ValidationToolbar, SystemToolbar from prompt_toolkit.layout.utils import fragment_list_width from prompt_toolkit.reactive import Integer from prompt_toolkit.selection import SelectionType @@ -43,7 +44,7 @@ class DisplayMultipleCursors(Processor): def __init__(self, *a): pass - def apply_transformation(self, app, document, lineno, + def apply_transformation(self, document, lineno, source_to_display, tokens): return Transformation(tokens) @@ -57,22 +58,22 @@ class CompletionVisualisation: def show_completions_toolbar(python_input): - return Condition(lambda app: python_input.completion_visualisation == CompletionVisualisation.TOOLBAR) + return Condition(lambda: python_input.completion_visualisation == CompletionVisualisation.TOOLBAR) def show_completions_menu(python_input): - return Condition(lambda app: python_input.completion_visualisation == CompletionVisualisation.POP_UP) + return Condition(lambda: python_input.completion_visualisation == CompletionVisualisation.POP_UP) def show_multi_column_completions_menu(python_input): - return Condition(lambda app: python_input.completion_visualisation == CompletionVisualisation.MULTI_COLUMN) + return Condition(lambda: python_input.completion_visualisation == CompletionVisualisation.MULTI_COLUMN) def python_sidebar(python_input): """ Create the `Layout` for the sidebar with the configurable options. """ - def get_text_fragments(app): + def get_text_fragments(): tokens = [] def append_category(category): @@ -86,11 +87,11 @@ def append(index, label, status): selected = index == python_input.selected_option_index @if_mousedown - def select_item(app, mouse_event): + def select_item(mouse_event): python_input.selected_option_index = index @if_mousedown - def goto_next(app, mouse_event): + def goto_next(mouse_event): " Select item and go to next value. " python_input.selected_option_index = index option = python_input.selected_option @@ -123,10 +124,10 @@ def goto_next(app, mouse_event): return tokens class Control(FormattedTextControl): - def move_cursor_down(self, app): + def move_cursor_down(self): python_input.selected_option_index += 1 - def move_cursor_up(self, app): + def move_cursor_up(self): python_input.selected_option_index -= 1 return Window( @@ -141,7 +142,7 @@ def python_sidebar_navigation(python_input): """ Create the `Layout` showing the navigation information for the sidebar. """ - def get_text_fragments(app): + def get_text_fragments(): tokens = [] # Show navigation info. @@ -183,7 +184,7 @@ def get_current_description(): i += 1 return '' - def get_help_text(app): + def get_help_text(): return [(token, get_current_description())] return ConditionalContainer( @@ -192,14 +193,14 @@ def get_help_text(app): style=token, height=Dimension(min=3)), filter=ShowSidebar(python_input) & - Condition(lambda app: python_input.show_sidebar_help) & ~IsDone()) + Condition(lambda: python_input.show_sidebar_help) & ~IsDone()) def signature_toolbar(python_input): """ Return the `Layout` for the signature. """ - def get_text_fragments(app): + def get_text_fragments(): result = [] append = result.append Signature = 'class:signature-toolbar' @@ -275,14 +276,14 @@ def __init__(self, python_input): def get_prompt_style(): return python_input.all_prompt_styles[python_input.prompt_style] - def get_prompt(app): - return get_prompt_style().in_tokens(app) + def get_prompt(): + return get_prompt_style().in_tokens() - def get_continuation_prompt(app, width): - return get_prompt_style().in2_tokens(app, width) + def get_continuation_prompt(width): + return get_prompt_style().in2_tokens(width) super(PythonPromptMargin, self).__init__(get_prompt, get_continuation_prompt, - show_numbers=Condition(lambda app: python_input.show_line_numbers)) + show_numbers=Condition(lambda: python_input.show_line_numbers)) def status_bar(python_input): @@ -292,21 +293,21 @@ def status_bar(python_input): TB = 'class:status-toolbar' @if_mousedown - def toggle_paste_mode(app, mouse_event): + def toggle_paste_mode(mouse_event): python_input.paste_mode = not python_input.paste_mode @if_mousedown - def enter_history(app, mouse_event): - python_input.enter_history(app) + def enter_history(mouse_event): + python_input.enter_history() - def get_text_fragments(app): + def get_text_fragments(): python_buffer = python_input.default_buffer result = [] append = result.append append((TB, ' ')) - result.extend(get_inputmode_fragments(app, python_input)) + result.extend(get_inputmode_fragments(python_input)) append((TB, ' ')) # Position in history. @@ -314,6 +315,7 @@ def get_text_fragments(app): len(python_buffer._working_lines)))) # Shortcuts. + app = get_app() if not python_input.vi_mode and app.current_buffer == python_input.search_buffer: append((TB, '[Ctrl-G] Cancel search [Enter] Go to this position.')) elif bool(app.current_buffer.selection_state) and not python_input.vi_mode: @@ -337,19 +339,18 @@ def get_text_fragments(app): return ConditionalContainer( content=Window(content=FormattedTextControl(get_text_fragments), style=TB), filter=~IsDone() & RendererHeightIsKnown() & - Condition(lambda app: python_input.show_status_bar and + Condition(lambda: python_input.show_status_bar and not python_input.show_exit_confirmation)) -def get_inputmode_fragments(app, python_input): +def get_inputmode_fragments(python_input): """ Return current input mode as a list of (token, text) tuples for use in a toolbar. - - :param app: `CommandLineInterface` instance. """ + app = get_app() @if_mousedown - def toggle_vi_mode(app, mouse_event): + def toggle_vi_mode(mouse_event): python_input.vi_mode = not python_input.vi_mode token = 'class:status-toolbar' @@ -394,7 +395,7 @@ def show_sidebar_button_info(python_input): (The right part of the status bar.) """ @if_mousedown - def toggle_sidebar(app, mouse_event): + def toggle_sidebar(mouse_event): " Click handler for the menu. " python_input.show_sidebar = not python_input.show_sidebar @@ -409,7 +410,7 @@ def toggle_sidebar(app, mouse_event): ] width = fragment_list_width(tokens) - def get_text_fragments(app): + def get_text_fragments(): # Python version return tokens @@ -420,7 +421,7 @@ def get_text_fragments(app): height=Dimension.exact(1), width=Dimension.exact(width)), filter=~IsDone() & RendererHeightIsKnown() & - Condition(lambda app: python_input.show_status_bar and + Condition(lambda: python_input.show_status_bar and not python_input.show_exit_confirmation)) @@ -428,7 +429,7 @@ def exit_confirmation(python_input, style='class:exit-confirmation'): """ Create `Layout` for the exit message. """ - def get_text_fragments(app): + def get_text_fragments(): # Show "Do you really want to exit?" return [ (style, '\n %s ([y]/n)' % python_input.exit_message), @@ -436,7 +437,7 @@ def get_text_fragments(app): (style, ' \n'), ] - visible = ~IsDone() & Condition(lambda app: python_input.show_exit_confirmation) + visible = ~IsDone() & Condition(lambda: python_input.show_exit_confirmation) return ConditionalContainer( content=Window(FormattedTextControl(get_text_fragments), style=style), # , has_focus=visible)), @@ -447,10 +448,10 @@ def meta_enter_message(python_input): """ Create the `Layout` for the 'Meta+Enter` message. """ - def get_text_fragments(app): + def get_text_fragments(): return [('class:accept-message', ' [Meta+Enter] Execute ')] - def extra_condition(app): + def extra_condition(): " Only show when... " b = python_input.default_buffer @@ -480,7 +481,7 @@ def create_layout(python_input, search_toolbar = SearchToolbar(python_input.search_buffer) def create_python_input_window(): - def menu_position(app): + def menu_position(): """ When there is no autocompletion menu to be shown, and we have a signature, set the pop-up position at `bracket_start`. @@ -508,7 +509,7 @@ def menu_position(app): ConditionalProcessor( processor=HighlightMatchingBracketProcessor(chars='[](){}'), filter=HasFocus(DEFAULT_BUFFER) & ~IsDone() & - Condition(lambda app: python_input.highlight_matching_parenthesis)), + Condition(lambda: python_input.highlight_matching_parenthesis)), ConditionalProcessor( processor=AppendAutoSuggestion(), filter=~IsDone()) @@ -523,10 +524,10 @@ def menu_position(app): # cursor is never below the "Press [Meta+Enter]" message which is a float. scroll_offsets=ScrollOffsets(bottom=1, left=4, right=4), # As long as we're editing, prefer a minimal height of 6. - get_height=(lambda app: ( - None if app.is_done or python_input.show_exit_confirmation + height=(lambda: ( + None if get_app().is_done or python_input.show_exit_confirmation else input_buffer_height)), - wrap_lines=Condition(lambda app: python_input.wrap_lines), + wrap_lines=Condition(lambda: python_input.wrap_lines), ) root_container = HSplit([ @@ -561,7 +562,7 @@ def menu_position(app): ]), ArgToolbar(), search_toolbar, - SystemToolbar(python_input.loop), + SystemToolbar(), ValidationToolbar(), CompletionsToolbar(extra_filter=show_completions_toolbar(python_input)), diff --git a/ptpython/prompt_style.py b/ptpython/prompt_style.py index beb02855..ab1351ac 100644 --- a/ptpython/prompt_style.py +++ b/ptpython/prompt_style.py @@ -14,12 +14,12 @@ class PromptStyle(with_metaclass(ABCMeta, object)): Base class for all prompts. """ @abstractmethod - def in_tokens(self, cli): + def in_tokens(self): " Return the input tokens. " return [] @abstractmethod - def in2_tokens(self, cli, width): + def in2_tokens(self, width): """ Tokens for every following input line. @@ -29,7 +29,7 @@ def in2_tokens(self, cli, width): return [] @abstractmethod - def out_tokens(self, cli): + def out_tokens(self): " Return the output tokens. " return [] @@ -41,19 +41,19 @@ class IPythonPrompt(PromptStyle): def __init__(self, python_input): self.python_input = python_input - def in_tokens(self, cli): + def in_tokens(self): return [ ('class:in', 'In ['), ('class:in.number', '%s' % self.python_input.current_statement_index), ('class:in', ']: '), ] - def in2_tokens(self, cli, width): + def in2_tokens(self, width): return [ ('class:in', '...: '.rjust(width)), ] - def out_tokens(self, cli): + def out_tokens(self): return [ ('class:out', 'Out['), ('class:out.number', '%s' % self.python_input.current_statement_index), @@ -66,11 +66,11 @@ class ClassicPrompt(PromptStyle): """ The classic Python prompt. """ - def in_tokens(self, cli): + def in_tokens(self): return [('class:prompt', '>>> ')] - def in2_tokens(self, cli, width): + def in2_tokens(self, width): return [('class:prompt.dots', '...')] - def out_tokens(self, cli): + def out_tokens(self): return [] diff --git a/ptpython/python_input.py b/ptpython/python_input.py index d22e47c5..6d5e96aa 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -4,16 +4,17 @@ """ from __future__ import unicode_literals -from prompt_toolkit.application import Application -from prompt_toolkit.auto_suggest import AutoSuggestFromHistory, ConditionalAutoSuggest +from prompt_toolkit.application import Application, get_app +from prompt_toolkit.auto_suggest import AutoSuggestFromHistory, ConditionalAutoSuggest, ThreadedAutoSuggest from prompt_toolkit.buffer import Buffer +from prompt_toolkit.completion import ThreadedCompleter from prompt_toolkit.document import Document from prompt_toolkit.enums import DEFAULT_BUFFER, EditingMode +from prompt_toolkit.eventloop.defaults import get_event_loop from prompt_toolkit.filters import Condition from prompt_toolkit.history import FileHistory, InMemoryHistory from prompt_toolkit.input.defaults import create_input from prompt_toolkit.key_binding import merge_key_bindings, ConditionalKeyBindings, KeyBindings -from prompt_toolkit.key_binding.defaults import load_key_bindings from prompt_toolkit.key_binding.vi_state import InputMode from prompt_toolkit.layout.lexers import PygmentsLexer, DynamicLexer, SimpleLexer from prompt_toolkit.output.defaults import create_output @@ -190,8 +191,8 @@ def __init__(self, # The buffers. self.default_buffer = self._create_buffer() - self.search_buffer = Buffer(loop=loop) - self.docstring_buffer = Buffer(loop=loop, read_only=True) + self.search_buffer = Buffer() + self.docstring_buffer = Buffer(read_only=True) # Tokens to be shown at the prompt. self.prompt_style = 'classic' # The currently active style. @@ -201,11 +202,11 @@ def __init__(self, 'classic': ClassicPrompt(), } - self.get_input_prompt_tokens = lambda app: \ - self.all_prompt_styles[self.prompt_style].in_tokens(app) + self.get_input_prompt_tokens = lambda: \ + self.all_prompt_styles[self.prompt_style].in_tokens() - self.get_output_prompt_tokens = lambda app: \ - self.all_prompt_styles[self.prompt_style].out_tokens(app) + self.get_output_prompt_tokens = lambda: \ + self.all_prompt_styles[self.prompt_style].out_tokens() #: Load styles. self.code_styles = get_all_code_styles() @@ -238,7 +239,8 @@ def __init__(self, if vi_mode: self.app.editing_mode = EditingMode.VI - def _accept_handler(self, app, buff): + def _accept_handler(self, buff): + app = get_app() app.set_return_value(buff.text) app.pre_run_callables.append(buff.reset) @@ -485,7 +487,6 @@ def _create_application(self): Create an `Application` instance. """ return Application( - loop=self.loop, input=self.input, output=self.output, layout=create_layout( @@ -497,28 +498,28 @@ def _create_application(self): extra_body=self._extra_layout_body, extra_toolbars=self._extra_toolbars), key_bindings=merge_key_bindings([ - ConditionalKeyBindings( - key_bindings=load_key_bindings( - enable_abort_and_exit_bindings=True, - enable_search=True, - enable_open_in_editor=Condition(lambda app: self.enable_open_in_editor), - enable_system_bindings=Condition(lambda app: self.enable_system_bindings), - enable_auto_suggest_bindings=Condition(lambda app: self.enable_auto_suggest)), - +# ConditionalKeyBindings( +# key_bindings=load_key_bindings( +# enable_abort_and_exit_bindings=True, +# enable_search=True, +# enable_open_in_editor=Condition(lambda: self.enable_open_in_editor), +# enable_system_bindings=Condition(lambda: self.enable_system_bindings), +# enable_auto_suggest_bindings=Condition(lambda: self.enable_auto_suggest)), +#), # Disable all default key bindings when the sidebar or the exit confirmation # are shown. - filter=Condition(lambda app: not (self.show_sidebar or self.show_exit_confirmation)) - ), +# filter=Condition(lambda: not (self.show_sidebar or self.show_exit_confirmation)) +# ), load_python_bindings(self), load_sidebar_bindings(self), load_confirm_exit_bindings(self), # Extra key bindings should not be active when the sidebar is visible. ConditionalKeyBindings( self.extra_key_bindings, - Condition(lambda app: not self.show_sidebar)) + Condition(lambda: not self.show_sidebar)) ]), - paste_mode=Condition(lambda app: self.paste_mode), - mouse_support=Condition(lambda app: self.enable_mouse_support), + paste_mode=Condition(lambda: self.paste_mode), + mouse_support=Condition(lambda: self.enable_mouse_support), style=DynamicStyle(lambda: self._current_style), get_title=lambda: self.terminal_title, reverse_vi_search_direction=True) @@ -528,18 +529,18 @@ def _create_buffer(self): Create the `Buffer` for the Python input. """ python_buffer = Buffer( - loop=self.loop, name=DEFAULT_BUFFER, + name=DEFAULT_BUFFER, complete_while_typing=Condition(lambda: self.complete_while_typing), enable_history_search=Condition(lambda: self.enable_history_search), tempfile_suffix='.py', history=self.history, - completer=self._completer, + completer=ThreadedCompleter(self._completer), validator=ConditionalValidator( self._validator, Condition(lambda: self.enable_input_validation)), auto_suggest=ConditionalAutoSuggest( - AutoSuggestFromHistory(), - Condition(lambda app: self.enable_auto_suggest)), + ThreadedAutoSuggest(AutoSuggestFromHistory()), + Condition(lambda: self.enable_auto_suggest)), accept_handler=self._accept_handler, on_text_changed=self._on_input_timeout) @@ -630,15 +631,16 @@ def run(): else: self._on_input_timeout(buff) - app.loop.run_in_executor(run) + get_event_loop().run_in_executor(run) - def on_reset(self, app): + def on_reset(self): self.signatures = [] - def enter_history(self, app): + def enter_history(self): """ Display the history. """ + app = get_app() app.vi_state.input_mode = InputMode.NAVIGATION def done(f): @@ -650,5 +652,5 @@ def done(f): history = History(self, self.default_buffer.document) - future = app.run_sub_application(history.app) + future = app.run_in_terminal_async(history.app.run_async) future.add_done_callback(done) diff --git a/ptpython/repl.py b/ptpython/repl.py index af6bfcf2..eb63d166 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -112,7 +112,7 @@ def compile_with_flags(code, mode): locals['_'] = locals['_%i' % self.current_statement_index] = result if result is not None: - out_tokens = self.get_output_prompt_tokens(self.app) + out_tokens = self.get_output_prompt_tokens() try: result_str = '%r\n' % (result, ) diff --git a/ptpython/utils.py b/ptpython/utils.py index d53b7422..2cdf2491 100644 --- a/ptpython/utils.py +++ b/ptpython/utils.py @@ -119,9 +119,9 @@ def if_mousedown(handler): (When applied to a token list. Scroll events will bubble up and are handled by the Window.) """ - def handle_if_mouse_down(app, mouse_event): + def handle_if_mouse_down(mouse_event): if mouse_event.event_type == MouseEventType.MOUSE_DOWN: - return handler(app, mouse_event) + return handler(mouse_event) else: return NotImplemented return handle_if_mouse_down From bc50874dcfbe465965b2c6c742d24c130318dced Mon Sep 17 00:00:00 2001 From: Peter Holloway Date: Mon, 26 Jun 2017 15:03:26 +0100 Subject: [PATCH 188/470] Use __main__.__dict__ for locals when running from command line Fixes issues #148 and #201 --- ptpython/entry_points/run_ptpython.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ptpython/entry_points/run_ptpython.py b/ptpython/entry_points/run_ptpython.py index 67e9c072..fd49afea 100644 --- a/ptpython/entry_points/run_ptpython.py +++ b/ptpython/entry_points/run_ptpython.py @@ -64,9 +64,11 @@ def configure(repl): if os.path.exists(path): run_config(repl, path) + import __main__ embed(vi_mode=vi_mode, history_filename=os.path.join(config_dir, 'history'), configure=configure, + locals=__main__.__dict__, startup_paths=startup_paths, title='Python REPL (ptpython)') From 02c295177a05d7385891c41083a43a125d494c5b Mon Sep 17 00:00:00 2001 From: Arthur Noel Date: Tue, 9 May 2017 20:50:20 +0100 Subject: [PATCH 189/470] setup.py: extras_require not extra_require --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b0b89a64..065512a3 100644 --- a/setup.py +++ b/setup.py @@ -35,7 +35,7 @@ 'ptipython%s.%s = ptpython.entry_points.run_ptipython:run' % sys.version_info[:2], ] }, - extra_require={ + extras_require={ 'ptipython': ['ipython'] # For ptipython, we need to have IPython } ) From 1b72e1f6c56b95dcb8e3e5553f97663c9364376c Mon Sep 17 00:00:00 2001 From: Orivej Desh Date: Wed, 30 Mar 2016 19:06:32 +0000 Subject: [PATCH 190/470] Support user_ns arg for compatibility with start_ipython. --- ptpython/entry_points/run_ptipython.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ptpython/entry_points/run_ptipython.py b/ptpython/entry_points/run_ptipython.py index 1def5fc2..506f4b8f 100644 --- a/ptpython/entry_points/run_ptipython.py +++ b/ptpython/entry_points/run_ptipython.py @@ -20,7 +20,7 @@ import sys -def run(): +def run(user_ns=None): a = docopt.docopt(__doc__) vi_mode = bool(a['--vi']) @@ -55,7 +55,8 @@ def run(): # Create an empty namespace for this interactive shell. (If we don't do # that, all the variables from this function will become available in # the IPython shell.) - user_ns = {} + if user_ns is None: + user_ns = {} # Startup path startup_paths = [] From b268acba22a17adb092ba628207396ebf36fddb8 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 6 Jul 2017 18:18:09 +0200 Subject: [PATCH 191/470] Use __main__.__dict__ for globals as well as locals when running ptpython Thanks: Peter Holloway (@facingBackwards) --- ptpython/entry_points/run_ptpython.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ptpython/entry_points/run_ptpython.py b/ptpython/entry_points/run_ptpython.py index fd49afea..333fd1df 100644 --- a/ptpython/entry_points/run_ptpython.py +++ b/ptpython/entry_points/run_ptpython.py @@ -69,6 +69,7 @@ def configure(repl): history_filename=os.path.join(config_dir, 'history'), configure=configure, locals=__main__.__dict__, + globals=__main__.__dict__, startup_paths=startup_paths, title='Python REPL (ptpython)') From d050124c04c7740db0d12584444994e9a48c7936 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 6 Jul 2017 18:28:00 +0200 Subject: [PATCH 192/470] Prompt_toolkit 2.0 changes. --- ptpython/filters.py | 1 + ptpython/python_input.py | 1 - ptpython/repl.py | 9 ++++++++- ptpython/style.py | 27 ++++++++++++--------------- 4 files changed, 21 insertions(+), 17 deletions(-) diff --git a/ptpython/filters.py b/ptpython/filters.py index ddf7cf5c..8ddc3c6a 100644 --- a/ptpython/filters.py +++ b/ptpython/filters.py @@ -5,6 +5,7 @@ __all__ = ( 'HasSignature', 'ShowSidebar', + 'ShowSignature', 'ShowDocstring', ) diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 6d5e96aa..9c1ca9ca 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -521,7 +521,6 @@ def _create_application(self): paste_mode=Condition(lambda: self.paste_mode), mouse_support=Condition(lambda: self.enable_mouse_support), style=DynamicStyle(lambda: self._current_style), - get_title=lambda: self.terminal_title, reverse_vi_search_direction=True) def _create_buffer(self): diff --git a/ptpython/repl.py b/ptpython/repl.py index eb63d166..5803abf3 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -16,6 +16,7 @@ from prompt_toolkit.layout.utils import fragment_list_width from prompt_toolkit.styles import token_list_to_formatted_text from prompt_toolkit.utils import DummyContext +from prompt_toolkit.shortcuts import set_title, clear_title from .python_input import PythonInput from .eventloop import create_event_loop @@ -53,6 +54,9 @@ def _load_start_paths(self): output.write('WARNING | File not found: {}\n\n'.format(path)) def run(self): + if self.terminal_title: + set_title(self.terminal_title) + while True: # Run the UI. try: @@ -65,6 +69,9 @@ def run(self): else: self._process_text(text) + if self.terminal_title: + clear_title() + def _process_text(self, text): line = self.default_buffer.text @@ -132,7 +139,7 @@ def compile_with_flags(code, mode): out_tokens.extend(_lex_python_result(result_str)) else: out_tokens.append(('', result_str)) - self.app.print_formatted_text( + self.app.print_text( token_list_to_formatted_text(out_tokens)) # If not a valid `eval` expression, run using `exec` instead. except SyntaxError: diff --git a/ptpython/style.py b/ptpython/style.py index 3b9bbfe9..f93a4e11 100644 --- a/ptpython/style.py +++ b/ptpython/style.py @@ -2,7 +2,7 @@ from prompt_toolkit.styles import Style, merge_styles, default_style from prompt_toolkit.styles.pygments import style_from_pygments -#from prompt_toolkit.utils import is_windows, is_conemu_ansi +from prompt_toolkit.utils import is_windows, is_conemu_ansi, is_windows_vt100_supported from pygments.styles import get_style_by_name, get_all_styles __all__ = ( @@ -110,9 +110,6 @@ def generate_style(python_style, ui_style): 'status-toolbar.pastemodeon': 'bg:#aa4444 #ffffff', 'status-toolbar.pythonversion': 'bg:#222222 #ffffff bold', - # When Control-C has been pressed. Grayed. -# 'aborted': '#888888 bg:default important', - # The options sidebar. 'sidebar': 'bg:#bbbbbb #000000', 'sidebar.title': 'bg:#668866 #ffffff', @@ -145,17 +142,17 @@ def generate_style(python_style, ui_style): } -# # Some changes to get a bit more contrast on Windows consoles. -# # (They only support 16 colors.) -# if is_windows() and not is_conemu_ansi(): -# default_ui_style.update({ -# Token.Sidebar.Title: 'bg:#00ff00 #ffffff', -# Token.ExitConfirmation: 'bg:#ff4444 #ffffff', -# Token.Toolbar.Validation: 'bg:#ff4444 #ffffff', -# -# Token.Menu.Completions.Completion: 'bg:#ffffff #000000', -# Token.Menu.Completions.Completion.Current: 'bg:#aaaaaa #000000', -# }) +# Some changes to get a bit more contrast on Windows consoles. +# (They only support 16 colors.) +if is_windows() and not is_conemu_ansi() and not is_windows_vt100_supported(): + default_ui_style.update({ + 'sidebar.title': 'bg:#00ff00 #ffffff', + 'exitconfirmation': 'bg:#ff4444 #ffffff', + 'toolbar.validation': 'bg:#ff4444 #ffffff', + + 'menu.completions.completion': 'bg:#ffffff #000000', + 'menu.completions.completion.current': 'bg:#aaaaaa #000000', + }) blue_ui_style = {} From 74081aa8a62f9df0d526840910b9bc58240eaeb1 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sun, 10 Dec 2017 21:58:56 +0100 Subject: [PATCH 193/470] Use print_formatted_text. --- ptpython/repl.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/ptpython/repl.py b/ptpython/repl.py index 5803abf3..de1611fd 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -14,9 +14,10 @@ from prompt_toolkit.document import Document from prompt_toolkit.eventloop.defaults import create_asyncio_event_loop from prompt_toolkit.layout.utils import fragment_list_width -from prompt_toolkit.styles import token_list_to_formatted_text from prompt_toolkit.utils import DummyContext from prompt_toolkit.shortcuts import set_title, clear_title +from prompt_toolkit.shortcuts import print as print_formatted_text +from prompt_toolkit.formatted_text import PygmentsTokens from .python_input import PythonInput from .eventloop import create_event_loop @@ -139,8 +140,7 @@ def compile_with_flags(code, mode): out_tokens.extend(_lex_python_result(result_str)) else: out_tokens.append(('', result_str)) - self.app.print_text( - token_list_to_formatted_text(out_tokens)) + print_formatted_text(PygmentsTokens(out_tokens)) # If not a valid `eval` expression, run using `exec` instead. except SyntaxError: code = compile_with_flags(line, 'exec') @@ -180,8 +180,7 @@ def _handle_exception(self, e): tokens = _lex_python_traceback(tb) else: tokens = [('', tb)] - self.app.print_formatted_text( - token_list_to_formatted_text(tokens)) + print_formatted_text(PygmentsTokens(tokens)) output.write('%s\n' % e) output.flush() From 9754d8953e35fe477fc33ea908fc5cc581d08965 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sun, 10 Dec 2017 22:01:56 +0100 Subject: [PATCH 194/470] Don't use extra_filter in CompletionsToolbar/Menus. --- ptpython/layout.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/ptpython/layout.py b/ptpython/layout.py index 7674b97d..0cad0ccc 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -540,15 +540,17 @@ def menu_position(): floats=[ Float(xcursor=True, ycursor=True, - content=CompletionsMenu( - scroll_offset=Integer.from_callable( - lambda: python_input.completion_menu_scroll_offset), - max_height=12, - extra_filter=show_completions_menu(python_input))), + content=ConditionalContainer( + content=CompletionsMenu( + scroll_offset=Integer.from_callable( + lambda: python_input.completion_menu_scroll_offset), + max_height=12), + filter=show_completions_menu(python_input))), Float(xcursor=True, ycursor=True, - content=MultiColumnCompletionsMenu( - extra_filter=show_multi_column_completions_menu(python_input))), + content=ConditionalContainer( + content=MultiColumnCompletionsMenu(), + filter=show_multi_column_completions_menu(python_input))), Float(xcursor=True, ycursor=True, content=signature_toolbar(python_input)), @@ -564,7 +566,9 @@ def menu_position(): search_toolbar, SystemToolbar(), ValidationToolbar(), - CompletionsToolbar(extra_filter=show_completions_toolbar(python_input)), + ConditionalContainer( + content=CompletionsToolbar(), + filter=show_completions_toolbar(python_input)), # Docstring region. ConditionalContainer( From b8449868a44282464a93dcd7575c0022347ad1a0 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sun, 24 Dec 2017 17:22:50 +0100 Subject: [PATCH 195/470] Compatibility fix for latest prompt_toolkit. --- ptpython/repl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ptpython/repl.py b/ptpython/repl.py index de1611fd..b65ff3c9 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -16,7 +16,7 @@ from prompt_toolkit.layout.utils import fragment_list_width from prompt_toolkit.utils import DummyContext from prompt_toolkit.shortcuts import set_title, clear_title -from prompt_toolkit.shortcuts import print as print_formatted_text +from prompt_toolkit.shortcuts import print_formatted_text from prompt_toolkit.formatted_text import PygmentsTokens from .python_input import PythonInput From abc6237826dd6376078493e958b31cb83680a44d Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sat, 30 Dec 2017 18:14:02 +0100 Subject: [PATCH 196/470] Compatibility with the latest prompt_toolkit. --- examples/python-input.py | 11 +++-------- ptpython/eventloop.py | 10 ++-------- ptpython/ipython.py | 5 +---- ptpython/python_input.py | 3 +-- ptpython/repl.py | 15 ++++++--------- 5 files changed, 13 insertions(+), 31 deletions(-) diff --git a/examples/python-input.py b/examples/python-input.py index 53aaa2dd..bcfd6fca 100755 --- a/examples/python-input.py +++ b/examples/python-input.py @@ -3,19 +3,14 @@ """ from __future__ import unicode_literals -from prompt_toolkit.eventloop.defaults import create_event_loop from ptpython.python_input import PythonInput def main(): - loop = create_event_loop() - try: - prompt = PythonInput(loop=loop) + prompt = PythonInput() - code_obj = prompt.app.run() - print('You said: ' + code_obj.text) - finally: - loop.close() + text = prompt.app.run() + print('You said: ' + text) if __name__ == '__main__': diff --git a/ptpython/eventloop.py b/ptpython/eventloop.py index 4794988d..600b5842 100644 --- a/ptpython/eventloop.py +++ b/ptpython/eventloop.py @@ -13,7 +13,7 @@ import time __all__ = ( - 'create_eventloop', + 'inputhook', ) @@ -66,13 +66,7 @@ def wait_using_polling(): wait_using_polling() -def _inputhook(inputhook_context): +def inputhook(inputhook_context): # Only call the real input hook when the 'Tkinter' library was loaded. if 'Tkinter' in sys.modules or 'tkinter' in sys.modules: _inputhook_tk(inputhook_context) - - -def create_event_loop(): - loop = _create_event_loop(inputhook=_inputhook) - set_event_loop(loop) - return loop diff --git a/ptpython/ipython.py b/ptpython/ipython.py index c541b371..94e87c32 100644 --- a/ptpython/ipython.py +++ b/ptpython/ipython.py @@ -20,7 +20,6 @@ from prompt_toolkit.layout.lexers import PygmentsLexer, SimpleLexer from .python_input import PythonInput, PythonValidator, PythonCompleter -from .eventloop import create_eventloop from .style import default_ui_style from IPython.terminal.embed import InteractiveShellEmbed as _InteractiveShellEmbed @@ -229,7 +228,6 @@ def __init__(self, *a, **kw): def get_globals(): return self.user_ns - self._eventloop = create_eventloop() ipython_input = IPythonInput( self, get_globals=get_globals, vi_mode=vi_mode, @@ -243,8 +241,7 @@ def get_globals(): ipython_input.prompt_style = 'ipython' # Don't take from config. self._cli = CommandLineInterface( - application=ipython_input.create_application(), - eventloop=self._eventloop) + application=ipython_input.create_application()) def prompt_for_code(self): # IPython 5.0 calls `prompt_for_code` instead of `raw_input`. diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 9c1ca9ca..135c7eb7 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -122,7 +122,7 @@ class PythonInput(object): python_code = python_input.run() """ def __init__(self, - loop, get_globals=None, get_locals=None, history_filename=None, + get_globals=None, get_locals=None, history_filename=None, vi_mode=False, input=None, @@ -136,7 +136,6 @@ def __init__(self, _extra_layout_body=None, _extra_toolbars=None, _input_buffer_height=None): - self.loop = loop self.get_globals = get_globals or (lambda: {}) self.get_locals = get_locals or self.get_globals diff --git a/ptpython/repl.py b/ptpython/repl.py index b65ff3c9..0afb3651 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -12,7 +12,7 @@ from pygments.lexers import PythonTracebackLexer, PythonLexer from prompt_toolkit.document import Document -from prompt_toolkit.eventloop.defaults import create_asyncio_event_loop +from prompt_toolkit.eventloop.defaults import use_asyncio_event_loop from prompt_toolkit.layout.utils import fragment_list_width from prompt_toolkit.utils import DummyContext from prompt_toolkit.shortcuts import set_title, clear_title @@ -20,7 +20,7 @@ from prompt_toolkit.formatted_text import PygmentsTokens from .python_input import PythonInput -from .eventloop import create_event_loop +from .eventloop import inputhook import os import six @@ -61,7 +61,7 @@ def run(self): while True: # Run the UI. try: - text = self.app.run() + text = self.app.run(inputhook=inputhook) except EOFError: return except KeyboardInterrupt: @@ -292,14 +292,11 @@ def get_locals(): # Create eventloop. if return_asyncio_coroutine: - loop = create_asyncio_event_loop() - else: - loop = create_event_loop() + use_asyncio_event_loop() # Create REPL. - repl = PythonRepl(loop=loop, get_globals=get_globals, get_locals=get_locals, vi_mode=vi_mode, - history_filename=history_filename, - startup_paths=startup_paths) + repl = PythonRepl(get_globals=get_globals, get_locals=get_locals, vi_mode=vi_mode, + history_filename=history_filename, startup_paths=startup_paths) if title: repl.terminal_title = title From 3155fc1979c6637c94e78139f4bc40c79de39b1c Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sun, 7 Jan 2018 21:18:08 +0100 Subject: [PATCH 197/470] Upgrade prompt_toolkit version number in setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 065512a3..ad46f68b 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ install_requires = [ 'docopt', 'jedi>=0.9.0', - 'prompt_toolkit>=1.0.0,<2.0.0', + 'prompt_toolkit>=2.0.0,<2.1.0', 'pygments', ], entry_points={ From 323d58f779d695dc8d2560b863298051ead1ee74 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sun, 7 Jan 2018 22:01:07 +0100 Subject: [PATCH 198/470] Don't include prompt_toolkit default style. --- ptpython/style.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ptpython/style.py b/ptpython/style.py index f93a4e11..44ddb6ba 100644 --- a/ptpython/style.py +++ b/ptpython/style.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals -from prompt_toolkit.styles import Style, merge_styles, default_style +from prompt_toolkit.styles import Style, merge_styles from prompt_toolkit.styles.pygments import style_from_pygments from prompt_toolkit.utils import is_windows, is_conemu_ansi, is_windows_vt100_supported from pygments.styles import get_style_by_name, get_all_styles @@ -37,7 +37,6 @@ def generate_style(python_style, ui_style): containing style rules. """ return merge_styles([ - default_style(), python_style, ui_style ]) From 783236a5cd23da2da0facdd92e084d7aaffc6d82 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Mon, 8 Jan 2018 20:37:00 +0100 Subject: [PATCH 199/470] Small bugfix in print_formatted_text call. --- ptpython/repl.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ptpython/repl.py b/ptpython/repl.py index 0afb3651..b981bd1e 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -10,6 +10,7 @@ from __future__ import unicode_literals from pygments.lexers import PythonTracebackLexer, PythonLexer +from pygments.token import Token from prompt_toolkit.document import Document from prompt_toolkit.eventloop.defaults import use_asyncio_event_loop @@ -177,9 +178,9 @@ def _handle_exception(self, e): # (We use the default style. Most other styles result # in unreadable colors for the traceback.) if self.enable_syntax_highlighting: - tokens = _lex_python_traceback(tb) + tokens = list(_lex_python_traceback(tb)) else: - tokens = [('', tb)] + tokens = [(Token, tb)] print_formatted_text(PygmentsTokens(tokens)) output.write('%s\n' % e) From 4901105efdcdbc285ff89d4b3d2622245c53eb60 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 9 Jan 2018 21:48:09 +0100 Subject: [PATCH 200/470] Don't include default_pygments_style. --- ptpython/python_input.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 135c7eb7..80ae4e96 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -520,6 +520,7 @@ def _create_application(self): paste_mode=Condition(lambda: self.paste_mode), mouse_support=Condition(lambda: self.enable_mouse_support), style=DynamicStyle(lambda: self._current_style), + include_default_pygments_style=False, reverse_vi_search_direction=True) def _create_buffer(self): From bf12d0949d8a184fd42233730118c2dabdae27e7 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sat, 27 Jan 2018 15:40:26 +0100 Subject: [PATCH 201/470] Fixed history browser. --- ptpython/history_browser.py | 2 +- ptpython/python_input.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/ptpython/history_browser.py b/ptpython/history_browser.py index 5149359a..1b35015e 100644 --- a/ptpython/history_browser.py +++ b/ptpython/history_browser.py @@ -506,7 +506,7 @@ def _(event): @handle('enter', filter=main_buffer_focussed) def _(event): " Accept input. " - event.app.set_return_value(history.default_buffer.document) + event.app.set_return_value(history.default_buffer.text) enable_system_bindings = Condition(lambda: python_input.enable_system_bindings) diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 80ae4e96..1adc2e61 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -5,6 +5,7 @@ from __future__ import unicode_literals from prompt_toolkit.application import Application, get_app +from prompt_toolkit.application.run_in_terminal import run_coroutine_in_terminal from prompt_toolkit.auto_suggest import AutoSuggestFromHistory, ConditionalAutoSuggest, ThreadedAutoSuggest from prompt_toolkit.buffer import Buffer from prompt_toolkit.completion import ThreadedCompleter @@ -644,12 +645,13 @@ def enter_history(self): def done(f): result = f.result() + assert isinstance(result, str), 'got %r' % (result, ) if result is not None: - self.default_buffer.document = result + self.default_buffer.text = result app.vi_state.input_mode = InputMode.INSERT history = History(self, self.default_buffer.document) - future = app.run_in_terminal_async(history.app.run_async) + future = run_coroutine_in_terminal(history.app.run_async) future.add_done_callback(done) From d00f8d454fe2b94c499869e6677cce7a22601fc4 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sun, 28 Jan 2018 18:22:43 +0100 Subject: [PATCH 202/470] Prompt_toolkit 2.0 changes. --- ptpython/completer.py | 7 +++- ptpython/eventloop.py | 2 - ptpython/history_browser.py | 50 ++++++++++-------------- ptpython/ipython.py | 10 ++--- ptpython/key_bindings.py | 58 ++++++++++++++-------------- ptpython/layout.py | 76 +++++++++++++++++++++---------------- ptpython/prompt_style.py | 20 +++++----- ptpython/python_input.py | 68 ++++++++++++++++++--------------- ptpython/repl.py | 26 +++++++++---- ptpython/style.py | 9 ++++- 10 files changed, 177 insertions(+), 149 deletions(-) diff --git a/ptpython/completer.py b/ptpython/completer.py index 90da6451..0d547de0 100644 --- a/ptpython/completer.py +++ b/ptpython/completer.py @@ -1,7 +1,6 @@ from __future__ import unicode_literals -from prompt_toolkit.completion import Completer, Completion -from prompt_toolkit.contrib.completers import PathCompleter +from prompt_toolkit.completion import Completer, Completion, PathCompleter from prompt_toolkit.contrib.regular_languages.compiler import compile as compile_grammar from prompt_toolkit.contrib.regular_languages.completion import GrammarCompleter @@ -151,6 +150,10 @@ def get_completions(self, document, complete_event): # In jedi.parser.__init__.py: 227, in remove_last_newline, # the assertion "newline.value.endswith('\n')" can fail. pass + except SystemError: + # In jedi.api.helpers.py: 144, in get_stack_at_position + # raise SystemError("This really shouldn't happen. There's a bug in Jedi.") + pass else: for c in completions: yield Completion(c.name_with_symbols, len(c.complete) - len(c.name_with_symbols), diff --git a/ptpython/eventloop.py b/ptpython/eventloop.py index 600b5842..43fe0549 100644 --- a/ptpython/eventloop.py +++ b/ptpython/eventloop.py @@ -7,8 +7,6 @@ in readline. ``prompt-toolkit`` doesn't understand that input hook, but this will fix it for Tk.) """ -from prompt_toolkit.eventloop.defaults import create_event_loop as _create_event_loop -from prompt_toolkit.eventloop.defaults import set_event_loop import sys import time diff --git a/ptpython/history_browser.py b/ptpython/history_browser.py index 1b35015e..18ec3f63 100644 --- a/ptpython/history_browser.py +++ b/ptpython/history_browser.py @@ -12,17 +12,17 @@ from prompt_toolkit.document import Document from prompt_toolkit.enums import DEFAULT_BUFFER from prompt_toolkit.filters import Condition, has_focus +from prompt_toolkit.formatted_text.utils import fragment_list_to_text from prompt_toolkit.key_binding import KeyBindings -from prompt_toolkit.layout.containers import HSplit, VSplit, Window, FloatContainer, Float, ConditionalContainer, Container, ScrollOffsets, Align +from prompt_toolkit.layout.containers import HSplit, VSplit, Window, FloatContainer, Float, ConditionalContainer, Container, ScrollOffsets, WindowAlign from prompt_toolkit.layout.controls import BufferControl, FormattedTextControl from prompt_toolkit.layout.dimension import Dimension as D from prompt_toolkit.layout.layout import Layout -from prompt_toolkit.layout.lexers import PygmentsLexer from prompt_toolkit.layout.margins import Margin, ScrollbarMargin -from prompt_toolkit.layout.processors import Processor, Transformation, HighlightSearchProcessor, HighlightSelectionProcessor, merge_processors -from prompt_toolkit.layout.widgets.toolbars import ArgToolbar, SearchToolbar -from prompt_toolkit.layout.utils import fragment_list_to_text -from prompt_toolkit.layout.widgets import Frame +from prompt_toolkit.layout.processors import Processor, Transformation +from prompt_toolkit.lexers import PygmentsLexer +from prompt_toolkit.widgets import Frame +from prompt_toolkit.widgets.toolbars import ArgToolbar, SearchToolbar from pygments.lexers import RstLexer from .utils import if_mousedown @@ -110,34 +110,29 @@ class HistoryLayout(object): application. """ def __init__(self, history): - default_processors = [ - HighlightSearchProcessor(preview_search=True), - HighlightSelectionProcessor() - ] + search_toolbar = SearchToolbar() self.help_buffer_control = BufferControl( buffer=history.help_buffer, - lexer=PygmentsLexer(RstLexer), - input_processor=merge_processors(default_processors)) + lexer=PygmentsLexer(RstLexer)) help_window = _create_popup_window( title='History Help', body=Window( content=self.help_buffer_control, right_margins=[ScrollbarMargin(display_arrows=True)], - scroll_offsets=ScrollOffsets(top=2, bottom=2), - transparent=False)) + scroll_offsets=ScrollOffsets(top=2, bottom=2))) self.default_buffer_control = BufferControl( buffer=history.default_buffer, - input_processor=merge_processors( - default_processors + [GrayExistingText(history.history_mapping)]), + input_processors=[GrayExistingText(history.history_mapping)], lexer=PygmentsLexer(PythonLexer)) self.history_buffer_control = BufferControl( buffer=history.history_buffer, lexer=PygmentsLexer(PythonLexer), - input_processor=merge_processors(default_processors)) + search_buffer_control=search_toolbar.control, + preview_search=True) history_window = Window( content=self.history_buffer_control, @@ -149,7 +144,7 @@ def __init__(self, history): # Top title bar. Window( content=FormattedTextControl(_get_top_toolbar_fragments), - align=Align.CENTER, + align=WindowAlign.CENTER, style='class:status-toolbar'), FloatContainer( content=VSplit([ @@ -170,16 +165,12 @@ def __init__(self, history): # Help text as a float. Float(width=60, top=3, bottom=2, content=ConditionalContainer( - # XXXX XXX - # (We use InFocusStack, because it's possible to search - # through the help text as well, and at that point the search - # buffer has the focus.) - content=help_window, filter=has_focus(history.help_buffer))), # XXX + content=help_window, filter=has_focus(history.help_buffer))), ] ), # Bottom toolbars. ArgToolbar(), - # SearchToolbar(), # XXX + search_toolbar, Window( content=FormattedTextControl( partial(_get_bottom_toolbar_fragments, history=history)), @@ -338,15 +329,16 @@ def __init__(self, history, python_history, original_document): self.selected_lines = set() # Process history. + history_strings = python_history.get_strings() history_lines = [] - for entry_nr, entry in list(enumerate(python_history))[-HISTORY_COUNT:]: + for entry_nr, entry in list(enumerate(history_strings))[-HISTORY_COUNT:]: self.lines_starting_new_entries.add(len(history_lines)) for line in entry.splitlines(): history_lines.append(line) - if len(python_history) > HISTORY_COUNT: + if len(history_strings) > HISTORY_COUNT: history_lines[0] = '# *** History has been truncated to %s lines ***' % HISTORY_COUNT self.history_lines = history_lines @@ -501,12 +493,12 @@ def _(event): @handle('c-g', filter=main_buffer_focussed) def _(event): " Cancel and go back. " - event.app.set_return_value(None) + event.app.exit(result=None) @handle('enter', filter=main_buffer_focussed) def _(event): " Accept input. " - event.app.set_return_value(history.default_buffer.text) + event.app.exit(result=history.default_buffer.text) enable_system_bindings = Condition(lambda: python_input.enable_system_bindings) @@ -540,7 +532,7 @@ def __init__(self, python_input, original_document): document=document, on_cursor_position_changed=self._history_buffer_pos_changed, accept_handler=( - lambda buff: get_app().set_return_value(self.default_buffer.text)), + lambda buff: get_app().exit(result=self.default_buffer.text)), read_only=True) self.default_buffer = Buffer( diff --git a/ptpython/ipython.py b/ptpython/ipython.py index 94e87c32..a58d1fca 100644 --- a/ptpython/ipython.py +++ b/ptpython/ipython.py @@ -43,11 +43,11 @@ class IPythonPrompt(PromptStyle): def __init__(self, prompt_manager): self.prompt_manager = prompt_manager - def in_tokens(self): + def in_prompt(self): text = self.prompt_manager.render('in', color=False, just=False) return [('class:in', text)] - def in2_tokens(self, width): + def in2_prompt(self, width): text = self.prompt_manager.render('in2', color=False, just=False) return [('class:in', text.rjust(width))] @@ -65,13 +65,13 @@ class IPython5Prompt(PromptStyle): def __init__(self, prompts): self.prompts = prompts - def in_tokens(self): + def in_prompt(self): return self.prompts.in_prompt_tokens() - def in2_tokens(self, width): + def in2_prompt(self, width): return self.prompts.continuation_prompt_tokens() - def out_tokens(self): + def out_prompt(self): return [] diff --git a/ptpython/key_bindings.py b/ptpython/key_bindings.py index 58e4962b..f9b198c8 100644 --- a/ptpython/key_bindings.py +++ b/ptpython/key_bindings.py @@ -2,7 +2,7 @@ from prompt_toolkit.document import Document from prompt_toolkit.enums import DEFAULT_BUFFER -from prompt_toolkit.filters import HasSelection, HasFocus, Condition, ViInsertMode, EmacsInsertMode, EmacsMode +from prompt_toolkit.filters import has_selection, has_focus, Condition, vi_insert_mode, emacs_insert_mode, emacs_mode from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.keys import Keys from prompt_toolkit.application import get_app @@ -39,7 +39,6 @@ def load_python_bindings(python_input): sidebar_visible = Condition(lambda: python_input.show_sidebar) handle = bindings.add - has_selection = HasSelection() @handle('c-l') def _(event): @@ -88,9 +87,9 @@ def is_multiline(): return document_is_multiline_python(python_input.default_buffer.document) @handle('enter', filter= ~sidebar_visible & ~has_selection & - (ViInsertMode() | EmacsInsertMode()) & - HasFocus(DEFAULT_BUFFER) & ~is_multiline) - @handle(Keys.Escape, Keys.Enter, filter= ~sidebar_visible & EmacsMode()) + (vi_insert_mode | emacs_insert_mode) & + has_focus(DEFAULT_BUFFER) & ~is_multiline) + @handle(Keys.Escape, Keys.Enter, filter= ~sidebar_visible & emacs_mode) def _(event): """ Accept input (for single line input). @@ -107,8 +106,8 @@ def _(event): b.validate_and_handle() @handle('enter', filter= ~sidebar_visible & ~has_selection & - (ViInsertMode() | EmacsInsertMode()) & - HasFocus(DEFAULT_BUFFER) & is_multiline) + (vi_insert_mode | emacs_insert_mode) & + has_focus(DEFAULT_BUFFER) & is_multiline) def _(event): """ Behaviour of the Enter key. @@ -142,22 +141,23 @@ def at_the_end(b): else: auto_newline(b) - @handle('c-d', filter=~sidebar_visible & Condition(lambda: - # Only when the `confirm_exit` flag is set. - python_input.confirm_exit and - # And the current buffer is empty. - get_app().current_buffer == python_input.default_buffer and - not get_app().current_buffer.text)) + @handle('c-d', filter=~sidebar_visible & + has_focus(python_input.default_buffer) & + Condition(lambda: + # Only when the `confirm_exit` flag is set. + python_input.confirm_exit and + # And the current buffer is empty. + not get_app().current_buffer.text)) def _(event): """ Override Control-D exit, to ask for confirmation. """ python_input.show_exit_confirmation = True - @handle('c-c') + @handle('c-c', filter=has_focus(python_input.default_buffer)) def _(event): " Abort when Control-C has been pressed. " - event.app.abort() + event.app.exit(exception=KeyboardInterrupt, style='class:aborting') return bindings @@ -171,23 +171,23 @@ def load_sidebar_bindings(python_input): handle = bindings.add sidebar_visible = Condition(lambda: python_input.show_sidebar) - @handle(Keys.Up, filter=sidebar_visible) - @handle(Keys.ControlP, filter=sidebar_visible) + @handle('up', filter=sidebar_visible) + @handle('c-p', filter=sidebar_visible) @handle('k', filter=sidebar_visible) def _(event): " Go to previous option. " python_input.selected_option_index = ( (python_input.selected_option_index - 1) % python_input.option_count) - @handle(Keys.Down, filter=sidebar_visible) - @handle(Keys.ControlN, filter=sidebar_visible) + @handle('down', filter=sidebar_visible) + @handle('c-n', filter=sidebar_visible) @handle('j', filter=sidebar_visible) def _(event): " Go to next option. " python_input.selected_option_index = ( (python_input.selected_option_index + 1) % python_input.option_count) - @handle(Keys.Right, filter=sidebar_visible) + @handle('right', filter=sidebar_visible) @handle('l', filter=sidebar_visible) @handle(' ', filter=sidebar_visible) def _(event): @@ -195,18 +195,18 @@ def _(event): option = python_input.selected_option option.activate_next() - @handle(Keys.Left, filter=sidebar_visible) + @handle('left', filter=sidebar_visible) @handle('h', filter=sidebar_visible) def _(event): " Select previous value for current option. " option = python_input.selected_option option.activate_previous() - @handle(Keys.ControlC, filter=sidebar_visible) - @handle(Keys.ControlG, filter=sidebar_visible) - @handle(Keys.ControlD, filter=sidebar_visible) - @handle(Keys.Enter, filter=sidebar_visible) - @handle(Keys.Escape, filter=sidebar_visible) + @handle('c-c', filter=sidebar_visible) + @handle('c-d', filter=sidebar_visible) + @handle('c-d', filter=sidebar_visible) + @handle('enter', filter=sidebar_visible) + @handle('escape', filter=sidebar_visible) def _(event): " Hide sidebar. " python_input.show_sidebar = False @@ -225,13 +225,13 @@ def load_confirm_exit_bindings(python_input): @handle('y', filter=confirmation_visible) @handle('Y', filter=confirmation_visible) - @handle(Keys.Enter, filter=confirmation_visible) - @handle(Keys.ControlD, filter=confirmation_visible) + @handle('enter', filter=confirmation_visible) + @handle('c-d', filter=confirmation_visible) def _(event): """ Really quit. """ - event.app.exit() + event.app.exit(exception=EOFError, style='class:exiting') @handle(Keys.Any, filter=confirmation_visible) def _(event): diff --git a/ptpython/layout.py b/ptpython/layout.py index 0cad0ccc..b89ab21d 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -5,21 +5,19 @@ from prompt_toolkit.application import get_app from prompt_toolkit.enums import DEFAULT_BUFFER, SEARCH_BUFFER -from prompt_toolkit.filters import IsDone, HasCompletions, RendererHeightIsKnown, HasFocus, Condition +from prompt_toolkit.filters import is_done, has_completions, renderer_height_is_known, has_focus, Condition +from prompt_toolkit.formatted_text.utils import fragment_list_width from prompt_toolkit.key_binding.vi_state import InputMode from prompt_toolkit.layout.containers import Window, HSplit, VSplit, FloatContainer, Float, ConditionalContainer, ScrollOffsets from prompt_toolkit.layout.controls import BufferControl, FormattedTextControl from prompt_toolkit.layout.dimension import Dimension from prompt_toolkit.layout.layout import Layout -from prompt_toolkit.layout.lexers import SimpleLexer from prompt_toolkit.layout.margins import PromptMargin from prompt_toolkit.layout.menus import CompletionsMenu, MultiColumnCompletionsMenu -from prompt_toolkit.layout.processors import ConditionalProcessor, AppendAutoSuggestion, HighlightSearchProcessor, HighlightSelectionProcessor, HighlightMatchingBracketProcessor, Processor, Transformation -from prompt_toolkit.layout.processors import merge_processors -from prompt_toolkit.layout.widgets.toolbars import CompletionsToolbar, ArgToolbar, SearchToolbar, ValidationToolbar, SystemToolbar -from prompt_toolkit.layout.utils import fragment_list_width -from prompt_toolkit.reactive import Integer +from prompt_toolkit.layout.processors import ConditionalProcessor, AppendAutoSuggestion, HighlightIncrementalSearchProcessor, HighlightSelectionProcessor, HighlightMatchingBracketProcessor, Processor, Transformation +from prompt_toolkit.lexers import SimpleLexer from prompt_toolkit.selection import SelectionType +from prompt_toolkit.widgets.toolbars import CompletionsToolbar, ArgToolbar, SearchToolbar, ValidationToolbar, SystemToolbar from .filters import HasSignature, ShowSidebar, ShowSignature, ShowDocstring from .utils import if_mousedown @@ -193,7 +191,7 @@ def get_help_text(): style=token, height=Dimension(min=3)), filter=ShowSidebar(python_input) & - Condition(lambda: python_input.show_sidebar_help) & ~IsDone()) + Condition(lambda: python_input.show_sidebar_help) & ~is_done) def signature_toolbar(python_input): @@ -257,12 +255,12 @@ def get_text_fragments(): # Show only when there is a signature HasSignature(python_input) & # And there are no completions to be shown. (would cover signature pop-up.) - ~(HasCompletions() & (show_completions_menu(python_input) | + ~(has_completions & (show_completions_menu(python_input) | show_multi_column_completions_menu(python_input))) # Signature needs to be shown. & ShowSignature(python_input) & # Not done yet. - ~IsDone()) + ~is_done) class PythonPromptMargin(PromptMargin): @@ -277,13 +275,16 @@ def get_prompt_style(): return python_input.all_prompt_styles[python_input.prompt_style] def get_prompt(): - return get_prompt_style().in_tokens() + return get_prompt_style().in_prompt() - def get_continuation_prompt(width): - return get_prompt_style().in2_tokens(width) + def get_continuation(width, line_number, is_soft_wrap): + if python_input.show_line_numbers and not is_soft_wrap: + text = ('%i ' % (line_number + 1)).rjust(width) + return [('class:line-number', text)] + else: + return get_prompt_style().in2_prompt(width) - super(PythonPromptMargin, self).__init__(get_prompt, get_continuation_prompt, - show_numbers=Condition(lambda: python_input.show_line_numbers)) + super(PythonPromptMargin, self).__init__(get_prompt, get_continuation) def status_bar(python_input): @@ -338,7 +339,7 @@ def get_text_fragments(): return ConditionalContainer( content=Window(content=FormattedTextControl(get_text_fragments), style=TB), - filter=~IsDone() & RendererHeightIsKnown() & + filter=~is_done & renderer_height_is_known & Condition(lambda: python_input.show_status_bar and not python_input.show_exit_confirmation)) @@ -354,16 +355,22 @@ def toggle_vi_mode(mouse_event): python_input.vi_mode = not python_input.vi_mode token = 'class:status-toolbar' - input_mode_t = 'class:status-toolbar,input-mode' + input_mode_t = 'class:status-toolbar.input-mode' mode = app.vi_state.input_mode result = [] append = result.append - append((token + 'class:input-mode', '[F4] ', toggle_vi_mode)) + append((input_mode_t, '[F4] ', toggle_vi_mode)) # InputMode if python_input.vi_mode: + recording_register = app.vi_state.recording_register + if recording_register: + append((token, ' ')) + append((token + ' class:record', 'RECORD({})'.format(recording_register))) + append((token, ' - ')) + if bool(app.current_buffer.selection_state): if app.current_buffer.selection_state.type == SelectionType.LINES: append((input_mode_t, 'Vi (VISUAL LINE)', toggle_vi_mode)) @@ -383,6 +390,11 @@ def toggle_vi_mode(mouse_event): append((input_mode_t, 'Vi (REPLACE)', toggle_vi_mode)) append((token, ' ')) else: + if app.emacs_state.is_recording: + append((token, ' ')) + append((token + ' class:record', 'RECORD')) + append((token, ' - ')) + append((input_mode_t, 'Emacs', toggle_vi_mode)) append((token, ' ')) @@ -420,7 +432,7 @@ def get_text_fragments(): style='class:status-toolbar', height=Dimension.exact(1), width=Dimension.exact(width)), - filter=~IsDone() & RendererHeightIsKnown() & + filter=~is_done & renderer_height_is_known & Condition(lambda: python_input.show_status_bar and not python_input.show_exit_confirmation)) @@ -437,7 +449,7 @@ def get_text_fragments(): (style, ' \n'), ] - visible = ~IsDone() & Condition(lambda: python_input.show_exit_confirmation) + visible = ~is_done & Condition(lambda: python_input.show_exit_confirmation) return ConditionalContainer( content=Window(FormattedTextControl(get_text_fragments), style=style), # , has_focus=visible)), @@ -461,7 +473,7 @@ def extra_condition(): python_input.accept_input_on_enter is None) and '\n' in b.text) - visible = ~IsDone() & HasFocus(DEFAULT_BUFFER) & Condition(extra_condition) + visible = ~is_done & has_focus(DEFAULT_BUFFER) & Condition(extra_condition) return ConditionalContainer( content=Window(FormattedTextControl(get_text_fragments)), @@ -498,22 +510,23 @@ def menu_position(): buffer=python_input.default_buffer, search_buffer_control=search_toolbar.control, lexer=lexer, - input_processor=merge_processors([ + include_default_input_processors=False, + input_processors=[ ConditionalProcessor( - processor=HighlightSearchProcessor(preview_search=True), - filter=HasFocus(SEARCH_BUFFER) | HasFocus(search_toolbar.control), + processor=HighlightIncrementalSearchProcessor(), + filter=has_focus(SEARCH_BUFFER) | has_focus(search_toolbar.control), ), HighlightSelectionProcessor(), DisplayMultipleCursors(), # Show matching parentheses, but only while editing. ConditionalProcessor( processor=HighlightMatchingBracketProcessor(chars='[](){}'), - filter=HasFocus(DEFAULT_BUFFER) & ~IsDone() & + filter=has_focus(DEFAULT_BUFFER) & ~is_done & Condition(lambda: python_input.highlight_matching_parenthesis)), ConditionalProcessor( processor=AppendAutoSuggestion(), - filter=~IsDone()) - ] + extra_buffer_processors), + filter=~is_done) + ] + extra_buffer_processors, menu_position=menu_position, # Make sure that we always see the result of an reverse-i-search: @@ -542,7 +555,7 @@ def menu_position(): ycursor=True, content=ConditionalContainer( content=CompletionsMenu( - scroll_offset=Integer.from_callable( + scroll_offset=( lambda: python_input.completion_menu_scroll_offset), max_height=12), filter=show_completions_menu(python_input))), @@ -576,7 +589,7 @@ def menu_position(): height=D.exact(1), char='\u2500', style='class:separator'), - filter=HasSignature(python_input) & ShowDocstring(python_input) & ~IsDone()), + filter=HasSignature(python_input) & ShowDocstring(python_input) & ~is_done), ConditionalContainer( content=Window( BufferControl( @@ -585,8 +598,7 @@ def menu_position(): #lexer=PythonLexer, ), height=D(max=12)), - filter=HasSignature(python_input) & ShowDocstring(python_input) & ~IsDone(), - ), + filter=HasSignature(python_input) & ShowDocstring(python_input) & ~is_done), ]), ConditionalContainer( content=HSplit([ @@ -594,7 +606,7 @@ def menu_position(): Window(style='class:sidebar,separator', height=1), python_sidebar_navigation(python_input), ]), - filter=ShowSidebar(python_input) & ~IsDone()) + filter=ShowSidebar(python_input) & ~is_done) ]), ] + extra_toolbars + [ VSplit([ diff --git a/ptpython/prompt_style.py b/ptpython/prompt_style.py index ab1351ac..58514afe 100644 --- a/ptpython/prompt_style.py +++ b/ptpython/prompt_style.py @@ -14,22 +14,22 @@ class PromptStyle(with_metaclass(ABCMeta, object)): Base class for all prompts. """ @abstractmethod - def in_tokens(self): + def in_prompt(self): " Return the input tokens. " return [] @abstractmethod - def in2_tokens(self, width): + def in2_prompt(self, width): """ Tokens for every following input line. :param width: The available width. This is coming from the width taken - by `in_tokens`. + by `in_prompt`. """ return [] @abstractmethod - def out_tokens(self): + def out_prompt(self): " Return the output tokens. " return [] @@ -41,19 +41,19 @@ class IPythonPrompt(PromptStyle): def __init__(self, python_input): self.python_input = python_input - def in_tokens(self): + def in_prompt(self): return [ ('class:in', 'In ['), ('class:in.number', '%s' % self.python_input.current_statement_index), ('class:in', ']: '), ] - def in2_tokens(self, width): + def in2_prompt(self, width): return [ ('class:in', '...: '.rjust(width)), ] - def out_tokens(self): + def out_prompt(self): return [ ('class:out', 'Out['), ('class:out.number', '%s' % self.python_input.current_statement_index), @@ -66,11 +66,11 @@ class ClassicPrompt(PromptStyle): """ The classic Python prompt. """ - def in_tokens(self): + def in_prompt(self): return [('class:prompt', '>>> ')] - def in2_tokens(self, width): + def in2_prompt(self, width): return [('class:prompt.dots', '...')] - def out_tokens(self): + def out_prompt(self): return [] diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 1adc2e61..67378165 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -13,11 +13,12 @@ from prompt_toolkit.enums import DEFAULT_BUFFER, EditingMode from prompt_toolkit.eventloop.defaults import get_event_loop from prompt_toolkit.filters import Condition -from prompt_toolkit.history import FileHistory, InMemoryHistory +from prompt_toolkit.history import FileHistory, InMemoryHistory, ThreadedHistory from prompt_toolkit.input.defaults import create_input from prompt_toolkit.key_binding import merge_key_bindings, ConditionalKeyBindings, KeyBindings from prompt_toolkit.key_binding.vi_state import InputMode -from prompt_toolkit.layout.lexers import PygmentsLexer, DynamicLexer, SimpleLexer +from prompt_toolkit.lexers import PygmentsLexer, DynamicLexer, SimpleLexer +from prompt_toolkit.output import ColorDepth from prompt_toolkit.output.defaults import create_output from prompt_toolkit.styles import DynamicStyle from prompt_toolkit.utils import is_windows @@ -113,6 +114,14 @@ def activate_previous(self): self.activate_next(_previous=True) +COLOR_DEPTHS = { + ColorDepth.DEPTH_1_BIT: 'Monochrome', + ColorDepth.DEPTH_4_BIT: 'ANSI Colors', + ColorDepth.DEPTH_8_BIT: '256 colors', + ColorDepth.DEPTH_24_BIT: 'True color', +} + + class PythonInput(object): """ Prompt for reading Python input. @@ -128,7 +137,7 @@ def __init__(self, input=None, output=None, - true_color=False, + color_depth=None, # For internal use. extra_key_bindings=None, @@ -140,14 +149,15 @@ def __init__(self, self.get_globals = get_globals or (lambda: {}) self.get_locals = get_locals or self.get_globals - self.output = output or create_output(true_color=Condition(lambda: self.true_color)) - self.input = input or create_input(sys.stdin) - self._completer = _completer or PythonCompleter(self.get_globals, self.get_locals) self._validator = _validator or PythonValidator(self.get_compiler_flags) - self.history = FileHistory(history_filename) if history_filename else InMemoryHistory() self._lexer = _lexer or PygmentsLexer(PythonLexer) + if history_filename: + self.history = ThreadedHistory(FileHistory(history_filename)) + else: + self.history = InMemoryHistory() + self._input_buffer_height = _input_buffer_height self._extra_layout_body = _extra_layout_body or [] self._extra_toolbars = _extra_toolbars or [] @@ -202,11 +212,11 @@ def __init__(self, 'classic': ClassicPrompt(), } - self.get_input_prompt_tokens = lambda: \ - self.all_prompt_styles[self.prompt_style].in_tokens() + self.get_input_prompt = lambda: \ + self.all_prompt_styles[self.prompt_style].in_prompt() - self.get_output_prompt_tokens = lambda: \ - self.all_prompt_styles[self.prompt_style].out_tokens() + self.get_output_prompt = lambda: \ + self.all_prompt_styles[self.prompt_style].out_prompt() #: Load styles. self.code_styles = get_all_code_styles() @@ -218,7 +228,6 @@ def __init__(self, self._current_code_style_name = 'win32' self._current_style = self._generate_style() - self.true_color = true_color # Options to be configurable from the sidebar. self.options = self._create_options() @@ -234,14 +243,17 @@ def __init__(self, # (Never run more than one at the same time.) self._get_signatures_thread_running = False - self.app = self._create_application() + self.output = output or create_output() + self.input = input or create_input(sys.stdin) + + self.app = self._create_application(color_depth) if vi_mode: self.app.editing_mode = EditingMode.VI def _accept_handler(self, buff): app = get_app() - app.set_return_value(buff.text) + app.exit(result=buff.text) app.pre_run_callables.append(buff.reset) @property @@ -326,6 +338,9 @@ def use_ui_colorscheme(self, name): self._current_ui_style_name = name self._current_style = self._generate_style() + def _use_color_depth(self, depth): + get_app().color_depth = depth + def _generate_style(self): """ Create new Style instance. @@ -476,13 +491,16 @@ def get_values(): get_values=lambda: dict( (name, partial(self.use_ui_colorscheme, name)) for name in self.ui_styles) ), - simple_option(title='True color (24 bit)', - description='Use 24 bit colors instead of 265 colors', - field_name='true_color'), + Option(title='Color depth', + description='Monochrome (1 bit), 16 ANSI colors (4 bit),\n256 colors (8 bit), or 24 bit.', + get_current_value=lambda: COLOR_DEPTHS[get_app().color_depth], + get_values=lambda: dict( + (name, partial(self._use_color_depth, depth)) for depth, name in COLOR_DEPTHS.items()) + ), ]), ] - def _create_application(self): + def _create_application(self, color_depth): """ Create an `Application` instance. """ @@ -498,18 +516,6 @@ def _create_application(self): extra_body=self._extra_layout_body, extra_toolbars=self._extra_toolbars), key_bindings=merge_key_bindings([ -# ConditionalKeyBindings( -# key_bindings=load_key_bindings( -# enable_abort_and_exit_bindings=True, -# enable_search=True, -# enable_open_in_editor=Condition(lambda: self.enable_open_in_editor), -# enable_system_bindings=Condition(lambda: self.enable_system_bindings), -# enable_auto_suggest_bindings=Condition(lambda: self.enable_auto_suggest)), -#), - # Disable all default key bindings when the sidebar or the exit confirmation - # are shown. -# filter=Condition(lambda: not (self.show_sidebar or self.show_exit_confirmation)) -# ), load_python_bindings(self), load_sidebar_bindings(self), load_confirm_exit_bindings(self), @@ -518,6 +524,7 @@ def _create_application(self): self.extra_key_bindings, Condition(lambda: not self.show_sidebar)) ]), + color_depth=color_depth, paste_mode=Condition(lambda: self.paste_mode), mouse_support=Condition(lambda: self.enable_mouse_support), style=DynamicStyle(lambda: self._current_style), @@ -645,7 +652,6 @@ def enter_history(self): def done(f): result = f.result() - assert isinstance(result, str), 'got %r' % (result, ) if result is not None: self.default_buffer.text = result diff --git a/ptpython/repl.py b/ptpython/repl.py index b981bd1e..96c50b86 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -14,7 +14,8 @@ from prompt_toolkit.document import Document from prompt_toolkit.eventloop.defaults import use_asyncio_event_loop -from prompt_toolkit.layout.utils import fragment_list_width +from prompt_toolkit.formatted_text import merge_formatted_text, FormattedText +from prompt_toolkit.formatted_text.utils import fragment_list_width from prompt_toolkit.utils import DummyContext from prompt_toolkit.shortcuts import set_title, clear_title from prompt_toolkit.shortcuts import print_formatted_text @@ -121,7 +122,7 @@ def compile_with_flags(code, mode): locals['_'] = locals['_%i' % self.current_statement_index] = result if result is not None: - out_tokens = self.get_output_prompt_tokens() + out_prompt = self.get_output_prompt() try: result_str = '%r\n' % (result, ) @@ -133,15 +134,23 @@ def compile_with_flags(code, mode): result_str = '%s\n' % repr(result).decode('utf-8') # Align every line to the first one. - line_sep = '\n' + ' ' * fragment_list_width(out_tokens) + line_sep = '\n' + ' ' * fragment_list_width(out_prompt) result_str = line_sep.join(result_str.splitlines()) + '\n' # Write output tokens. if self.enable_syntax_highlighting: - out_tokens.extend(_lex_python_result(result_str)) + formatted_output = merge_formatted_text([ + out_prompt, + PygmentsTokens(list(_lex_python_result(result_str))), + ]) else: - out_tokens.append(('', result_str)) - print_formatted_text(PygmentsTokens(out_tokens)) + formatted_output = FormattedText( + out_prompt + [('', result_str)]) + + print_formatted_text( + formatted_output, style=self._current_style, + include_default_pygments_style=False) + # If not a valid `eval` expression, run using `exec` instead. except SyntaxError: code = compile_with_flags(line, 'exec') @@ -181,7 +190,10 @@ def _handle_exception(self, e): tokens = list(_lex_python_traceback(tb)) else: tokens = [(Token, tb)] - print_formatted_text(PygmentsTokens(tokens)) + + print_formatted_text( + PygmentsTokens(tokens), style=self._current_style, + include_default_pygments_style=False) output.write('%s\n' % e) output.flush() diff --git a/ptpython/style.py b/ptpython/style.py index 44ddb6ba..15c5b2ad 100644 --- a/ptpython/style.py +++ b/ptpython/style.py @@ -1,7 +1,7 @@ from __future__ import unicode_literals from prompt_toolkit.styles import Style, merge_styles -from prompt_toolkit.styles.pygments import style_from_pygments +from prompt_toolkit.styles.pygments import style_from_pygments_cls from prompt_toolkit.utils import is_windows, is_conemu_ansi, is_windows_vt100_supported from pygments.styles import get_style_by_name, get_all_styles @@ -16,7 +16,7 @@ def get_all_code_styles(): """ Return a mapping from style names to their classes. """ - result = dict((name, style_from_pygments(get_style_by_name(name))) for name in get_all_styles()) + result = dict((name, style_from_pygments_cls(get_style_by_name(name))) for name in get_all_styles()) result['win32'] = Style.from_dict(win32_code_style) return result @@ -69,6 +69,8 @@ def generate_style(python_style, ui_style): default_ui_style = { + 'control-character': 'ansiblue', + # Classic prompt. 'prompt': 'bold', 'prompt.dots': 'noinherit', @@ -108,6 +110,9 @@ def generate_style(python_style, ui_style): 'status-toolbar.key': 'bg:#000000 #888888', 'status-toolbar.pastemodeon': 'bg:#aa4444 #ffffff', 'status-toolbar.pythonversion': 'bg:#222222 #ffffff bold', + 'status-toolbar paste-mode-on': 'bg:#aa4444 #ffffff', + 'record': 'bg:#884444 white', + 'status-toolbar.input-mode': '#ffff44', # The options sidebar. 'sidebar': 'bg:#bbbbbb #000000', From f565e6a086d3c58415ede6df67046e90f2efe5bf Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sun, 17 Jun 2018 14:49:48 +0200 Subject: [PATCH 203/470] Fix for setting color depth. (workaround for now). --- ptpython/python_input.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 67378165..9381a159 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -339,7 +339,7 @@ def use_ui_colorscheme(self, name): self._current_style = self._generate_style() def _use_color_depth(self, depth): - get_app().color_depth = depth + get_app()._color_depth = depth def _generate_style(self): """ From 8c6dc4633eb7c820c4d124de4ce4db27ce75ce30 Mon Sep 17 00:00:00 2001 From: Carl George Date: Tue, 19 Jun 2018 15:09:27 -0500 Subject: [PATCH 204/470] switch from asyncio.async to asyncio.ensure_future Python 3.7 removes asyncio.async (it has been deprecated since 3.4.4). It was replaced by asyncio.ensure_future. https://docs.python.org/3.7/whatsnew/3.7.html https://docs.python.org/3.6/library/asyncio-task.html#asyncio.async --- examples/asyncio-python-embed.py | 4 ++-- ptpython/contrib/asyncssh_repl.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/asyncio-python-embed.py b/examples/asyncio-python-embed.py index eadad2d8..280d4b52 100755 --- a/examples/asyncio-python-embed.py +++ b/examples/asyncio-python-embed.py @@ -45,8 +45,8 @@ def interactive_shell(): def main(): - asyncio.async(print_counter()) - asyncio.async(interactive_shell()) + asyncio.ensure_future(print_counter()) + asyncio.ensure_future(interactive_shell()) loop.run_forever() loop.close() diff --git a/ptpython/contrib/asyncssh_repl.py b/ptpython/contrib/asyncssh_repl.py index 02b8fd9b..a4df4449 100644 --- a/ptpython/contrib/asyncssh_repl.py +++ b/ptpython/contrib/asyncssh_repl.py @@ -92,7 +92,7 @@ def connection_made(self, chan): self._chan = chan # Run REPL interface. - f = asyncio.async(self.cli.run_async()) + f = asyncio.ensure_future(self.cli.run_async()) # Close channel when done. def done(_): From 35303d69d713f3988459fb6dccb6273e9ab16486 Mon Sep 17 00:00:00 2001 From: Julian <22564375+ju-w@users.noreply.github.com> Date: Tue, 19 Jun 2018 19:31:51 +0200 Subject: [PATCH 205/470] Unified handling of source files. --- ptpython/entry_points/run_ptipython.py | 7 +++++-- ptpython/entry_points/run_ptpython.py | 5 ++++- ptpython/repl.py | 4 ++-- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/ptpython/entry_points/run_ptipython.py b/ptpython/entry_points/run_ptipython.py index 506f4b8f..a563f52e 100644 --- a/ptpython/entry_points/run_ptipython.py +++ b/ptpython/entry_points/run_ptipython.py @@ -48,7 +48,10 @@ def run(user_ns=None): # When a file has been given, run that, otherwise start the shell. if a[''] and not a['--interactive']: sys.argv = a[''] - six.exec_(compile(open(a[''][0], "rb").read(), a[''][0], 'exec')) + path = a[''][0] + with open(path, 'rb') as f: + code = compile(f.read(), path, 'exec') + six.exec_(code) else: enable_deprecation_warnings() @@ -71,7 +74,7 @@ def run(user_ns=None): # exec scripts from startup paths for path in startup_paths: if os.path.exists(path): - with open(path, 'r') as f: + with open(path, 'rb') as f: code = compile(f.read(), path, 'exec') six.exec_(code, user_ns, user_ns) else: diff --git a/ptpython/entry_points/run_ptpython.py b/ptpython/entry_points/run_ptpython.py index 333fd1df..356d6bd3 100644 --- a/ptpython/entry_points/run_ptpython.py +++ b/ptpython/entry_points/run_ptpython.py @@ -52,7 +52,10 @@ def run(): # When a file has been given, run that, otherwise start the shell. if a[''] and not a['--interactive']: sys.argv = a[''] - six.exec_(compile(open(a[''][0], "rb").read(), a[''][0], 'exec')) + path = a[''][0] + with open(path, 'rb') as f: + code = compile(f.read(), path, 'exec') + six.exec_(code) # Run interactive shell. else: diff --git a/ptpython/repl.py b/ptpython/repl.py index 96c50b86..8abae91d 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -49,7 +49,7 @@ def _load_start_paths(self): if self._startup_paths: for path in self._startup_paths: if os.path.exists(path): - with open(path, 'r') as f: + with open(path, 'rb') as f: code = compile(f.read(), path, 'exec') six.exec_(code, self.get_globals(), self.get_locals()) else: @@ -255,7 +255,7 @@ def enter_to_continue(): try: namespace = {} - with open(config_file, 'r') as f: + with open(config_file, 'rb') as f: code = compile(f.read(), config_file, 'exec') six.exec_(code, namespace, namespace) From d89130a9ab9680b3421c7c6d73b9192cc6ddaf7d Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 7 Aug 2018 20:39:55 +0200 Subject: [PATCH 206/470] Added an option for swapping between dark/light colors. --- ptpython/python_input.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 9381a159..dfda781b 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -20,7 +20,7 @@ from prompt_toolkit.lexers import PygmentsLexer, DynamicLexer, SimpleLexer from prompt_toolkit.output import ColorDepth from prompt_toolkit.output.defaults import create_output -from prompt_toolkit.styles import DynamicStyle +from prompt_toolkit.styles import DynamicStyle, SwapLightAndDarkStyleTransformation, ConditionalStyleTransformation from prompt_toolkit.utils import is_windows from prompt_toolkit.validation import ConditionalValidator @@ -191,6 +191,7 @@ def __init__(self, # with the current input. self.enable_syntax_highlighting = True + self.swap_light_and_dark = False self.highlight_matching_parenthesis = False self.show_sidebar = False # Currently show the sidebar. self.show_sidebar_help = True # When the sidebar is visible, also show the help text. @@ -228,6 +229,7 @@ def __init__(self, self._current_code_style_name = 'win32' self._current_style = self._generate_style() + self.color_depth = color_depth or ColorDepth.default() # Options to be configurable from the sidebar. self.options = self._create_options() @@ -246,7 +248,7 @@ def __init__(self, self.output = output or create_output() self.input = input or create_input(sys.stdin) - self.app = self._create_application(color_depth) + self.app = self._create_application() if vi_mode: self.app.editing_mode = EditingMode.VI @@ -339,7 +341,7 @@ def use_ui_colorscheme(self, name): self._current_style = self._generate_style() def _use_color_depth(self, depth): - get_app()._color_depth = depth + self.color_depth = depth def _generate_style(self): """ @@ -479,6 +481,9 @@ def get_values(): simple_option(title='Syntax highlighting', description='Use colors for syntax highligthing', field_name='enable_syntax_highlighting'), + simple_option(title='Swap light/dark colors', + description='Swap light and dark colors.', + field_name='swap_light_and_dark'), Option(title='Code', description='Color scheme to use for the Python code.', get_current_value=lambda: self._current_code_style_name, @@ -493,14 +498,14 @@ def get_values(): ), Option(title='Color depth', description='Monochrome (1 bit), 16 ANSI colors (4 bit),\n256 colors (8 bit), or 24 bit.', - get_current_value=lambda: COLOR_DEPTHS[get_app().color_depth], + get_current_value=lambda: COLOR_DEPTHS[self.color_depth], get_values=lambda: dict( (name, partial(self._use_color_depth, depth)) for depth, name in COLOR_DEPTHS.items()) ), ]), ] - def _create_application(self, color_depth): + def _create_application(self): """ Create an `Application` instance. """ @@ -524,10 +529,13 @@ def _create_application(self, color_depth): self.extra_key_bindings, Condition(lambda: not self.show_sidebar)) ]), - color_depth=color_depth, + color_depth=lambda: self.color_depth, paste_mode=Condition(lambda: self.paste_mode), mouse_support=Condition(lambda: self.enable_mouse_support), style=DynamicStyle(lambda: self._current_style), + style_transformation=ConditionalStyleTransformation( + SwapLightAndDarkStyleTransformation(), + filter=Condition(lambda: self.swap_light_and_dark)), include_default_pygments_style=False, reverse_vi_search_direction=True) From d49769786702f38d7876c77769be4d162573a150 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sun, 30 Sep 2018 20:22:52 +0200 Subject: [PATCH 207/470] Ensure that '' is always in sys.path. (Workaround for Jedi bug.) --- ptpython/repl.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ptpython/repl.py b/ptpython/repl.py index 8abae91d..0f141edd 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -99,6 +99,11 @@ def _execute(self, line): """ output = self.app.output + # WORKAROUND: Due to a bug in Jedi, the current directory is removed + # from sys.path. See: https://github.com/davidhalter/jedi/issues/1148 + if '' not in sys.path: + sys.path.insert(0, '') + def compile_with_flags(code, mode): " Compile code with the right compiler flags. " return compile(code, '', mode, From db24a15035e924081aaf41e1cc7ec181f9b23c49 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sun, 30 Sep 2018 20:27:42 +0200 Subject: [PATCH 208/470] Release 2.0.1 --- CHANGELOG | 6 ++++++ setup.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 73febeea..0ad87b2c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,12 @@ CHANGELOG ========= +2.0.1: 2018-09-30 +----------------- + +Upgrade to prompt_toolkit 2.0.x. + + 0.36: 2016-10-16 ---------------- diff --git a/setup.py b/setup.py index ad46f68b..efb10505 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ setup( name='ptpython', author='Jonathan Slenders', - version='0.36', + version='2.0.1', url='https://github.com/jonathanslenders/ptpython', description='Python REPL build on top of prompt_toolkit', long_description=long_description, From b29cfac374b72e367f3f6f0646effd4eb63ba967 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sun, 30 Sep 2018 20:41:28 +0200 Subject: [PATCH 209/470] Fixed .travis.ci (No tox anymore). --- .travis.yml | 27 ++++++++++++++++++--------- tox.ini | 10 ---------- 2 files changed, 18 insertions(+), 19 deletions(-) delete mode 100644 tox.ini diff --git a/.travis.yml b/.travis.yml index 02853d77..79a93e91 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,12 +1,21 @@ +sudo: false language: python -env: - - TOXENV=py26 - - TOXENV=py27 - - TOXENV=py33 - - TOXENV=py34 - - TOXENV=pypy - - TOXENV=pypy3 + +matrix: + include: + - python: 3.6 + - python: 3.5 + - python: 3.4 + - python: 3.3 + - python: 2.7 + - python: 2.6 + - python: pypy + - python: pypy3 + install: - - travis_retry pip install tox + - travis_retry pip install . pytest + - pip list + script: - - tox + - echo "$TRAVIS_PYTHON_VERSION" + - ./tests/run_tests.py diff --git a/tox.ini b/tox.ini deleted file mode 100644 index 40a358ab..00000000 --- a/tox.ini +++ /dev/null @@ -1,10 +0,0 @@ -# Tox (http://tox.testrun.org/) is a tool for running tests -# in multiple virtualenvs. This configuration file will run the -# test suite on all supported python versions. To use it, "pip install tox" -# and then run "tox" from this directory. - -[tox] -envlist = py26, py27, py33, py34, pypy, pypy3 - -[testenv] -commands = {toxinidir}/tests/run_tests.py From 6c0b99ea2b4be963b42c1062b2f33880ba30b6d3 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Wed, 15 Nov 2017 08:18:41 +0100 Subject: [PATCH 210/470] Suppress all unknown Jedi errors. --- ptpython/completer.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ptpython/completer.py b/ptpython/completer.py index 0d547de0..7a63912a 100644 --- a/ptpython/completer.py +++ b/ptpython/completer.py @@ -154,6 +154,12 @@ def get_completions(self, document, complete_event): # In jedi.api.helpers.py: 144, in get_stack_at_position # raise SystemError("This really shouldn't happen. There's a bug in Jedi.") pass + except NotImplementedError: + # See: https://github.com/jonathanslenders/ptpython/issues/223 + pass + except Exception: + # Supress all other Jedi exceptions. + pass else: for c in completions: yield Completion(c.name_with_symbols, len(c.complete) - len(c.name_with_symbols), From 9b61c9941bd4a5dff61c7bbb03172b0a515ee83b Mon Sep 17 00:00:00 2001 From: Nicholas Devenish Date: Tue, 27 Feb 2018 18:34:15 +0000 Subject: [PATCH 211/470] Don't crash history browser with no history In the case where there was no history, hitting space would try to add a non-existent line. Fixes #214. --- ptpython/history_browser.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ptpython/history_browser.py b/ptpython/history_browser.py index 18ec3f63..3d14067a 100644 --- a/ptpython/history_browser.py +++ b/ptpython/history_browser.py @@ -418,6 +418,10 @@ def _(event): b = event.current_buffer line_no = b.document.cursor_position_row + if not history_mapping.history_lines: + # If we've no history, then nothing to do + return + if line_no in history_mapping.selected_lines: # Remove line. history_mapping.selected_lines.remove(line_no) From 4dd378b25849183e49d9706783681fe3816c7fe6 Mon Sep 17 00:00:00 2001 From: Daniel Pope Date: Thu, 22 Mar 2018 11:31:22 +0000 Subject: [PATCH 212/470] Document using ptpython as your default REPL --- README.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.rst b/README.rst index f5752004..47a9e0eb 100644 --- a/README.rst +++ b/README.rst @@ -74,6 +74,20 @@ Embedding the REPL in any Python application is easy: from ptpython.repl import embed embed(globals(), locals()) +You can make ptpython your default Python REPL by creating a `PYTHONSTARTUP file +`_ containing code +like this: + +.. code:: python + + import sys + try: + from ptpython.repl import embed + except ImportError: + print("ptpython is not available: falling back to standard prompt") + else: + sys.exit(embed(globals(), locals())) + Multiline editing ***************** From 32c4c8f147548f6ad76237e7e16c495b902371d0 Mon Sep 17 00:00:00 2001 From: Nasy Date: Sat, 2 Jun 2018 14:37:43 +0800 Subject: [PATCH 213/470] Update python-embed-with-custom-prompt.py Since there was a new abstract method in2_tokens, this example is not able to run successfully. --- examples/python-embed-with-custom-prompt.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/examples/python-embed-with-custom-prompt.py b/examples/python-embed-with-custom-prompt.py index fe889fad..28eca860 100755 --- a/examples/python-embed-with-custom-prompt.py +++ b/examples/python-embed-with-custom-prompt.py @@ -23,6 +23,11 @@ def in_tokens(self, cli): (Token.In, '] >>: '), ] + def in2_tokens(self, cli, width): + return [ + (Token.In, '...: '.rjust(width)), + ] + def out_tokens(self, cli): return [ (Token.Out, 'Result['), From c2a58d39d632773b7bb3997fa22cc87c7d683964 Mon Sep 17 00:00:00 2001 From: Cenk Alti Date: Fri, 2 Mar 2018 09:40:29 +0300 Subject: [PATCH 214/470] Set last exception in sys module. Fixes #216 --- ptpython/repl.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ptpython/repl.py b/ptpython/repl.py index 0f141edd..fa6488ef 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -169,6 +169,10 @@ def _handle_exception(self, e): # Instead of just calling ``traceback.format_exc``, we take the # traceback and skip the bottom calls of this framework. t, v, tb = sys.exc_info() + + # Required for pdb.post_mortem() to work. + sys.last_type, sys.last_value, sys.last_traceback = t, v, tb + tblist = traceback.extract_tb(tb) for line_nr, tb_tuple in enumerate(tblist): From 64479e1c800ad0dda6e5f0ed9e49823ed0080981 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sun, 30 Sep 2018 22:02:27 +0200 Subject: [PATCH 215/470] Require prompt_toolkit 2.0.5 (we were using some newer features). --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index efb10505..51352a04 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ install_requires = [ 'docopt', 'jedi>=0.9.0', - 'prompt_toolkit>=2.0.0,<2.1.0', + 'prompt_toolkit>=2.0.5,<2.1.0', 'pygments', ], entry_points={ From 1319ce6b1317f51e36e90781e2eeebc2e775e212 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sun, 30 Sep 2018 22:10:28 +0200 Subject: [PATCH 216/470] Release 2.0.2 --- CHANGELOG | 9 +++++++++ setup.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 0ad87b2c..f7f327ce 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,15 @@ CHANGELOG ========= +2.0.2: 2018-09-30 +----------------- + +Fixes: +- Don't crash the history browser when there was no history. +- Set last exception in the sys module, when an exception was raised. +- Require prompt_toolkit 2.0.5. + + 2.0.1: 2018-09-30 ----------------- diff --git a/setup.py b/setup.py index 51352a04..0e378e1b 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ setup( name='ptpython', author='Jonathan Slenders', - version='2.0.1', + version='2.0.2', url='https://github.com/jonathanslenders/ptpython', description='Python REPL build on top of prompt_toolkit', long_description=long_description, From 165619d8b377f735a0a14f5af08b98e7cc48c944 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sun, 7 Oct 2018 20:38:19 +0200 Subject: [PATCH 217/470] Don't reset text in accept_handler (prompt_toolkit change). --- ptpython/python_input.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ptpython/python_input.py b/ptpython/python_input.py index dfda781b..5cc6c5cf 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -257,6 +257,7 @@ def _accept_handler(self, buff): app = get_app() app.exit(result=buff.text) app.pre_run_callables.append(buff.reset) + return True # Keep text, we call 'reset' later on. @property def option_count(self): From ff0e75457970e3e537f5f2d1dcf4bfcafe67b721 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 9 Oct 2018 21:16:51 +0200 Subject: [PATCH 218/470] Allow changing brightness. --- ptpython/python_input.py | 42 ++++++++++++++++++++++++++++++++++++---- ptpython/repl.py | 2 ++ 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 5cc6c5cf..e0318435 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -20,7 +20,7 @@ from prompt_toolkit.lexers import PygmentsLexer, DynamicLexer, SimpleLexer from prompt_toolkit.output import ColorDepth from prompt_toolkit.output.defaults import create_output -from prompt_toolkit.styles import DynamicStyle, SwapLightAndDarkStyleTransformation, ConditionalStyleTransformation +from prompt_toolkit.styles import DynamicStyle, SwapLightAndDarkStyleTransformation, ConditionalStyleTransformation, AdjustBrightnessStyleTransformation, merge_style_transformations from prompt_toolkit.utils import is_windows from prompt_toolkit.validation import ConditionalValidator @@ -231,6 +231,9 @@ def __init__(self, self._current_style = self._generate_style() self.color_depth = color_depth or ColorDepth.default() + self.max_brightness = 1.0 + self.min_brightness = 0.0 + # Options to be configurable from the sidebar. self.options = self._create_options() self.selected_option_index = 0 @@ -248,6 +251,15 @@ def __init__(self, self.output = output or create_output() self.input = input or create_input(sys.stdin) + self.style_transformation = merge_style_transformations([ + ConditionalStyleTransformation( + SwapLightAndDarkStyleTransformation(), + filter=Condition(lambda: self.swap_light_and_dark)), + AdjustBrightnessStyleTransformation( + lambda: self.min_brightness, + lambda: self.max_brightness), + ]) + self.app = self._create_application() if vi_mode: @@ -344,6 +356,14 @@ def use_ui_colorscheme(self, name): def _use_color_depth(self, depth): self.color_depth = depth + def _set_min_brightness(self, value): + self.min_brightness = value + self.max_brightness = max(self.max_brightness, value) + + def _set_max_brightness(self, value): + self.max_brightness = value + self.min_brightness = min(self.min_brightness, value) + def _generate_style(self): """ Create new Style instance. @@ -384,6 +404,8 @@ def get_values(): get_values=get_values, get_current_value=get_current_value) + brightness_values = [1.0 / 20 * value for value in range(0, 21)] + return [ OptionCategory('Input', [ simple_option(title='Editing mode', @@ -503,6 +525,20 @@ def get_values(): get_values=lambda: dict( (name, partial(self._use_color_depth, depth)) for depth, name in COLOR_DEPTHS.items()) ), + Option(title='Min brightness', + description='Minimum brightness for the color scheme (default=0.0).', + get_current_value=lambda: '%.2f' % self.min_brightness, + get_values=lambda: dict( + ('%.2f' % value, partial(self._set_min_brightness, value)) + for value in brightness_values) + ), + Option(title='Max brightness', + description='Maximum brightness for the color scheme (default=1.0).', + get_current_value=lambda: '%.2f' % self.max_brightness, + get_values=lambda: dict( + ('%.2f' % value, partial(self._set_max_brightness, value)) + for value in brightness_values) + ), ]), ] @@ -534,9 +570,7 @@ def _create_application(self): paste_mode=Condition(lambda: self.paste_mode), mouse_support=Condition(lambda: self.enable_mouse_support), style=DynamicStyle(lambda: self._current_style), - style_transformation=ConditionalStyleTransformation( - SwapLightAndDarkStyleTransformation(), - filter=Condition(lambda: self.swap_light_and_dark)), + style_transformation=self.style_transformation, include_default_pygments_style=False, reverse_vi_search_direction=True) diff --git a/ptpython/repl.py b/ptpython/repl.py index fa6488ef..c480869a 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -154,6 +154,7 @@ def compile_with_flags(code, mode): print_formatted_text( formatted_output, style=self._current_style, + style_transformation=self.style_transformation, include_default_pygments_style=False) # If not a valid `eval` expression, run using `exec` instead. @@ -202,6 +203,7 @@ def _handle_exception(self, e): print_formatted_text( PygmentsTokens(tokens), style=self._current_style, + style_transformation=self.style_transformation, include_default_pygments_style=False) output.write('%s\n' % e) From 350436055944343b6459f0b26853b473777d3522 Mon Sep 17 00:00:00 2001 From: Chris Miller Date: Mon, 8 Oct 2018 16:10:07 -0400 Subject: [PATCH 219/470] Update config.py example to use key_processor It appears that as of release 2.0.1 on 2018-06-02 the python-prompt-toolkit package was updated so that ``` * `input_processor` was renamed to `key_processor`. ``` This config example was updated at the time to import the correct object, but the comment that actually uses it later on was not updated. This simply fixes the comment as well so that people using the most recent version of ptpython can uncomment the config example here and use it. Tested with a fresh install of ptpython and this config file with lines 141-144 uncommented. I was getting this error when typing 'jj' in Vim insert mode in ptpython: ``` Unhandled exception in event loop: File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/prompt_toolkit/eventloop/posix.py", line 154, in _run_task t() File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/prompt_toolkit/eventloop/context.py", line 115, in new_func return func(*a, **kw) File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/prompt_toolkit/application/application.py", line 555, in read_from_input self.key_processor.process_keys() File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/prompt_toolkit/key_binding/key_processor.py", line 273, in process_keys self._process_coroutine.send(key_press) File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/prompt_toolkit/key_binding/key_processor.py", line 180, in _process self._call_handler(matches[-1], key_sequence=buffer[:]) File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/prompt_toolkit/key_binding/key_processor.py", line 323, in _call_handler handler.call(event) File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/prompt_toolkit/key_binding/key_bindings.py", line 78, in call return self.handler(event) File "/Users/chris_miller/.ptpython/config.py", line 143, in _ event.cli.input_processor.feed(KeyPress(Keys.Escape)) Exception 'Application' object has no attribute 'input_processor' ``` After the change proposed here the error went away. --- examples/ptpython_config/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/ptpython_config/config.py b/examples/ptpython_config/config.py index 273977ef..ddee6c09 100644 --- a/examples/ptpython_config/config.py +++ b/examples/ptpython_config/config.py @@ -141,7 +141,7 @@ def _(event): @repl.add_key_binding('j', 'j', filter=ViInsertMode()) def _(event): " Map 'jj' to Escape. " - event.cli.input_processor.feed(KeyPress(Keys.Escape)) + event.cli.key_processor.feed(KeyPress(Keys.Escape)) """ # Custom key binding for some simple autocorrection while typing. From 477aba25b7dc7051014acbf222972ea4eb15f6c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kai=20Xia=28=E5=A4=8F=E6=81=BA=29?= Date: Mon, 1 Oct 2018 11:05:44 +1000 Subject: [PATCH 220/470] Fix documentation in PythonInput class --- ptpython/python_input.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ptpython/python_input.py b/ptpython/python_input.py index e0318435..3c4058fb 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -129,7 +129,7 @@ class PythonInput(object): :: python_input = PythonInput(...) - python_code = python_input.run() + python_code = python_input.app.run() """ def __init__(self, get_globals=None, get_locals=None, history_filename=None, From 7895a424319aeb061c7d0d3db757b1464e6715cc Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Fri, 12 Oct 2018 19:00:17 +0200 Subject: [PATCH 221/470] Release 2.0.3 --- CHANGELOG | 6 ++++++ setup.py | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index f7f327ce..8954fbdc 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,12 @@ CHANGELOG ========= +2.0.3: 2018-10-12 +----------------- + +- Allow changing the min/max brightness. +- Some changes for compatibility with the latest prompt_toolkit. + 2.0.2: 2018-09-30 ----------------- diff --git a/setup.py b/setup.py index 0e378e1b..fe536d87 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ setup( name='ptpython', author='Jonathan Slenders', - version='2.0.2', + version='2.0.3', url='https://github.com/jonathanslenders/ptpython', description='Python REPL build on top of prompt_toolkit', long_description=long_description, @@ -22,7 +22,7 @@ install_requires = [ 'docopt', 'jedi>=0.9.0', - 'prompt_toolkit>=2.0.5,<2.1.0', + 'prompt_toolkit>=2.0.6,<2.1.0', 'pygments', ], entry_points={ From 447f9953d91de95e91e53ba8236ef0451b83e410 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sat, 13 Oct 2018 22:43:09 +0200 Subject: [PATCH 222/470] Fixed ptipython. --- ptpython/ipython.py | 89 ++++++++++++--------------------------------- ptpython/layout.py | 4 +- 2 files changed, 25 insertions(+), 68 deletions(-) diff --git a/ptpython/ipython.py b/ptpython/ipython.py index a58d1fca..ad87f8f4 100644 --- a/ptpython/ipython.py +++ b/ptpython/ipython.py @@ -11,13 +11,15 @@ from __future__ import unicode_literals, print_function from prompt_toolkit.completion import Completion, Completer -from prompt_toolkit.contrib.completers import PathCompleter, WordCompleter, SystemCompleter +from prompt_toolkit.completion import PathCompleter, WordCompleter +from prompt_toolkit.contrib.completers import SystemCompleter from prompt_toolkit.contrib.regular_languages.compiler import compile from prompt_toolkit.contrib.regular_languages.completion import GrammarCompleter from prompt_toolkit.contrib.regular_languages.lexer import GrammarLexer from prompt_toolkit.document import Document -from prompt_toolkit.interface import CommandLineInterface -from prompt_toolkit.layout.lexers import PygmentsLexer, SimpleLexer +from prompt_toolkit.formatted_text import PygmentsTokens +from prompt_toolkit.lexers import PygmentsLexer, SimpleLexer +from prompt_toolkit.styles import Style from .python_input import PythonInput, PythonValidator, PythonCompleter from .style import default_ui_style @@ -36,29 +38,6 @@ class IPythonPrompt(PromptStyle): - """ - PromptStyle that uses the templates, as set by IPython. - Usually, something like "In [1]:". - """ - def __init__(self, prompt_manager): - self.prompt_manager = prompt_manager - - def in_prompt(self): - text = self.prompt_manager.render('in', color=False, just=False) - return [('class:in', text)] - - def in2_prompt(self, width): - text = self.prompt_manager.render('in2', color=False, just=False) - return [('class:in', text.rjust(width))] - - def out_tokens(self): - # This function is currently not used by IPython. But for completeness, - # it would look like this. - text = self.prompt_manager.render('out', color=False, just=False) - return [('class:out', text)] - - -class IPython5Prompt(PromptStyle): """ Style for IPython >5.0, use the prompt_toolkit tokens directly. """ @@ -66,10 +45,10 @@ def __init__(self, prompts): self.prompts = prompts def in_prompt(self): - return self.prompts.in_prompt_tokens() + return PygmentsTokens(self.prompts.in_prompt_tokens()) def in2_prompt(self, width): - return self.prompts.continuation_prompt_tokens() + return PygmentsTokens(self.prompts.continuation_prompt_tokens()) def out_prompt(self): return [] @@ -183,29 +162,21 @@ def __init__(self, ipython_shell, *a, **kw): super(IPythonInput, self).__init__(*a, **kw) self.ipython_shell = ipython_shell - # Prompt for IPython < 5.0 - if hasattr(ipython_shell, 'prompt_manager'): - self.all_prompt_styles['ipython'] = IPythonPrompt(ipython_shell.prompt_manager) - self.prompt_style = 'ipython' - - # Prompt for IPython >=5.0: - if hasattr(ipython_shell, 'prompts'): - self.all_prompt_styles['ipython'] = IPython5Prompt(ipython_shell.prompts) - self.prompt_style = 'ipython' - + self.all_prompt_styles['ipython'] = IPythonPrompt(ipython_shell.prompts) + self.prompt_style = 'ipython' # UI style for IPython. Add tokens that are used by IPython>5.0 style_dict = {} style_dict.update(default_ui_style) style_dict.update({ - 'prompt': '#009900', - 'prompt-num': '#00ff00 bold', - 'out-prompt': '#990000', - 'out-prompt-num': '#ff0000 bold', + 'pygments.prompt': '#009900', + 'pygments.prompt-num': '#00ff00 bold', + 'pygments.out-prompt': '#990000', + 'pygments.out-prompt-num': '#ff0000 bold', }) self.ui_styles = { - 'default': style_dict, + 'default': Style.from_dict(style_dict), } self.use_ui_colorscheme('default') @@ -223,44 +194,30 @@ def __init__(self, *a, **kw): configure = kw.pop('configure', None) title = kw.pop('title', None) + # Don't ask IPython to confirm for exit. We have our own exit prompt. + self.confirm_exit = False + super(InteractiveShellEmbed, self).__init__(*a, **kw) def get_globals(): return self.user_ns - ipython_input = IPythonInput( + python_input = IPythonInput( self, get_globals=get_globals, vi_mode=vi_mode, history_filename=history_filename) if title: - ipython_input.terminal_title = title + python_input.terminal_title = title if configure: - configure(ipython_input) - ipython_input.prompt_style = 'ipython' # Don't take from config. + configure(python_input) + python_input.prompt_style = 'ipython' # Don't take from config. - self._cli = CommandLineInterface( - application=ipython_input.create_application()) + self.python_input = python_input def prompt_for_code(self): - # IPython 5.0 calls `prompt_for_code` instead of `raw_input`. - return self.raw_input(self) - - def raw_input(self, prompt=''): - print('') - try: - string = self._cli.run(reset_current_buffer=True).text - - # In case of multiline input, make sure to append a newline to the input, - # otherwise, IPython will ask again for more input in some cases. - if '\n' in string: - return string + '\n\n' - else: - return string - except EOFError: - self.ask_exit() - return '' + return self.python_input.app.run() def initialize_extensions(shell, extensions): diff --git a/ptpython/layout.py b/ptpython/layout.py index b89ab21d..e6399176 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -6,7 +6,7 @@ from prompt_toolkit.application import get_app from prompt_toolkit.enums import DEFAULT_BUFFER, SEARCH_BUFFER from prompt_toolkit.filters import is_done, has_completions, renderer_height_is_known, has_focus, Condition -from prompt_toolkit.formatted_text.utils import fragment_list_width +from prompt_toolkit.formatted_text import fragment_list_width, to_formatted_text from prompt_toolkit.key_binding.vi_state import InputMode from prompt_toolkit.layout.containers import Window, HSplit, VSplit, FloatContainer, Float, ConditionalContainer, ScrollOffsets from prompt_toolkit.layout.controls import BufferControl, FormattedTextControl @@ -275,7 +275,7 @@ def get_prompt_style(): return python_input.all_prompt_styles[python_input.prompt_style] def get_prompt(): - return get_prompt_style().in_prompt() + return to_formatted_text(get_prompt_style().in_prompt()) def get_continuation(width, line_number, is_soft_wrap): if python_input.show_line_numbers and not is_soft_wrap: From 78a71b938e5bfcd728ddf52472ebf8a1afcea13f Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sat, 13 Oct 2018 23:01:32 +0200 Subject: [PATCH 223/470] Fixed config: setting of color depth. --- examples/ptpython_config/config.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/examples/ptpython_config/config.py b/examples/ptpython_config/config.py index ddee6c09..9c316cba 100644 --- a/examples/ptpython_config/config.py +++ b/examples/ptpython_config/config.py @@ -103,9 +103,12 @@ def configure(repl): # Use this colorscheme for the code. repl.use_code_colorscheme('pastie') - # Enable 24bit True color. (Not all terminals support this. -- maybe check - # $TERM before changing.) - repl.true_color = False + # Set color depth (keep in mind that not all terminals support true color). + + #repl.color_depth = 'DEPTH_1_BIT' # Monochrome. + #repl.color_depth = 'DEPTH_4_BIT' # ANSI colors only. + repl.color_depth = 'DEPTH_8_BIT' # The default, 256 colors. + #repl.color_depth = 'DEPTH_24_BIT' # True color. # Syntax. repl.enable_syntax_highlighting = True From ceddae5cf728853b4071c2248d10a349fc8ab5eb Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sat, 13 Oct 2018 23:11:14 +0200 Subject: [PATCH 224/470] Fixed Control-D key binding for exiting REPL (when confirm_exit=False). --- ptpython/key_bindings.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/ptpython/key_bindings.py b/ptpython/key_bindings.py index f9b198c8..2589d351 100644 --- a/ptpython/key_bindings.py +++ b/ptpython/key_bindings.py @@ -144,15 +144,16 @@ def at_the_end(b): @handle('c-d', filter=~sidebar_visible & has_focus(python_input.default_buffer) & Condition(lambda: - # Only when the `confirm_exit` flag is set. - python_input.confirm_exit and - # And the current buffer is empty. + # The current buffer is empty. not get_app().current_buffer.text)) def _(event): """ Override Control-D exit, to ask for confirmation. """ - python_input.show_exit_confirmation = True + if python_input.confirm_exit: + python_input.show_exit_confirmation = True + else: + event.app.exit(exception=EOFError) @handle('c-c', filter=has_focus(python_input.default_buffer)) def _(event): From 0106ea65390467f3e4ea82b927256a7cca3db888 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sun, 14 Oct 2018 19:46:56 +0200 Subject: [PATCH 225/470] Fixed auto-suggest key bindings. --- ptpython/python_input.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 3c4058fb..2add83bb 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -8,6 +8,7 @@ from prompt_toolkit.application.run_in_terminal import run_coroutine_in_terminal from prompt_toolkit.auto_suggest import AutoSuggestFromHistory, ConditionalAutoSuggest, ThreadedAutoSuggest from prompt_toolkit.buffer import Buffer +from prompt_toolkit.key_binding.bindings.auto_suggest import load_auto_suggest_bindings from prompt_toolkit.completion import ThreadedCompleter from prompt_toolkit.document import Document from prompt_toolkit.enums import DEFAULT_BUFFER, EditingMode @@ -559,6 +560,7 @@ def _create_application(self): extra_toolbars=self._extra_toolbars), key_bindings=merge_key_bindings([ load_python_bindings(self), + load_auto_suggest_bindings(), load_sidebar_bindings(self), load_confirm_exit_bindings(self), # Extra key bindings should not be active when the sidebar is visible. From d570e83cefe2ce0f124236e22c72fd9cf89f50b5 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Mon, 15 Oct 2018 21:09:02 +0200 Subject: [PATCH 226/470] Correctly focus/unfocus sidebar. --- ptpython/key_bindings.py | 5 + ptpython/layout.py | 270 ++++++++++++++++++++------------------- ptpython/python_input.py | 19 +-- 3 files changed, 152 insertions(+), 142 deletions(-) diff --git a/ptpython/key_bindings.py b/ptpython/key_bindings.py index 2589d351..f124a918 100644 --- a/ptpython/key_bindings.py +++ b/ptpython/key_bindings.py @@ -53,6 +53,10 @@ def _(event): Show/hide sidebar. """ python_input.show_sidebar = not python_input.show_sidebar + if python_input.show_sidebar: + event.app.layout.focus(python_input.ptpython_layout.sidebar) + else: + event.app.layout.focus_last() @handle('f3') def _(event): @@ -211,6 +215,7 @@ def _(event): def _(event): " Hide sidebar. " python_input.show_sidebar = False + event.app.layout.focus_last() return bindings diff --git a/ptpython/layout.py b/ptpython/layout.py index e6399176..3cc230f0 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -28,7 +28,7 @@ import sys __all__ = ( - 'create_layout', + 'PtPythonLayout', 'CompletionVisualisation', ) @@ -480,139 +480,143 @@ def extra_condition(): filter=visible) -def create_layout(python_input, - lexer=PythonLexer, - extra_body=None, extra_toolbars=None, - extra_buffer_processors=None, input_buffer_height=None): - D = Dimension - extra_body = [extra_body] if extra_body else [] - extra_toolbars = extra_toolbars or [] - extra_buffer_processors = extra_buffer_processors or [] - input_buffer_height = input_buffer_height or D(min=6) - - search_toolbar = SearchToolbar(python_input.search_buffer) - - def create_python_input_window(): - def menu_position(): - """ - When there is no autocompletion menu to be shown, and we have a signature, - set the pop-up position at `bracket_start`. - """ - b = python_input.default_buffer - - if b.complete_state is None and python_input.signatures: - row, col = python_input.signatures[0].bracket_start - index = b.document.translate_row_col_to_index(row - 1, col) - return index - - return Window( - BufferControl( - buffer=python_input.default_buffer, - search_buffer_control=search_toolbar.control, - lexer=lexer, - include_default_input_processors=False, - input_processors=[ - ConditionalProcessor( - processor=HighlightIncrementalSearchProcessor(), - filter=has_focus(SEARCH_BUFFER) | has_focus(search_toolbar.control), - ), - HighlightSelectionProcessor(), - DisplayMultipleCursors(), - # Show matching parentheses, but only while editing. - ConditionalProcessor( - processor=HighlightMatchingBracketProcessor(chars='[](){}'), - filter=has_focus(DEFAULT_BUFFER) & ~is_done & - Condition(lambda: python_input.highlight_matching_parenthesis)), - ConditionalProcessor( - processor=AppendAutoSuggestion(), - filter=~is_done) - ] + extra_buffer_processors, - menu_position=menu_position, - - # Make sure that we always see the result of an reverse-i-search: - preview_search=True, - ), - left_margins=[PythonPromptMargin(python_input)], - # Scroll offsets. The 1 at the bottom is important to make sure the - # cursor is never below the "Press [Meta+Enter]" message which is a float. - scroll_offsets=ScrollOffsets(bottom=1, left=4, right=4), - # As long as we're editing, prefer a minimal height of 6. - height=(lambda: ( - None if get_app().is_done or python_input.show_exit_confirmation - else input_buffer_height)), - wrap_lines=Condition(lambda: python_input.wrap_lines), - ) - - root_container = HSplit([ - VSplit([ - HSplit([ - FloatContainer( - content=HSplit( - [create_python_input_window()] + extra_body - ), - floats=[ - Float(xcursor=True, - ycursor=True, - content=ConditionalContainer( - content=CompletionsMenu( - scroll_offset=( - lambda: python_input.completion_menu_scroll_offset), - max_height=12), - filter=show_completions_menu(python_input))), - Float(xcursor=True, - ycursor=True, - content=ConditionalContainer( - content=MultiColumnCompletionsMenu(), - filter=show_multi_column_completions_menu(python_input))), - Float(xcursor=True, - ycursor=True, - content=signature_toolbar(python_input)), - Float(left=2, - bottom=1, - content=exit_confirmation(python_input)), - Float(bottom=0, right=0, height=1, - content=meta_enter_message(python_input), - hide_when_covering_content=True), - Float(bottom=1, left=1, right=0, content=python_sidebar_help(python_input)), - ]), - ArgToolbar(), - search_toolbar, - SystemToolbar(), - ValidationToolbar(), - ConditionalContainer( - content=CompletionsToolbar(), - filter=show_completions_toolbar(python_input)), - - # Docstring region. - ConditionalContainer( - content=Window( - height=D.exact(1), - char='\u2500', - style='class:separator'), - filter=HasSignature(python_input) & ShowDocstring(python_input) & ~is_done), - ConditionalContainer( - content=Window( - BufferControl( - buffer=python_input.docstring_buffer, - lexer=SimpleLexer(style='class:docstring'), - #lexer=PythonLexer, +class PtPythonLayout(object): + def __init__(self, python_input, lexer=PythonLexer, extra_body=None, + extra_toolbars=None, extra_buffer_processors=None, + input_buffer_height=None): + D = Dimension + extra_body = [extra_body] if extra_body else [] + extra_toolbars = extra_toolbars or [] + extra_buffer_processors = extra_buffer_processors or [] + input_buffer_height = input_buffer_height or D(min=6) + + search_toolbar = SearchToolbar(python_input.search_buffer) + + def create_python_input_window(): + def menu_position(): + """ + When there is no autocompletion menu to be shown, and we have a + signature, set the pop-up position at `bracket_start`. + """ + b = python_input.default_buffer + + if b.complete_state is None and python_input.signatures: + row, col = python_input.signatures[0].bracket_start + index = b.document.translate_row_col_to_index(row - 1, col) + return index + + return Window( + BufferControl( + buffer=python_input.default_buffer, + search_buffer_control=search_toolbar.control, + lexer=lexer, + include_default_input_processors=False, + input_processors=[ + ConditionalProcessor( + processor=HighlightIncrementalSearchProcessor(), + filter=has_focus(SEARCH_BUFFER) | has_focus(search_toolbar.control), ), - height=D(max=12)), - filter=HasSignature(python_input) & ShowDocstring(python_input) & ~is_done), - ]), - ConditionalContainer( - content=HSplit([ - python_sidebar(python_input), - Window(style='class:sidebar,separator', height=1), - python_sidebar_navigation(python_input), + HighlightSelectionProcessor(), + DisplayMultipleCursors(), + # Show matching parentheses, but only while editing. + ConditionalProcessor( + processor=HighlightMatchingBracketProcessor(chars='[](){}'), + filter=has_focus(DEFAULT_BUFFER) & ~is_done & + Condition(lambda: python_input.highlight_matching_parenthesis)), + ConditionalProcessor( + processor=AppendAutoSuggestion(), + filter=~is_done) + ] + extra_buffer_processors, + menu_position=menu_position, + + # Make sure that we always see the result of an reverse-i-search: + preview_search=True, + ), + left_margins=[PythonPromptMargin(python_input)], + # Scroll offsets. The 1 at the bottom is important to make sure + # the cursor is never below the "Press [Meta+Enter]" message + # which is a float. + scroll_offsets=ScrollOffsets(bottom=1, left=4, right=4), + # As long as we're editing, prefer a minimal height of 6. + height=(lambda: ( + None if get_app().is_done or python_input.show_exit_confirmation + else input_buffer_height)), + wrap_lines=Condition(lambda: python_input.wrap_lines), + ) + + sidebar = python_sidebar(python_input) + + root_container = HSplit([ + VSplit([ + HSplit([ + FloatContainer( + content=HSplit( + [create_python_input_window()] + extra_body + ), + floats=[ + Float(xcursor=True, + ycursor=True, + content=ConditionalContainer( + content=CompletionsMenu( + scroll_offset=( + lambda: python_input.completion_menu_scroll_offset), + max_height=12), + filter=show_completions_menu(python_input))), + Float(xcursor=True, + ycursor=True, + content=ConditionalContainer( + content=MultiColumnCompletionsMenu(), + filter=show_multi_column_completions_menu(python_input))), + Float(xcursor=True, + ycursor=True, + content=signature_toolbar(python_input)), + Float(left=2, + bottom=1, + content=exit_confirmation(python_input)), + Float(bottom=0, right=0, height=1, + content=meta_enter_message(python_input), + hide_when_covering_content=True), + Float(bottom=1, left=1, right=0, content=python_sidebar_help(python_input)), + ]), + ArgToolbar(), + search_toolbar, + SystemToolbar(), + ValidationToolbar(), + ConditionalContainer( + content=CompletionsToolbar(), + filter=show_completions_toolbar(python_input)), + + # Docstring region. + ConditionalContainer( + content=Window( + height=D.exact(1), + char='\u2500', + style='class:separator'), + filter=HasSignature(python_input) & ShowDocstring(python_input) & ~is_done), + ConditionalContainer( + content=Window( + BufferControl( + buffer=python_input.docstring_buffer, + lexer=SimpleLexer(style='class:docstring'), + #lexer=PythonLexer, + ), + height=D(max=12)), + filter=HasSignature(python_input) & ShowDocstring(python_input) & ~is_done), ]), - filter=ShowSidebar(python_input) & ~is_done) - ]), - ] + extra_toolbars + [ - VSplit([ - status_bar(python_input), - show_sidebar_button_info(python_input), + ConditionalContainer( + content=HSplit([ + sidebar, + Window(style='class:sidebar,separator', height=1), + python_sidebar_navigation(python_input), + ]), + filter=ShowSidebar(python_input) & ~is_done) + ]), + ] + extra_toolbars + [ + VSplit([ + status_bar(python_input), + show_sidebar_button_info(python_input), + ]) ]) - ]) - return Layout(root_container) + self.layout = Layout(root_container) + self.sidebar = sidebar diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 2add83bb..7777e324 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -28,7 +28,7 @@ from .completer import PythonCompleter from .history_browser import History from .key_bindings import load_python_bindings, load_sidebar_bindings, load_confirm_exit_bindings -from .layout import create_layout, CompletionVisualisation +from .layout import PtPythonLayout, CompletionVisualisation from .prompt_style import IPythonPrompt, ClassicPrompt from .style import get_all_code_styles, get_all_ui_styles, generate_style from .utils import get_jedi_script_from_document @@ -260,6 +260,14 @@ def __init__(self, lambda: self.min_brightness, lambda: self.max_brightness), ]) + self.ptpython_layout = PtPythonLayout( + self, + lexer=DynamicLexer( + lambda: self._lexer if self.enable_syntax_highlighting else SimpleLexer()), + input_buffer_height=self._input_buffer_height, + extra_buffer_processors=self._extra_buffer_processors, + extra_body=self._extra_layout_body, + extra_toolbars=self._extra_toolbars) self.app = self._create_application() @@ -550,14 +558,7 @@ def _create_application(self): return Application( input=self.input, output=self.output, - layout=create_layout( - self, - lexer=DynamicLexer( - lambda: self._lexer if self.enable_syntax_highlighting else SimpleLexer()), - input_buffer_height=self._input_buffer_height, - extra_buffer_processors=self._extra_buffer_processors, - extra_body=self._extra_layout_body, - extra_toolbars=self._extra_toolbars), + layout=self.ptpython_layout.layout, key_bindings=merge_key_bindings([ load_python_bindings(self), load_auto_suggest_bindings(), From 0b97478e6f6ee3cb0abdd7d99ffe6f0f9c7a9a4c Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Mon, 22 Oct 2018 21:23:31 +0200 Subject: [PATCH 227/470] Fixed open_in_editor and suspend key bindings. --- ptpython/key_bindings.py | 8 ++++++++ ptpython/python_input.py | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/ptpython/key_bindings.py b/ptpython/key_bindings.py index f124a918..001f59b9 100644 --- a/ptpython/key_bindings.py +++ b/ptpython/key_bindings.py @@ -47,6 +47,14 @@ def _(event): """ event.app.renderer.clear() + @handle('c-z') + def _(event): + """ + Suspend. + """ + if python_input.enable_system_bindings: + event.app.suspend_to_background() + @handle('f2') def _(event): """ diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 7777e324..6de5180a 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -9,6 +9,7 @@ from prompt_toolkit.auto_suggest import AutoSuggestFromHistory, ConditionalAutoSuggest, ThreadedAutoSuggest from prompt_toolkit.buffer import Buffer from prompt_toolkit.key_binding.bindings.auto_suggest import load_auto_suggest_bindings +from prompt_toolkit.key_binding.bindings.open_in_editor import load_open_in_editor_bindings from prompt_toolkit.completion import ThreadedCompleter from prompt_toolkit.document import Document from prompt_toolkit.enums import DEFAULT_BUFFER, EditingMode @@ -564,6 +565,9 @@ def _create_application(self): load_auto_suggest_bindings(), load_sidebar_bindings(self), load_confirm_exit_bindings(self), + ConditionalKeyBindings( + load_open_in_editor_bindings(), + Condition(lambda: self.enable_open_in_editor)), # Extra key bindings should not be active when the sidebar is visible. ConditionalKeyBindings( self.extra_key_bindings, From 23e15c815096fb1d699ffee0891649b27af24b9d Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 30 Oct 2018 22:31:57 +0100 Subject: [PATCH 228/470] Fixed Control-C key binding for ptipython. --- ptpython/ipython.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ptpython/ipython.py b/ptpython/ipython.py index ad87f8f4..be4bd178 100644 --- a/ptpython/ipython.py +++ b/ptpython/ipython.py @@ -217,7 +217,11 @@ def get_globals(): self.python_input = python_input def prompt_for_code(self): - return self.python_input.app.run() + try: + return self.python_input.app.run() + except KeyboardInterrupt: + self.python_input.default_buffer.document = Document() + return '' def initialize_extensions(shell, extensions): From c45fc89e5852f28365c6a87f4b501feeebc27e58 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 30 Oct 2018 22:34:30 +0100 Subject: [PATCH 229/470] Release 2.0.4 --- CHANGELOG | 11 +++++++++++ setup.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 8954fbdc..d8fcd0aa 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,17 @@ CHANGELOG ========= +2.0.4: 2018-10-30 +----------------- + +- Fixed ptipython. +- Fixed config: setting of color depth. +- Fixed auto-suggest key bindings. +- Fixed Control-D key binding for exiting REPL when (confirm_exit=False). +- Correctly focus/unfocus sidebar. +- Fixed open_in_editor and suspend key bindings. + + 2.0.3: 2018-10-12 ----------------- diff --git a/setup.py b/setup.py index fe536d87..88584f29 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ setup( name='ptpython', author='Jonathan Slenders', - version='2.0.3', + version='2.0.4', url='https://github.com/jonathanslenders/ptpython', description='Python REPL build on top of prompt_toolkit', long_description=long_description, From 7408355a19565cb7bbc464a4e14012eccc944307 Mon Sep 17 00:00:00 2001 From: Roman Yurchak Date: Thu, 29 Nov 2018 18:35:52 +0100 Subject: [PATCH 230/470] Python 3.7 is supported --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 47a9e0eb..c14a239f 100644 --- a/README.rst +++ b/README.rst @@ -12,7 +12,7 @@ ptpython |Build Status| Ptpython is an advanced Python REPL. It should work on all -Python versions from 2.6 up to 3.5 and work cross platform (Linux, +Python versions from 2.6 up to 3.7 and work cross platform (Linux, BSD, OS X and Windows). From 5a463320252f2e01b74f668eff01a021d781f140 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Mon, 7 Jan 2019 10:45:58 +0100 Subject: [PATCH 231/470] Fix ResourceWarning: unclosed file in setup.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Mickaël Schoentgen --- setup.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/setup.py b/setup.py index 88584f29..01868e8d 100644 --- a/setup.py +++ b/setup.py @@ -3,12 +3,8 @@ import sys from setuptools import setup, find_packages -long_description = open( - os.path.join( - os.path.dirname(__file__), - 'README.rst' - ) -).read() +with open(os.path.join(os.path.dirname(__file__), 'README.rst')) as f: + long_description = f.read() setup( From f9ed1d5e900fe3df8c80b0b9082de72c82b90099 Mon Sep 17 00:00:00 2001 From: "Derek A. Thomas" Date: Tue, 5 Feb 2019 11:03:41 -0800 Subject: [PATCH 232/470] fix patching stdout in embedded python repl --- ptpython/repl.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ptpython/repl.py b/ptpython/repl.py index c480869a..250c9705 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -20,6 +20,7 @@ from prompt_toolkit.shortcuts import set_title, clear_title from prompt_toolkit.shortcuts import print_formatted_text from prompt_toolkit.formatted_text import PygmentsTokens +from prompt_toolkit.patch_stdout import patch_stdout as patch_stdout_context from .python_input import PythonInput from .eventloop import inputhook @@ -331,7 +332,7 @@ def get_locals(): app = repl.app # Start repl. - patch_context = app.patch_stdout_context() if patch_stdout else DummyContext() + patch_context = patch_stdout_context() if patch_stdout else DummyContext() if return_asyncio_coroutine: # XXX def coroutine(): From 87bb1e3854128ab754c96e15be8fd0d5f75102a6 Mon Sep 17 00:00:00 2001 From: "Derek A. Thomas" Date: Tue, 5 Feb 2019 15:49:55 -0800 Subject: [PATCH 233/470] fix embed coroutine for asyncio repl - maintains compatibility with py2 syntax --- ptpython/repl.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/ptpython/repl.py b/ptpython/repl.py index 250c9705..e6091738 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -337,8 +337,20 @@ def get_locals(): if return_asyncio_coroutine: # XXX def coroutine(): with patch_context: - for future in app.run_async(): - yield future + while True: + iterator = iter(app.run_async().to_asyncio_future()) + try: + while True: + yield next(iterator) + except StopIteration as exc: + if exc.args: + text = exc.args[0] + else: + text = None + try: + repl._process_text(text) + except EOFError: + return return coroutine() else: with patch_context: From f68576dc5fade39376d381b3d3d8bb3c1fd79b23 Mon Sep 17 00:00:00 2001 From: "Derek A. Thomas" Date: Tue, 5 Feb 2019 15:51:24 -0800 Subject: [PATCH 234/470] fix capturing EOFError in asyncio-python-embed example --- examples/asyncio-python-embed.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/examples/asyncio-python-embed.py b/examples/asyncio-python-embed.py index 280d4b52..fef19b7f 100755 --- a/examples/asyncio-python-embed.py +++ b/examples/asyncio-python-embed.py @@ -38,10 +38,11 @@ def interactive_shell(): counter variable. """ print('You should be able to read and update the "counter[0]" variable from this shell.') - yield from embed(globals=globals(), return_asyncio_coroutine=True, patch_stdout=True) - - # Stop the loop when quitting the repl. (Ctrl-D press.) - loop.stop() + try: + yield from embed(globals=globals(), return_asyncio_coroutine=True, patch_stdout=True) + except EOFError: + # Stop the loop when quitting the repl. (Ctrl-D press.) + loop.stop() def main(): From 14c8564fbb1a6ca74586b15d676173ab0fa59b7d Mon Sep 17 00:00:00 2001 From: "Derek A. Thomas" Date: Tue, 5 Feb 2019 15:59:57 -0800 Subject: [PATCH 235/470] reduce some extra conditions in embedded repl coroutine that are never reached --- ptpython/repl.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/ptpython/repl.py b/ptpython/repl.py index e6091738..9ca8ffaf 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -343,14 +343,8 @@ def coroutine(): while True: yield next(iterator) except StopIteration as exc: - if exc.args: - text = exc.args[0] - else: - text = None - try: - repl._process_text(text) - except EOFError: - return + text = exc.args[0] + repl._process_text(text) return coroutine() else: with patch_context: From b1bba26a491324cd65e0ef46c7b818c4b88fd993 Mon Sep 17 00:00:00 2001 From: Miroslav Shubernetskiy Date: Fri, 2 Nov 2018 11:30:02 -0400 Subject: [PATCH 236/470] small fix in example config --- examples/ptpython_config/config.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/examples/ptpython_config/config.py b/examples/ptpython_config/config.py index 9c316cba..a834112c 100644 --- a/examples/ptpython_config/config.py +++ b/examples/ptpython_config/config.py @@ -132,9 +132,7 @@ def _(event): """ @repl.add_key_binding(Keys.ControlE, Keys.ControlE) def _(event): - b = event.current_buffer - if b.accept_action.is_returnable: - b.accept_action.validate_and_handle(event.cli, b) + event.current_buffer.validate_and_handle() """ From 686d308acc8190c9462731d75f91856067cb5161 Mon Sep 17 00:00:00 2001 From: zach valenta Date: Mon, 4 Mar 2019 06:47:16 -0500 Subject: [PATCH 237/470] fix typo --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index c14a239f..b953b02d 100644 --- a/README.rst +++ b/README.rst @@ -39,7 +39,7 @@ Features - Support for `bracketed paste `_ [2]. - Both Vi and Emacs key bindings. - Support for double width (Chinese) characters. -- ... and many other stuff. +- ... and many other things. [1] Disabled by default. (Enable in the menu.) From b3a77c732dba926300ef59498361fa2ad0598427 Mon Sep 17 00:00:00 2001 From: NightMachinary <36224762+NightMachinary@users.noreply.github.com> Date: Thu, 15 Aug 2019 21:06:54 +0430 Subject: [PATCH 238/470] Fixed a typo --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index b953b02d..e65c98ec 100644 --- a/README.rst +++ b/README.rst @@ -121,7 +121,7 @@ Running system commands: Press ``Meta-!`` in Emacs mode or just ``!`` in Vi navigation mode to see the "Shell command" prompt. There you can enter system commands without leaving the REPL. -Selecting text: Press ``Control+Space`` in Emacs mode on ``V`` (major V) in Vi +Selecting text: Press ``Control+Space`` in Emacs mode or ``V`` (major V) in Vi navigation mode. From 0524be1610c383df5f25f1b28097e1722022cf8f Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Mon, 30 Sep 2019 23:10:51 +0100 Subject: [PATCH 239/470] enable universal wheels --- setup.cfg | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 setup.cfg diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 00000000..3c6e79cf --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[bdist_wheel] +universal=1 From 5eb7158114a80efcb50e74ece16572a9b3337f0d Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Mon, 30 Sep 2019 23:11:07 +0100 Subject: [PATCH 240/470] add python3 trove classifier for caniusepython3 --- setup.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/setup.py b/setup.py index 01868e8d..be25c433 100644 --- a/setup.py +++ b/setup.py @@ -21,6 +21,11 @@ 'prompt_toolkit>=2.0.6,<2.1.0', 'pygments', ], + classifiers=[ + 'Programming Language :: Python', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 2', + ], entry_points={ 'console_scripts': [ 'ptpython = ptpython.entry_points.run_ptpython:run', From 284b38ff7d767f1bf4772765bb0cc364337398d9 Mon Sep 17 00:00:00 2001 From: "Derek A. Thomas" Date: Wed, 13 Feb 2019 11:08:42 -0800 Subject: [PATCH 241/470] change PythonRepl._process_text to use input text instead of ignoring it --- ptpython/repl.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ptpython/repl.py b/ptpython/repl.py index 9ca8ffaf..72487ad7 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -76,8 +76,7 @@ def run(self): if self.terminal_title: clear_title() - def _process_text(self, text): - line = self.default_buffer.text + def _process_text(self, line): if line and not line.isspace(): try: From 19416eef9d55fd457f9a47a170f2222885b21236 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 8 Oct 2019 22:53:13 +0200 Subject: [PATCH 242/470] Improved autocompletion: - Added fuzzy completion. - Added dictionary key completion (for keys which are strings). - Highlighting of Python keywords in completion drop down. --- examples/ptpython_config/config.py | 4 + ptpython/completer.py | 125 ++++++++++++++++++++++++++++- ptpython/python_input.py | 25 +++++- ptpython/style.py | 7 ++ setup.py | 2 +- 5 files changed, 157 insertions(+), 6 deletions(-) diff --git a/examples/ptpython_config/config.py b/examples/ptpython_config/config.py index a834112c..28e7c0bf 100644 --- a/examples/ptpython_config/config.py +++ b/examples/ptpython_config/config.py @@ -61,6 +61,10 @@ def configure(repl): # completion menu is shown.) repl.complete_while_typing = True + # Fuzzy and dictionary completion. + self.enable_fuzzy_completion = False + self.enable_dictionary_completion = False + # Vi mode. repl.vi_mode = False diff --git a/ptpython/completer.py b/ptpython/completer.py index 7a63912a..8fa0e314 100644 --- a/ptpython/completer.py +++ b/ptpython/completer.py @@ -6,7 +6,10 @@ from ptpython.utils import get_jedi_script_from_document +import keyword +import ast import re +import six __all__ = ( 'PythonCompleter', @@ -17,11 +20,14 @@ class PythonCompleter(Completer): """ Completer for Python code. """ - def __init__(self, get_globals, get_locals): + def __init__(self, get_globals, get_locals, get_enable_dictionary_completion): super(PythonCompleter, self).__init__() self.get_globals = get_globals self.get_locals = get_locals + self.get_enable_dictionary_completion = get_enable_dictionary_completion + + self.dictionary_completer = DictionaryCompleter(get_globals, get_locals) self._path_completer_cache = None self._path_completer_grammar_cache = None @@ -108,7 +114,16 @@ def get_completions(self, document, complete_event): """ Get Python completions. """ - # Do Path completions + # Do dictionary key completions. + if self.get_enable_dictionary_completion(): + has_dict_completions = False + for c in self.dictionary_completer.get_completions(document, complete_event): + has_dict_completions = True + yield c + if has_dict_completions: + return + + # Do Path completions (if there were no dictionary completions). if complete_event.completion_requested or self._complete_path_while_typing(document): for c in self._path_completer.get_completions(document, complete_event): yield c @@ -162,5 +177,107 @@ def get_completions(self, document, complete_event): pass else: for c in completions: - yield Completion(c.name_with_symbols, len(c.complete) - len(c.name_with_symbols), - display=c.name_with_symbols) + yield Completion( + c.name_with_symbols, len(c.complete) - len(c.name_with_symbols), + display=c.name_with_symbols, + style=_get_style_for_name(c.name_with_symbols)) + + +class DictionaryCompleter(Completer): + """ + Experimental completer for Python dictionary keys. + + Warning: This does an `eval` on the Python object before the open square + bracket, which is potentially dangerous. It doesn't match on + function calls, so it only triggers attribute access. + """ + def __init__(self, get_globals, get_locals): + super(DictionaryCompleter, self).__init__() + + self.get_globals = get_globals + self.get_locals = get_locals + + self.pattern = re.compile( + r''' + # Any expression safe enough to eval while typing. + # No operators, except dot, and only other dict lookups. + # Technically, this can be unsafe of course, if bad code runs + # in `__getattr__` or ``__getitem__``. + ( + # Variable name + [a-zA-Z0-9_]+ + + \s* + + (?: + # Attribute access. + \s* \. \s* [a-zA-Z0-9_]+ \s* + + | + + # Item lookup. + # (We match the square brackets. We don't care about + # matching quotes here in the regex. Nested square + # brackets are not supported.) + \s* \[ [a-zA-Z0-9_'"\s]+ \] \s* + )* + ) + + # Dict loopup to complete (square bracket open + start of + # string). + \[ + \s* ([a-zA-Z0-9_'"]*)$ + ''', + re.VERBOSE + ) + + def get_completions(self, document, complete_event): + match = self.pattern.search(document.text_before_cursor) + if match is not None: + object_var, key = match.groups() + object_var = object_var.strip() + + # Do lookup of `object_var` in the context. + try: + result = eval(object_var, self.get_globals(), self.get_locals()) + except BaseException as e: + return # Many exception, like NameError can be thrown here. + + # If this object is a dictionary, complete the keys. + if isinstance(result, dict): + # Try to evaluate the key. + key_obj = key + for k in [key, key + '"', key + "'"]: + try: + key_obj = ast.literal_eval(k) + except (SyntaxError, ValueError): + continue + else: + break + + for k in result: + if six.text_type(k).startswith(key_obj): + yield Completion( + six.text_type(repr(k)), + - len(key), + display=six.text_type(repr(k)) + ) + +try: + import builtins + _builtin_names = dir(builtins) +except ImportError: # Python 2. + _builtin_names = [] + + +def _get_style_for_name(name): + """ + Return completion style to use for this name. + """ + if name in _builtin_names: + return 'class:completion.builtin' + + if keyword.iskeyword(name): + return 'class:completion.keyword' + + return '' diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 6de5180a..2101657e 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -25,6 +25,7 @@ from prompt_toolkit.styles import DynamicStyle, SwapLightAndDarkStyleTransformation, ConditionalStyleTransformation, AdjustBrightnessStyleTransformation, merge_style_transformations from prompt_toolkit.utils import is_windows from prompt_toolkit.validation import ConditionalValidator +from prompt_toolkit.completion import FuzzyCompleter from .completer import PythonCompleter from .history_browser import History @@ -151,7 +152,10 @@ def __init__(self, self.get_globals = get_globals or (lambda: {}) self.get_locals = get_locals or self.get_globals - self._completer = _completer or PythonCompleter(self.get_globals, self.get_locals) + self._completer = _completer or FuzzyCompleter( + PythonCompleter(self.get_globals, self.get_locals, + lambda: self.enable_dictionary_completion), + enable_fuzzy=Condition(lambda: self.enable_fuzzy_completion)) self._validator = _validator or PythonValidator(self.get_compiler_flags) self._lexer = _lexer or PygmentsLexer(PythonLexer) @@ -193,6 +197,8 @@ def __init__(self, # with the current input. self.enable_syntax_highlighting = True + self.enable_fuzzy_completion = False + self.enable_dictionary_completion = False self.swap_light_and_dark = False self.highlight_matching_parenthesis = False self.show_sidebar = False # Currently show the sidebar. @@ -433,6 +439,23 @@ def get_values(): 'on': lambda: enable('complete_while_typing') and disable('enable_history_search'), 'off': lambda: disable('complete_while_typing'), }), + Option(title='Enable fuzzy completion', + description="Enable fuzzy completion.", + get_current_value=lambda: ['off', 'on'][self.enable_fuzzy_completion], + get_values=lambda: { + 'on': lambda: enable('enable_fuzzy_completion'), + 'off': lambda: disable('enable_fuzzy_completion'), + }), + Option(title='Dictionary completion', + description='Enable experimental dictionary completion.\n' + 'WARNING: this does "eval" on fragments of\n' + ' your Python input and is\n' + ' potentially unsafe.', + get_current_value=lambda: ['off', 'on'][self.enable_dictionary_completion], + get_values=lambda: { + 'on': lambda: enable('enable_dictionary_completion'), + 'off': lambda: disable('enable_dictionary_completion'), + }), Option(title='History search', description='When pressing the up-arrow, filter the history on input starting ' 'with the current text. (Not compatible with "Complete while typing".)', diff --git a/ptpython/style.py b/ptpython/style.py index 15c5b2ad..7a2cd2a1 100644 --- a/ptpython/style.py +++ b/ptpython/style.py @@ -83,6 +83,13 @@ def generate_style(python_style, ui_style): 'out': '#ff0000', 'out.number': '#ff0000', + # Completions. + 'completion.builtin': '', + 'completion.keyword': 'fg:#008800', + + 'completion.keyword fuzzymatch.inside': 'fg:#008800', + 'completion.keyword fuzzymatch.outside': 'fg:#44aa44', + # Separator between windows. (Used above docstring.) 'separator': '#bbbbbb', diff --git a/setup.py b/setup.py index be25c433..da16fe41 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,7 @@ install_requires = [ 'docopt', 'jedi>=0.9.0', - 'prompt_toolkit>=2.0.6,<2.1.0', + 'prompt_toolkit>=2.0.8,<2.1.0', 'pygments', ], classifiers=[ From 6edce3c3d31a5c3f5c071e1a88d62d51a11c42d1 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Wed, 9 Oct 2019 17:05:57 +0100 Subject: [PATCH 243/470] Release 2.0.5 --- CHANGELOG | 16 ++++++++++++++++ setup.py | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index d8fcd0aa..c64a87d1 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,22 @@ CHANGELOG ========= + +2.0.5: 2019-10-09 +----------------- + +New features: +- Added dictionary completer (off by default). +- Added fuzzy completion (off by default). +- Highlight keywords in completion dropdown menu. +- Enable universal wheels. + +Fixes: +- Fixed embedding repl as asyncio coroutine. +- Fixed patching stdout in embedded repl. +- Fixed ResourceWarning in setup.py. + + 2.0.4: 2018-10-30 ----------------- diff --git a/setup.py b/setup.py index da16fe41..cac87b50 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ setup( name='ptpython', author='Jonathan Slenders', - version='2.0.4', + version='2.0.5', url='https://github.com/jonathanslenders/ptpython', description='Python REPL build on top of prompt_toolkit', long_description=long_description, From 74f7623fb2a93c06bf605dd77fd1c104cac1202e Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 10 Oct 2019 16:29:26 +0200 Subject: [PATCH 244/470] Fix 'get_enable_dictionary_completion' argument in ptipython. --- ptpython/ipython.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ptpython/ipython.py b/ptpython/ipython.py index be4bd178..8cc5a36e 100644 --- a/ptpython/ipython.py +++ b/ptpython/ipython.py @@ -90,11 +90,12 @@ def create_ipython_grammar(): """) -def create_completer(get_globals, get_locals, magics_manager, alias_manager): +def create_completer(get_globals, get_locals, magics_manager, alias_manager, + get_enable_dictionary_completion): g = create_ipython_grammar() return GrammarCompleter(g, { - 'python': PythonCompleter(get_globals, get_locals), + 'python': PythonCompleter(get_globals, get_locals, get_enable_dictionary_completion), 'magic': MagicsCompleter(magics_manager), 'alias_name': AliasCompleter(alias_manager), 'pdb_arg': WordCompleter(['on', 'off'], ignore_case=True), @@ -154,7 +155,8 @@ class IPythonInput(PythonInput): def __init__(self, ipython_shell, *a, **kw): kw['_completer'] = create_completer(kw['get_globals'], kw['get_globals'], ipython_shell.magics_manager, - ipython_shell.alias_manager) + ipython_shell.alias_manager, + lambda: self.enable_dictionary_completion) kw['_lexer'] = create_lexer() kw['_validator'] = IPythonValidator( get_compiler_flags=self.get_compiler_flags) From acc75aa5e8e4698ac86ecbee4f35c747897e634e Mon Sep 17 00:00:00 2001 From: Tim Savage Date: Wed, 16 Oct 2019 12:48:29 +1100 Subject: [PATCH 245/470] Fixed variable from self to repl The config entries for "Fuzzy and dictionary completion" was using self instead of repl. --- examples/ptpython_config/config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/ptpython_config/config.py b/examples/ptpython_config/config.py index 28e7c0bf..6fb0c54f 100644 --- a/examples/ptpython_config/config.py +++ b/examples/ptpython_config/config.py @@ -62,8 +62,8 @@ def configure(repl): repl.complete_while_typing = True # Fuzzy and dictionary completion. - self.enable_fuzzy_completion = False - self.enable_dictionary_completion = False + repl.enable_fuzzy_completion = False + repl.enable_dictionary_completion = False # Vi mode. repl.vi_mode = False From c69f0486ea606c7cb3d5009f68cb73022c6355d2 Mon Sep 17 00:00:00 2001 From: Tim Savage Date: Wed, 16 Oct 2019 12:51:12 +1100 Subject: [PATCH 246/470] Added help for windows users to find config location --- examples/ptpython_config/config.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/ptpython_config/config.py b/examples/ptpython_config/config.py index 6fb0c54f..bd18a563 100644 --- a/examples/ptpython_config/config.py +++ b/examples/ptpython_config/config.py @@ -1,7 +1,9 @@ """ Configuration example for ``ptpython``. -Copy this file to ~/.ptpython/config.py +Copy this file to ~/.ptpython/config.py on windows use +`os.path.expanduser("~/.ptpython/config.py")` to find the +correct location. """ from __future__ import unicode_literals from prompt_toolkit.filters import ViInsertMode From 424aa34645cf69b1adb39d633a569abfe2e7593c Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Mon, 28 Oct 2019 17:53:45 +0100 Subject: [PATCH 247/470] Fixed exec call, for when a filename was passed to ptpython. Pass the namespace explicitly. --- ptpython/entry_points/run_ptpython.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ptpython/entry_points/run_ptpython.py b/ptpython/entry_points/run_ptpython.py index 356d6bd3..b7701b99 100644 --- a/ptpython/entry_points/run_ptpython.py +++ b/ptpython/entry_points/run_ptpython.py @@ -55,7 +55,9 @@ def run(): path = a[''][0] with open(path, 'rb') as f: code = compile(f.read(), path, 'exec') - six.exec_(code) + # NOTE: We have to pass an empty dictionary as namespace. Omitting + # this argument causes imports to not be found. See issue #326. + six.exec_(code, {}) # Run interactive shell. else: From 90c1dad98465d2da013a9ea5e34a29b95af8c687 Mon Sep 17 00:00:00 2001 From: Carl George Date: Sun, 19 Mar 2017 21:59:52 -0500 Subject: [PATCH 248/470] implement XDG Base Directory specification * Use the appdirs module. * Print a warning if the legacy ~/.ptpython directory is detected. * Resolves #63. * Resolves #132. --- README.rst | 2 +- examples/ptpython_config/config.py | 4 +--- ptpython/entry_points/run_ptipython.py | 27 +++++++++++++++++++------- ptpython/entry_points/run_ptpython.py | 27 +++++++++++++++++++------- ptpython/python_input.py | 2 +- ptpython/repl.py | 2 +- setup.py | 1 + 7 files changed, 45 insertions(+), 20 deletions(-) diff --git a/README.rst b/README.rst index e65c98ec..f394054d 100644 --- a/README.rst +++ b/README.rst @@ -128,7 +128,7 @@ navigation mode. Configuration ************* -It is possible to create a ``~/.ptpython/config.py`` file to customize the configuration. +It is possible to create a ``$XDG_CONFIG_HOME/ptpython/config.py`` file to customize the configuration. Have a look at this example to see what is possible: `config.py `_ diff --git a/examples/ptpython_config/config.py b/examples/ptpython_config/config.py index bd18a563..0296727a 100644 --- a/examples/ptpython_config/config.py +++ b/examples/ptpython_config/config.py @@ -1,9 +1,7 @@ """ Configuration example for ``ptpython``. -Copy this file to ~/.ptpython/config.py on windows use -`os.path.expanduser("~/.ptpython/config.py")` to find the -correct location. +Copy this file to $XDG_CONFIG_HOME/ptpython/config.py """ from __future__ import unicode_literals from prompt_toolkit.filters import ViInsertMode diff --git a/ptpython/entry_points/run_ptipython.py b/ptpython/entry_points/run_ptipython.py index a563f52e..a541ddc5 100644 --- a/ptpython/entry_points/run_ptipython.py +++ b/ptpython/entry_points/run_ptipython.py @@ -9,11 +9,12 @@ Options: --vi : Use Vi keybindings instead of Emacs bindings. - --config-dir= : Pass config directory. By default '~/.ptpython/'. + --config-dir= : Pass config directory. By default '$XDG_CONFIG_HOME/ptpython'. -i, --interactive= : Start interactive shell after executing this file. """ -from __future__ import absolute_import, unicode_literals +from __future__ import absolute_import, unicode_literals, print_function +import appdirs import docopt import os import six @@ -24,11 +25,23 @@ def run(user_ns=None): a = docopt.docopt(__doc__) vi_mode = bool(a['--vi']) - config_dir = os.path.expanduser(a['--config-dir'] or '~/.ptpython/') - # Create config directory. - if not os.path.isdir(config_dir): - os.mkdir(config_dir) + config_dir = appdirs.user_config_dir('ptpython', 'Jonathan Slenders') + data_dir = appdirs.user_data_dir('ptpython', 'Jonathan Slenders') + + if a['--config-dir']: + # Override config_dir. + config_dir = os.path.expanduser(a['--config-dir']) + else: + # Warn about the legacy directory. + legacy_dir = os.path.expanduser('~/.ptpython') + if os.path.isdir(legacy_dir): + print('{0} is deprecated, migrate your configuration to {1}'.format(legacy_dir, config_dir)) + + # Create directories. + for d in (config_dir, data_dir): + if not os.path.isdir(d) and not os.path.islink(d): + os.mkdir(d) # If IPython is not available, show message and exit here with error status # code. @@ -89,7 +102,7 @@ def configure(repl): # Run interactive shell. embed(vi_mode=vi_mode, - history_filename=os.path.join(config_dir, 'history'), + history_filename=os.path.join(data_dir, 'history'), configure=configure, user_ns=user_ns, title='IPython REPL (ptipython)') diff --git a/ptpython/entry_points/run_ptpython.py b/ptpython/entry_points/run_ptpython.py index b7701b99..e092b24e 100644 --- a/ptpython/entry_points/run_ptpython.py +++ b/ptpython/entry_points/run_ptpython.py @@ -9,14 +9,15 @@ Options: --vi : Use Vi keybindings instead of Emacs bindings. - --config-dir= : Pass config directory. By default '~/.ptpython/'. + --config-dir= : Pass config directory. By default '$XDG_CONFIG_HOME/ptpython'. -i, --interactive= : Start interactive shell after executing this file. Other environment variables: PYTHONSTARTUP: file executed on interactive startup (no default) """ -from __future__ import absolute_import, unicode_literals +from __future__ import absolute_import, unicode_literals, print_function +import appdirs import docopt import os import six @@ -29,11 +30,23 @@ def run(): a = docopt.docopt(__doc__) vi_mode = bool(a['--vi']) - config_dir = os.path.expanduser(a['--config-dir'] or '~/.ptpython/') - # Create config directory. - if not os.path.isdir(config_dir): - os.mkdir(config_dir) + config_dir = appdirs.user_config_dir('ptpython', 'Jonathan Slenders') + data_dir = appdirs.user_data_dir('ptpython', 'Jonathan Slenders') + + if a['--config-dir']: + # Override config_dir. + config_dir = os.path.expanduser(a['--config-dir']) + else: + # Warn about the legacy directory. + legacy_dir = os.path.expanduser('~/.ptpython') + if os.path.isdir(legacy_dir): + print('{0} is deprecated, migrate your configuration to {1}'.format(legacy_dir, config_dir)) + + # Create directories. + for d in (config_dir, data_dir): + if not os.path.isdir(d) and not os.path.islink(d): + os.mkdir(d) # Startup path startup_paths = [] @@ -71,7 +84,7 @@ def configure(repl): import __main__ embed(vi_mode=vi_mode, - history_filename=os.path.join(config_dir, 'history'), + history_filename=os.path.join(data_dir, 'history'), configure=configure, locals=__main__.__dict__, globals=__main__.__dict__, diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 2101657e..2c855ba9 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -320,7 +320,7 @@ def get_compiler_flags(self): def add_key_binding(self): """ Shortcut for adding new key bindings. - (Mostly useful for a .ptpython/config.py file, that receives + (Mostly useful for a config.py file, that receives a PythonInput/Repl instance as input.) :: diff --git a/ptpython/repl.py b/ptpython/repl.py index 72487ad7..83cecce1 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -240,7 +240,7 @@ def enable_deprecation_warnings(): module='__main__') -def run_config(repl, config_file='~/.ptpython/config.py'): +def run_config(repl, config_file): """ Execute REPL config file. diff --git a/setup.py b/setup.py index cac87b50..e884f3c4 100644 --- a/setup.py +++ b/setup.py @@ -16,6 +16,7 @@ long_description=long_description, packages=find_packages('.'), install_requires = [ + 'appdirs', 'docopt', 'jedi>=0.9.0', 'prompt_toolkit>=2.0.8,<2.1.0', From b8a7abc402b0854bb9b8e53d8a33bdedbbba5db4 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 26 Nov 2019 17:32:46 +0100 Subject: [PATCH 249/470] Use prompt_toolkit instead of 'Jonathan Slenders' as appauthor. --- ptpython/entry_points/run_ptipython.py | 4 ++-- ptpython/entry_points/run_ptpython.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ptpython/entry_points/run_ptipython.py b/ptpython/entry_points/run_ptipython.py index a541ddc5..67239ce7 100644 --- a/ptpython/entry_points/run_ptipython.py +++ b/ptpython/entry_points/run_ptipython.py @@ -26,8 +26,8 @@ def run(user_ns=None): vi_mode = bool(a['--vi']) - config_dir = appdirs.user_config_dir('ptpython', 'Jonathan Slenders') - data_dir = appdirs.user_data_dir('ptpython', 'Jonathan Slenders') + config_dir = appdirs.user_config_dir('ptpython', 'prompt_toolkit') + data_dir = appdirs.user_data_dir('ptpython', 'prompt_toolkit') if a['--config-dir']: # Override config_dir. diff --git a/ptpython/entry_points/run_ptpython.py b/ptpython/entry_points/run_ptpython.py index e092b24e..ef9b44a8 100644 --- a/ptpython/entry_points/run_ptpython.py +++ b/ptpython/entry_points/run_ptpython.py @@ -31,8 +31,8 @@ def run(): vi_mode = bool(a['--vi']) - config_dir = appdirs.user_config_dir('ptpython', 'Jonathan Slenders') - data_dir = appdirs.user_data_dir('ptpython', 'Jonathan Slenders') + config_dir = appdirs.user_config_dir('ptpython', 'prompt_toolkit') + data_dir = appdirs.user_data_dir('ptpython', 'prompt_toolkit') if a['--config-dir']: # Override config_dir. From 54709f3b450be11c3078125ad71c6c7368bdc8b5 Mon Sep 17 00:00:00 2001 From: Nasy Date: Tue, 12 Nov 2019 23:39:16 -0500 Subject: [PATCH 250/470] Add Swap light/dark colors to config example --- examples/ptpython_config/config.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/examples/ptpython_config/config.py b/examples/ptpython_config/config.py index 0296727a..c79b01e0 100644 --- a/examples/ptpython_config/config.py +++ b/examples/ptpython_config/config.py @@ -48,6 +48,9 @@ def configure(repl): # When the sidebar is visible, also show the help text. repl.show_sidebar_help = True + # Swap light/dark colors on or off + repl.swap_light_and_dark = False + # Highlight matching parethesis. repl.highlight_matching_parenthesis = True From 392b08b91397cae228ad0fa85bab2068a27b697a Mon Sep 17 00:00:00 2001 From: Tim Gates Date: Mon, 30 Dec 2019 13:44:40 +1100 Subject: [PATCH 251/470] Fix simple typo: registeres -> registers Closes #331 --- ptpython/eventloop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ptpython/eventloop.py b/ptpython/eventloop.py index 43fe0549..9d16a2df 100644 --- a/ptpython/eventloop.py +++ b/ptpython/eventloop.py @@ -3,7 +3,7 @@ events when it's loaded and while we are waiting for input at the REPL. This way we don't block the UI of for instance ``turtle`` and other Tk libraries. -(Normally Tkinter registeres it's callbacks in ``PyOS_InputHook`` to integrate +(Normally Tkinter registers it's callbacks in ``PyOS_InputHook`` to integrate in readline. ``prompt-toolkit`` doesn't understand that input hook, but this will fix it for Tk.) """ From ac419e1a8662390ddc7383ade36643da5e0c4986 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 18 Jun 2019 18:47:25 +0200 Subject: [PATCH 252/470] Upgrade to prompt_toolkit 3.0. - Drop Python <3.5 support and prompt_toolkit 2. - Code formatting with black. - Sorted imports with isort. - Added type annotations. - Separate event loop for reading user input. --- .travis.yml | 15 +- examples/asyncio-python-embed.py | 15 +- examples/asyncio-ssh-python-embed.py | 16 +- examples/ptpython_config/config.py | 25 +- examples/python-embed-with-custom-prompt.py | 27 +- examples/python-embed.py | 2 +- examples/python-input.py | 4 +- ptpython/__main__.py | 1 - ptpython/completer.py | 142 ++-- ptpython/contrib/asyncssh_repl.py | 84 +- ptpython/entry_points/run_ptipython.py | 96 +-- ptpython/entry_points/run_ptpython.py | 173 ++-- ptpython/eventloop.py | 16 +- ptpython/filters.py | 24 +- ptpython/history_browser.py | 354 ++++---- ptpython/ipython.py | 176 ++-- ptpython/key_bindings.py | 160 ++-- ptpython/layout.py | 698 +++++++++------ ptpython/prompt_style.py | 61 +- ptpython/python_input.py | 899 ++++++++++++-------- ptpython/repl.py | 271 +++--- ptpython/style.py | 211 ++--- ptpython/utils.py | 76 +- ptpython/validator.py | 20 +- pyproject.toml | 13 + setup.py | 59 +- tests/run_tests.py | 16 +- 27 files changed, 2065 insertions(+), 1589 deletions(-) create mode 100644 pyproject.toml diff --git a/.travis.yml b/.travis.yml index 79a93e91..21611f91 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,18 +4,17 @@ language: python matrix: include: - python: 3.6 - - python: 3.5 - - python: 3.4 - - python: 3.3 - - python: 2.7 - - python: 2.6 - - python: pypy - - python: pypy3 + - python: 3.7 install: - - travis_retry pip install . pytest + - travis_retry pip install . pytest isort black - pip list script: - echo "$TRAVIS_PYTHON_VERSION" - ./tests/run_tests.py + + # Check wheather the imports were sorted correctly. + - isort -c -rc ptpython tests setup.py examples + + - black --check ptpython setup.py examples diff --git a/examples/asyncio-python-embed.py b/examples/asyncio-python-embed.py index fef19b7f..3b796b2a 100755 --- a/examples/asyncio-python-embed.py +++ b/examples/asyncio-python-embed.py @@ -12,10 +12,11 @@ prompt. """ from __future__ import unicode_literals -from ptpython.repl import embed import asyncio +from ptpython.repl import embed + loop = asyncio.get_event_loop() counter = [0] @@ -26,7 +27,7 @@ def print_counter(): Coroutine that prints counters and saves it in a global variable. """ while True: - print('Counter: %i' % counter[0]) + print("Counter: %i" % counter[0]) counter[0] += 1 yield from asyncio.sleep(3) @@ -37,9 +38,13 @@ def interactive_shell(): Coroutine that starts a Python REPL from which we can access the global counter variable. """ - print('You should be able to read and update the "counter[0]" variable from this shell.') + print( + 'You should be able to read and update the "counter[0]" variable from this shell.' + ) try: - yield from embed(globals=globals(), return_asyncio_coroutine=True, patch_stdout=True) + yield from embed( + globals=globals(), return_asyncio_coroutine=True, patch_stdout=True + ) except EOFError: # Stop the loop when quitting the repl. (Ctrl-D press.) loop.stop() @@ -53,5 +58,5 @@ def main(): loop.close() -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/examples/asyncio-ssh-python-embed.py b/examples/asyncio-ssh-python-embed.py index cbd07003..86b56073 100755 --- a/examples/asyncio-ssh-python-embed.py +++ b/examples/asyncio-ssh-python-embed.py @@ -6,9 +6,10 @@ Run this example and then SSH to localhost, port 8222. """ import asyncio -import asyncssh import logging +import asyncssh + from ptpython.contrib.asyncssh_repl import ReplSSHServerSession logging.basicConfig() @@ -19,6 +20,7 @@ class MySSHServer(asyncssh.SSHServer): """ Server without authentication, running `ReplSSHServerSession`. """ + def __init__(self, get_namespace): self.get_namespace = get_namespace @@ -37,22 +39,24 @@ def main(port=8222): loop = asyncio.get_event_loop() # Namespace exposed in the REPL. - environ = {'hello': 'world'} + environ = {"hello": "world"} # Start SSH server. def create_server(): return MySSHServer(lambda: environ) - print('Listening on :%i' % port) + print("Listening on :%i" % port) print('To connect, do "ssh localhost -p %i"' % port) loop.run_until_complete( - asyncssh.create_server(create_server, '', port, - server_host_keys=['/etc/ssh/ssh_host_dsa_key'])) + asyncssh.create_server( + create_server, "", port, server_host_keys=["/etc/ssh/ssh_host_dsa_key"] + ) + ) # Run eventloop. loop.run_forever() -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/examples/ptpython_config/config.py b/examples/ptpython_config/config.py index c79b01e0..ff8b8ac1 100644 --- a/examples/ptpython_config/config.py +++ b/examples/ptpython_config/config.py @@ -4,6 +4,7 @@ Copy this file to $XDG_CONFIG_HOME/ptpython/config.py """ from __future__ import unicode_literals + from prompt_toolkit.filters import ViInsertMode from prompt_toolkit.key_binding.key_processor import KeyPress from prompt_toolkit.keys import Keys @@ -11,9 +12,7 @@ from ptpython.layout import CompletionVisualisation -__all__ = ( - 'configure', -) +__all__ = ("configure",) def configure(repl): @@ -50,7 +49,7 @@ def configure(repl): # Swap light/dark colors on or off repl.swap_light_and_dark = False - + # Highlight matching parethesis. repl.highlight_matching_parenthesis = True @@ -75,7 +74,7 @@ def configure(repl): repl.paste_mode = False # Use the classic prompt. (Display '>>>' instead of 'In [1]'.) - repl.prompt_style = 'classic' # 'classic' or 'ipython' + repl.prompt_style = "classic" # 'classic' or 'ipython' # Don't insert a blank line after the output. repl.insert_blank_line_after_output = False @@ -108,14 +107,14 @@ def configure(repl): repl.enable_input_validation = True # Use this colorscheme for the code. - repl.use_code_colorscheme('pastie') + repl.use_code_colorscheme("pastie") # Set color depth (keep in mind that not all terminals support true color). - #repl.color_depth = 'DEPTH_1_BIT' # Monochrome. - #repl.color_depth = 'DEPTH_4_BIT' # ANSI colors only. - repl.color_depth = 'DEPTH_8_BIT' # The default, 256 colors. - #repl.color_depth = 'DEPTH_24_BIT' # True color. + # repl.color_depth = 'DEPTH_1_BIT' # Monochrome. + # repl.color_depth = 'DEPTH_4_BIT' # ANSI colors only. + repl.color_depth = "DEPTH_8_BIT" # The default, 256 colors. + # repl.color_depth = 'DEPTH_24_BIT' # True color. # Syntax. repl.enable_syntax_highlighting = True @@ -142,7 +141,6 @@ def _(event): event.current_buffer.validate_and_handle() """ - # Typing 'jj' in Vi Insert mode, should send escape. (Go back to navigation # mode.) """ @@ -178,8 +176,7 @@ def _(event): # `ptpython/style.py` for all possible tokens. _custom_ui_colorscheme = { # Blue prompt. - Token.Layout.Prompt: 'bg:#eeeeff #000000 bold', - + Token.Layout.Prompt: "bg:#eeeeff #000000 bold", # Make the status toolbar red. - Token.Toolbar.Status: 'bg:#ff0000 #000000', + Token.Toolbar.Status: "bg:#ff0000 #000000", } diff --git a/examples/python-embed-with-custom-prompt.py b/examples/python-embed-with-custom-prompt.py index 28eca860..bf27e936 100755 --- a/examples/python-embed-with-custom-prompt.py +++ b/examples/python-embed-with-custom-prompt.py @@ -4,10 +4,11 @@ """ from __future__ import unicode_literals -from ptpython.repl import embed -from ptpython.prompt_style import PromptStyle from pygments.token import Token +from ptpython.prompt_style import PromptStyle +from ptpython.repl import embed + def configure(repl): # There are several ways to override the prompt. @@ -18,25 +19,23 @@ def configure(repl): class CustomPrompt(PromptStyle): def in_tokens(self, cli): return [ - (Token.In, 'Input['), - (Token.In.Number, '%s' % repl.current_statement_index), - (Token.In, '] >>: '), + (Token.In, "Input["), + (Token.In.Number, "%s" % repl.current_statement_index), + (Token.In, "] >>: "), ] def in2_tokens(self, cli, width): - return [ - (Token.In, '...: '.rjust(width)), - ] + return [(Token.In, "...: ".rjust(width))] def out_tokens(self, cli): return [ - (Token.Out, 'Result['), - (Token.Out.Number, '%s' % repl.current_statement_index), - (Token.Out, ']: '), + (Token.Out, "Result["), + (Token.Out.Number, "%s" % repl.current_statement_index), + (Token.Out, "]: "), ] - repl.all_prompt_styles['custom'] = CustomPrompt() - repl.prompt_style = 'custom' + repl.all_prompt_styles["custom"] = CustomPrompt() + repl.prompt_style = "custom" # 2. Assign a new callable to `get_input_prompt_tokens`. This will always take effect. ## repl.get_input_prompt_tokens = lambda cli: [(Token.In, '[hello] >>> ')] @@ -52,5 +51,5 @@ def main(): embed(globals(), locals(), configure=configure) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/examples/python-embed.py b/examples/python-embed.py index 72c1c101..af24456e 100755 --- a/examples/python-embed.py +++ b/examples/python-embed.py @@ -10,5 +10,5 @@ def main(): embed(globals(), locals(), vi_mode=False) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/examples/python-input.py b/examples/python-input.py index bcfd6fca..1956070d 100755 --- a/examples/python-input.py +++ b/examples/python-input.py @@ -10,8 +10,8 @@ def main(): prompt = PythonInput() text = prompt.app.run() - print('You said: ' + text) + print("You said: " + text) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/ptpython/__main__.py b/ptpython/__main__.py index 7e4cbabe..83340a7b 100644 --- a/ptpython/__main__.py +++ b/ptpython/__main__.py @@ -1,7 +1,6 @@ """ Make `python -m ptpython` an alias for running `./ptpython`. """ -from __future__ import unicode_literals from .entry_points.run_ptpython import run run() diff --git a/ptpython/completer.py b/ptpython/completer.py index 8fa0e314..2ffaf62e 100644 --- a/ptpython/completer.py +++ b/ptpython/completer.py @@ -1,27 +1,33 @@ -from __future__ import unicode_literals +import ast +import keyword +import re +from typing import TYPE_CHECKING, Iterable -from prompt_toolkit.completion import Completer, Completion, PathCompleter +from prompt_toolkit.completion import ( + CompleteEvent, + Completer, + Completion, + PathCompleter, +) from prompt_toolkit.contrib.regular_languages.compiler import compile as compile_grammar from prompt_toolkit.contrib.regular_languages.completion import GrammarCompleter +from prompt_toolkit.document import Document from ptpython.utils import get_jedi_script_from_document -import keyword -import ast -import re -import six +if TYPE_CHECKING: + from prompt_toolkit.contrib.regular_languages.compiler import _CompiledGrammar -__all__ = ( - 'PythonCompleter', -) +__all__ = ["PythonCompleter"] class PythonCompleter(Completer): """ Completer for Python code. """ + def __init__(self, get_globals, get_locals, get_enable_dictionary_completion): - super(PythonCompleter, self).__init__() + super().__init__() self.get_globals = get_globals self.get_locals = get_locals @@ -33,17 +39,19 @@ def __init__(self, get_globals, get_locals, get_enable_dictionary_completion): self._path_completer_grammar_cache = None @property - def _path_completer(self): + def _path_completer(self) -> GrammarCompleter: if self._path_completer_cache is None: self._path_completer_cache = GrammarCompleter( - self._path_completer_grammar, { - 'var1': PathCompleter(expanduser=True), - 'var2': PathCompleter(expanduser=True), - }) + self._path_completer_grammar, + { + "var1": PathCompleter(expanduser=True), + "var2": PathCompleter(expanduser=True), + }, + ) return self._path_completer_cache @property - def _path_completer_grammar(self): + def _path_completer_grammar(self) -> "_CompiledGrammar": """ Return the grammar for matching paths inside strings inside Python code. @@ -54,15 +62,15 @@ def _path_completer_grammar(self): self._path_completer_grammar_cache = self._create_path_completer_grammar() return self._path_completer_grammar_cache - def _create_path_completer_grammar(self): - def unwrapper(text): - return re.sub(r'\\(.)', r'\1', text) + def _create_path_completer_grammar(self) -> "_CompiledGrammar": + def unwrapper(text: str) -> str: + return re.sub(r"\\(.)", r"\1", text) - def single_quoted_wrapper(text): - return text.replace('\\', '\\\\').replace("'", "\\'") + def single_quoted_wrapper(text: str) -> str: + return text.replace("\\", "\\\\").replace("'", "\\'") - def double_quoted_wrapper(text): - return text.replace('\\', '\\\\').replace('"', '\\"') + def double_quoted_wrapper(text: str) -> str: + return text.replace("\\", "\\\\").replace('"', '\\"') grammar = r""" # Text before the current string. @@ -91,40 +99,45 @@ def double_quoted_wrapper(text): return compile_grammar( grammar, - escape_funcs={ - 'var1': single_quoted_wrapper, - 'var2': double_quoted_wrapper, - }, - unescape_funcs={ - 'var1': unwrapper, - 'var2': unwrapper, - }) - - def _complete_path_while_typing(self, document): + escape_funcs={"var1": single_quoted_wrapper, "var2": double_quoted_wrapper}, + unescape_funcs={"var1": unwrapper, "var2": unwrapper}, + ) + + def _complete_path_while_typing(self, document: Document) -> bool: char_before_cursor = document.char_before_cursor - return document.text and ( - char_before_cursor.isalnum() or char_before_cursor in '/.~') + return bool( + document.text + and (char_before_cursor.isalnum() or char_before_cursor in "/.~") + ) - def _complete_python_while_typing(self, document): + def _complete_python_while_typing(self, document: Document) -> bool: char_before_cursor = document.char_before_cursor - return document.text and ( - char_before_cursor.isalnum() or char_before_cursor in '_.') + return bool( + document.text + and (char_before_cursor.isalnum() or char_before_cursor in "_.") + ) - def get_completions(self, document, complete_event): + def get_completions( + self, document: Document, complete_event: CompleteEvent + ) -> Iterable[Completion]: """ Get Python completions. """ # Do dictionary key completions. if self.get_enable_dictionary_completion(): has_dict_completions = False - for c in self.dictionary_completer.get_completions(document, complete_event): + for c in self.dictionary_completer.get_completions( + document, complete_event + ): has_dict_completions = True yield c if has_dict_completions: return # Do Path completions (if there were no dictionary completions). - if complete_event.completion_requested or self._complete_path_while_typing(document): + if complete_event.completion_requested or self._complete_path_while_typing( + document + ): for c in self._path_completer.get_completions(document, complete_event): yield c @@ -133,8 +146,12 @@ def get_completions(self, document, complete_event): return # Do Jedi Python completions. - if complete_event.completion_requested or self._complete_python_while_typing(document): - script = get_jedi_script_from_document(document, self.get_locals(), self.get_globals()) + if complete_event.completion_requested or self._complete_python_while_typing( + document + ): + script = get_jedi_script_from_document( + document, self.get_locals(), self.get_globals() + ) if script: try: @@ -178,9 +195,11 @@ def get_completions(self, document, complete_event): else: for c in completions: yield Completion( - c.name_with_symbols, len(c.complete) - len(c.name_with_symbols), + c.name_with_symbols, + len(c.complete) - len(c.name_with_symbols), display=c.name_with_symbols, - style=_get_style_for_name(c.name_with_symbols)) + style=_get_style_for_name(c.name_with_symbols), + ) class DictionaryCompleter(Completer): @@ -191,14 +210,15 @@ class DictionaryCompleter(Completer): bracket, which is potentially dangerous. It doesn't match on function calls, so it only triggers attribute access. """ + def __init__(self, get_globals, get_locals): - super(DictionaryCompleter, self).__init__() + super().__init__() self.get_globals = get_globals self.get_locals = get_locals self.pattern = re.compile( - r''' + r""" # Any expression safe enough to eval while typing. # No operators, except dot, and only other dict lookups. # Technically, this can be unsafe of course, if bad code runs @@ -227,11 +247,13 @@ def __init__(self, get_globals, get_locals): # string). \[ \s* ([a-zA-Z0-9_'"]*)$ - ''', - re.VERBOSE + """, + re.VERBOSE, ) - def get_completions(self, document, complete_event): + def get_completions( + self, document: Document, complete_event: CompleteEvent + ) -> Iterable[Completion]: match = self.pattern.search(document.text_before_cursor) if match is not None: object_var, key = match.groups() @@ -240,7 +262,7 @@ def get_completions(self, document, complete_event): # Do lookup of `object_var` in the context. try: result = eval(object_var, self.get_globals(), self.get_locals()) - except BaseException as e: + except BaseException: return # Many exception, like NameError can be thrown here. # If this object is a dictionary, complete the keys. @@ -256,28 +278,26 @@ def get_completions(self, document, complete_event): break for k in result: - if six.text_type(k).startswith(key_obj): - yield Completion( - six.text_type(repr(k)), - - len(key), - display=six.text_type(repr(k)) - ) + if str(k).startswith(key_obj): + yield Completion(str(repr(k)), -len(key), display=str(repr(k))) + try: import builtins + _builtin_names = dir(builtins) except ImportError: # Python 2. _builtin_names = [] -def _get_style_for_name(name): +def _get_style_for_name(name: str) -> str: """ Return completion style to use for this name. """ if name in _builtin_names: - return 'class:completion.builtin' + return "class:completion.builtin" if keyword.iskeyword(name): - return 'class:completion.keyword' + return "class:completion.keyword" - return '' + return "" diff --git a/ptpython/contrib/asyncssh_repl.py b/ptpython/contrib/asyncssh_repl.py index a4df4449..29c63afb 100644 --- a/ptpython/contrib/asyncssh_repl.py +++ b/ptpython/contrib/asyncssh_repl.py @@ -6,22 +6,18 @@ should make sure not to use Python 3-only syntax, because this package should be installable in Python 2 as well! """ -from __future__ import unicode_literals - import asyncio -import asyncssh +from typing import Optional, TextIO, cast -from prompt_toolkit.input import PipeInput -from prompt_toolkit.interface import CommandLineInterface -from prompt_toolkit.layout.screen import Size -from prompt_toolkit.shortcuts import create_asyncio_eventloop -from prompt_toolkit.terminal.vt100_output import Vt100_Output +import asyncssh +from prompt_toolkit.data_structures import Size +from prompt_toolkit.input import create_pipe_input +from prompt_toolkit.output.vt100 import Vt100_Output +from ptpython.python_input import _GetNamespace from ptpython.repl import PythonRepl -__all__ = ( - 'ReplSSHServerSession', -) +__all__ = ["ReplSSHServerSession"] class ReplSSHServerSession(asyncssh.SSHServerSession): @@ -31,51 +27,47 @@ class ReplSSHServerSession(asyncssh.SSHServerSession): :param get_globals: callable that returns the current globals. :param get_locals: (optional) callable that returns the current locals. """ - def __init__(self, get_globals, get_locals=None): - assert callable(get_globals) - assert get_locals is None or callable(get_locals) + def __init__( + self, get_globals: _GetNamespace, get_locals: Optional[_GetNamespace] = None + ) -> None: self._chan = None - def _globals(): + def _globals() -> dict: data = get_globals() - data.setdefault('print', self._print) + data.setdefault("print", self._print) return data - repl = PythonRepl(get_globals=_globals, - get_locals=get_locals or _globals) - - # Disable open-in-editor and system prompt. Because it would run and - # display these commands on the server side, rather than in the SSH - # client. - repl.enable_open_in_editor = False - repl.enable_system_bindings = False - # PipInput object, for sending input in the CLI. # (This is something that we can use in the prompt_toolkit event loop, # but still write date in manually.) - self._input_pipe = PipeInput() + self._input_pipe = create_pipe_input() # Output object. Don't render to the real stdout, but write everything # in the SSH channel. - class Stdout(object): - def write(s, data): + class Stdout: + def write(s, data: str) -> None: if self._chan is not None: - self._chan.write(data.replace('\n', '\r\n')) + data = data.replace("\n", "\r\n") + self._chan.write(data) - def flush(s): + def flush(s) -> None: pass - # Create command line interface. - self.cli = CommandLineInterface( - application=repl.create_application(), - eventloop=create_asyncio_eventloop(), + self.repl = PythonRepl( + get_globals=_globals, + get_locals=get_locals or _globals, input=self._input_pipe, - output=Vt100_Output(Stdout(), self._get_size)) + output=Vt100_Output(cast(TextIO, Stdout()), self._get_size), + ) - self._callbacks = self.cli.create_eventloop_callbacks() + # Disable open-in-editor and system prompt. Because it would run and + # display these commands on the server side, rather than in the SSH + # client. + self.repl.enable_open_in_editor = False + self.repl.enable_system_bindings = False - def _get_size(self): + def _get_size(self) -> Size: """ Callable that returns the current `Size`, required by Vt100_Output. """ @@ -92,22 +84,23 @@ def connection_made(self, chan): self._chan = chan # Run REPL interface. - f = asyncio.ensure_future(self.cli.run_async()) + f = asyncio.ensure_future(self.repl.run_async()) # Close channel when done. - def done(_): + def done(_) -> None: chan.close() self._chan = None + f.add_done_callback(done) - def shell_requested(self): + def shell_requested(self) -> bool: return True def terminal_size_changed(self, width, height, pixwidth, pixheight): """ When the terminal size changes, report back to CLI. """ - self._callbacks.terminal_size_changed() + self.repl.app._on_resize() def data_received(self, data, datatype): """ @@ -115,19 +108,12 @@ def data_received(self, data, datatype): """ self._input_pipe.send(data) - def _print(self, *data, **kw): + def _print(self, *data, sep=" ", end="\n", file=None) -> None: """ - _print(self, *data, sep=' ', end='\n', file=None) - Alternative 'print' function that prints back into the SSH channel. """ # Pop keyword-only arguments. (We cannot use the syntax from the # signature. Otherwise, Python2 will give a syntax error message when # installing.) - sep = kw.pop('sep', ' ') - end = kw.pop('end', '\n') - _ = kw.pop('file', None) - assert not kw, 'Too many keyword-only arguments' - data = sep.join(map(str, data)) self._chan.write(data + end) diff --git a/ptpython/entry_points/run_ptipython.py b/ptpython/entry_points/run_ptipython.py index 67239ce7..e7bcf39a 100644 --- a/ptpython/entry_points/run_ptipython.py +++ b/ptpython/entry_points/run_ptipython.py @@ -1,70 +1,37 @@ #!/usr/bin/env python -""" -ptipython: IPython interactive shell with the `prompt_toolkit` front-end. -Usage: - ptpython [ --vi ] - [ --config-dir= ] [ --interactive= ] - [--] [ ... ] - ptpython -h | --help - -Options: - --vi : Use Vi keybindings instead of Emacs bindings. - --config-dir= : Pass config directory. By default '$XDG_CONFIG_HOME/ptpython'. - -i, --interactive= : Start interactive shell after executing this file. -""" -from __future__ import absolute_import, unicode_literals, print_function - -import appdirs -import docopt import os -import six import sys +from .run_ptpython import create_parser, get_config_and_history_file -def run(user_ns=None): - a = docopt.docopt(__doc__) - - vi_mode = bool(a['--vi']) - - config_dir = appdirs.user_config_dir('ptpython', 'prompt_toolkit') - data_dir = appdirs.user_data_dir('ptpython', 'prompt_toolkit') - if a['--config-dir']: - # Override config_dir. - config_dir = os.path.expanduser(a['--config-dir']) - else: - # Warn about the legacy directory. - legacy_dir = os.path.expanduser('~/.ptpython') - if os.path.isdir(legacy_dir): - print('{0} is deprecated, migrate your configuration to {1}'.format(legacy_dir, config_dir)) +def run(user_ns=None): + a = create_parser().parse_args() - # Create directories. - for d in (config_dir, data_dir): - if not os.path.isdir(d) and not os.path.islink(d): - os.mkdir(d) + config_file, history_file = get_config_and_history_file(a) # If IPython is not available, show message and exit here with error status # code. try: import IPython except ImportError: - print('IPython not found. Please install IPython (pip install ipython).') + print("IPython not found. Please install IPython (pip install ipython).") sys.exit(1) else: from ptpython.ipython import embed from ptpython.repl import run_config, enable_deprecation_warnings # Add the current directory to `sys.path`. - if sys.path[0] != '': - sys.path.insert(0, '') + if sys.path[0] != "": + sys.path.insert(0, "") # When a file has been given, run that, otherwise start the shell. - if a[''] and not a['--interactive']: - sys.argv = a[''] - path = a[''][0] - with open(path, 'rb') as f: - code = compile(f.read(), path, 'exec') - six.exec_(code) + if a.args and not a.interactive: + sys.argv = a.args + path = a.args[0] + with open(path, "rb") as f: + code = compile(f.read(), path, "exec") + exec(code, {}) else: enable_deprecation_warnings() @@ -76,37 +43,38 @@ def run(user_ns=None): # Startup path startup_paths = [] - if 'PYTHONSTARTUP' in os.environ: - startup_paths.append(os.environ['PYTHONSTARTUP']) + if "PYTHONSTARTUP" in os.environ: + startup_paths.append(os.environ["PYTHONSTARTUP"]) # --interactive - if a['--interactive']: - startup_paths.append(a['--interactive']) - sys.argv = [a['--interactive']] + a[''] + if a.interactive: + startup_paths.append(a.args[0]) + sys.argv = a.args # exec scripts from startup paths for path in startup_paths: if os.path.exists(path): - with open(path, 'rb') as f: - code = compile(f.read(), path, 'exec') - six.exec_(code, user_ns, user_ns) + with open(path, "rb") as f: + code = compile(f.read(), path, "exec") + exec(code, user_ns, user_ns) else: - print('File not found: {}\n\n'.format(path)) + print("File not found: {}\n\n".format(path)) sys.exit(1) # Apply config file def configure(repl): - path = os.path.join(config_dir, 'config.py') - if os.path.exists(path): - run_config(repl, path) + if os.path.exists(config_file): + run_config(repl, config_file) # Run interactive shell. - embed(vi_mode=vi_mode, - history_filename=os.path.join(data_dir, 'history'), - configure=configure, - user_ns=user_ns, - title='IPython REPL (ptipython)') + embed( + vi_mode=a.vi, + history_filename=history_file, + configure=configure, + user_ns=user_ns, + title="IPython REPL (ptipython)", + ) -if __name__ == '__main__': +if __name__ == "__main__": run() diff --git a/ptpython/entry_points/run_ptpython.py b/ptpython/entry_points/run_ptpython.py index ef9b44a8..a8710792 100644 --- a/ptpython/entry_points/run_ptpython.py +++ b/ptpython/entry_points/run_ptpython.py @@ -15,81 +15,158 @@ Other environment variables: PYTHONSTARTUP: file executed on interactive startup (no default) """ -from __future__ import absolute_import, unicode_literals, print_function - -import appdirs -import docopt +import argparse import os -import six import sys +from typing import Tuple -from ptpython.repl import embed, enable_deprecation_warnings, run_config - - -def run(): - a = docopt.docopt(__doc__) - - vi_mode = bool(a['--vi']) +import appdirs +from prompt_toolkit.formatted_text import HTML +from prompt_toolkit.shortcuts import print_formatted_text - config_dir = appdirs.user_config_dir('ptpython', 'prompt_toolkit') - data_dir = appdirs.user_data_dir('ptpython', 'prompt_toolkit') +from ptpython.repl import embed, enable_deprecation_warnings, run_config - if a['--config-dir']: - # Override config_dir. - config_dir = os.path.expanduser(a['--config-dir']) - else: - # Warn about the legacy directory. - legacy_dir = os.path.expanduser('~/.ptpython') - if os.path.isdir(legacy_dir): - print('{0} is deprecated, migrate your configuration to {1}'.format(legacy_dir, config_dir)) +__all__ = ["create_parser", "get_config_and_history_file", "run"] + + +class _Parser(argparse.ArgumentParser): + def print_help(self): + super().print_help() + print("Other environment variables:") + print("PYTHONSTARTUP: file executed on interactive startup (no default)") + + +def create_parser() -> _Parser: + parser = _Parser(description="ptpython: Interactive Python shell.") + parser.add_argument("--vi", action="store_true", help="Enable Vi key bindings") + parser.add_argument( + "-i", + "--interactive", + action="store_true", + help="Start interactive shell after executing this file.", + ) + parser.add_argument( + "--config-file", type=str, help="Location of configuration file." + ) + parser.add_argument("--history-file", type=str, help="Location of history file.") + parser.add_argument( + "-V", "--version", action="store_true", help="Print version and exit." + ) + parser.add_argument("args", nargs="*", help="Script and arguments") + return parser + + +def get_config_and_history_file(namespace: argparse.Namespace) -> Tuple[str, str]: + """ + Check which config/history files to use, ensure that the directories for + these files exist, and return the config and history path. + """ + config_dir = appdirs.user_config_dir("ptpython", "prompt_toolkit") + data_dir = appdirs.user_data_dir("ptpython", "prompt_toolkit") # Create directories. for d in (config_dir, data_dir): if not os.path.isdir(d) and not os.path.islink(d): os.mkdir(d) + # Determine config file to be used. + config_file = os.path.join(config_dir, "config.py") + legacy_config_file = os.path.join(os.path.expanduser("~/.ptpython"), "config.py") + + warnings = [] + + # Config file + if namespace.config_file: + # Override config_file. + config_file = os.path.expanduser(namespace.config_file) + + elif os.path.isfile(legacy_config_file): + # Warn about the legacy configuration file. + warnings.append( + HTML( + " ~/.ptpython/config.py is deprecated, move your configuration to %s\n" + ) + % config_file + ) + config_file = legacy_config_file + + # Determine history file to be used. + history_file = os.path.join(data_dir, "history") + legacy_history_file = os.path.join(os.path.expanduser("~/.ptpython"), "history") + + if namespace.history_file: + # Override history_file. + history_file = os.path.expanduser(namespace.history_file) + + elif os.path.isfile(legacy_history_file): + # Warn about the legacy history file. + warnings.append( + HTML( + " ~/.ptpython/history is deprecated, move your history to %s\n" + ) + % history_file + ) + history_file = legacy_history_file + + # Print warnings. + if warnings: + print_formatted_text(HTML("Warning:")) + for w in warnings: + print_formatted_text(w) + + return config_file, history_file + + +def run() -> None: + a = create_parser().parse_args() + + config_file, history_file = get_config_and_history_file(a) + # Startup path startup_paths = [] - if 'PYTHONSTARTUP' in os.environ: - startup_paths.append(os.environ['PYTHONSTARTUP']) + if "PYTHONSTARTUP" in os.environ: + startup_paths.append(os.environ["PYTHONSTARTUP"]) # --interactive - if a['--interactive']: - startup_paths.append(a['--interactive']) - sys.argv = [a['--interactive']] + a[''] + if a.interactive and a.args: + startup_paths.append(a.args[0]) + sys.argv = a.args # Add the current directory to `sys.path`. - if sys.path[0] != '': - sys.path.insert(0, '') + if sys.path[0] != "": + sys.path.insert(0, "") # When a file has been given, run that, otherwise start the shell. - if a[''] and not a['--interactive']: - sys.argv = a[''] - path = a[''][0] - with open(path, 'rb') as f: - code = compile(f.read(), path, 'exec') + if a.args and not a.interactive: + sys.argv = a.args + path = a.args[0] + with open(path, "rb") as f: + code = compile(f.read(), path, "exec") # NOTE: We have to pass an empty dictionary as namespace. Omitting # this argument causes imports to not be found. See issue #326. - six.exec_(code, {}) + exec(code, {}) # Run interactive shell. else: enable_deprecation_warnings() # Apply config file - def configure(repl): - path = os.path.join(config_dir, 'config.py') - if os.path.exists(path): - run_config(repl, path) + def configure(repl) -> None: + if os.path.exists(config_file): + run_config(repl, config_file) import __main__ - embed(vi_mode=vi_mode, - history_filename=os.path.join(data_dir, 'history'), - configure=configure, - locals=__main__.__dict__, - globals=__main__.__dict__, - startup_paths=startup_paths, - title='Python REPL (ptpython)') - -if __name__ == '__main__': + + embed( + vi_mode=a.vi, + history_filename=history_file, + configure=configure, + locals=__main__.__dict__, + globals=__main__.__dict__, + startup_paths=startup_paths, + title="Python REPL (ptpython)", + ) + + +if __name__ == "__main__": run() diff --git a/ptpython/eventloop.py b/ptpython/eventloop.py index 9d16a2df..1e8c46a3 100644 --- a/ptpython/eventloop.py +++ b/ptpython/eventloop.py @@ -10,9 +10,7 @@ import sys import time -__all__ = ( - 'inputhook', -) +__all__ = ["inputhook"] def _inputhook_tk(inputhook_context): @@ -22,7 +20,8 @@ def _inputhook_tk(inputhook_context): """ # Get the current TK application. import _tkinter # Keep this imports inline! - from six.moves import tkinter + import tkinter + root = tkinter._default_root def wait_using_filehandler(): @@ -33,6 +32,7 @@ def wait_using_filehandler(): # Add a handler that sets the stop flag when `prompt-toolkit` has input # to process. stop = [False] + def done(*a): stop[0] = True @@ -52,13 +52,13 @@ def wait_using_polling(): """ while not inputhook_context.input_is_ready(): while root.dooneevent(_tkinter.ALL_EVENTS | _tkinter.DONT_WAIT): - pass + pass # Sleep to make the CPU idle, but not too long, so that the UI # stays responsive. - time.sleep(.01) + time.sleep(0.01) if root is not None: - if hasattr(root, 'createfilehandler'): + if hasattr(root, "createfilehandler"): wait_using_filehandler() else: wait_using_polling() @@ -66,5 +66,5 @@ def wait_using_polling(): def inputhook(inputhook_context): # Only call the real input hook when the 'Tkinter' library was loaded. - if 'Tkinter' in sys.modules or 'tkinter' in sys.modules: + if "Tkinter" in sys.modules or "tkinter" in sys.modules: _inputhook_tk(inputhook_context) diff --git a/ptpython/filters.py b/ptpython/filters.py index 8ddc3c6a..1adac135 100644 --- a/ptpython/filters.py +++ b/ptpython/filters.py @@ -1,38 +1,36 @@ -from __future__ import unicode_literals +from typing import TYPE_CHECKING from prompt_toolkit.filters import Filter -__all__ = ( - 'HasSignature', - 'ShowSidebar', - 'ShowSignature', - 'ShowDocstring', -) +if TYPE_CHECKING: + from .python_input import PythonInput + +__all__ = ["HasSignature", "ShowSidebar", "ShowSignature", "ShowDocstring"] class PythonInputFilter(Filter): - def __init__(self, python_input): + def __init__(self, python_input: "PythonInput") -> None: self.python_input = python_input - def __call__(self): + def __call__(self) -> bool: raise NotImplementedError class HasSignature(PythonInputFilter): - def __call__(self): + def __call__(self) -> bool: return bool(self.python_input.signatures) class ShowSidebar(PythonInputFilter): - def __call__(self): + def __call__(self) -> bool: return self.python_input.show_sidebar class ShowSignature(PythonInputFilter): - def __call__(self): + def __call__(self) -> bool: return self.python_input.show_signature class ShowDocstring(PythonInputFilter): - def __call__(self): + def __call__(self) -> bool: return self.python_input.show_docstring diff --git a/ptpython/history_browser.py b/ptpython/history_browser.py index 3d14067a..6d8ede43 100644 --- a/ptpython/history_browser.py +++ b/ptpython/history_browser.py @@ -4,7 +4,7 @@ `create_history_application` creates an `Application` instance that runs will run as a sub application of the Repl/PythonInput. """ -from __future__ import unicode_literals +from functools import partial from prompt_toolkit.application import Application from prompt_toolkit.application.current import get_app @@ -14,7 +14,17 @@ from prompt_toolkit.filters import Condition, has_focus from prompt_toolkit.formatted_text.utils import fragment_list_to_text from prompt_toolkit.key_binding import KeyBindings -from prompt_toolkit.layout.containers import HSplit, VSplit, Window, FloatContainer, Float, ConditionalContainer, Container, ScrollOffsets, WindowAlign +from prompt_toolkit.layout.containers import ( + ConditionalContainer, + Container, + Float, + FloatContainer, + HSplit, + ScrollOffsets, + VSplit, + Window, + WindowAlign, +) from prompt_toolkit.layout.controls import BufferControl, FormattedTextControl from prompt_toolkit.layout.dimension import Dimension as D from prompt_toolkit.layout.layout import Layout @@ -23,25 +33,16 @@ from prompt_toolkit.lexers import PygmentsLexer from prompt_toolkit.widgets import Frame from prompt_toolkit.widgets.toolbars import ArgToolbar, SearchToolbar +from pygments.lexers import Python3Lexer as PythonLexer from pygments.lexers import RstLexer -from .utils import if_mousedown - from ptpython.layout import get_inputmode_fragments -from functools import partial -import six - -if six.PY2: - from pygments.lexers import PythonLexer -else: - from pygments.lexers import Python3Lexer as PythonLexer +from .utils import if_mousedown HISTORY_COUNT = 2000 -__all__ = ( - 'HistoryLayout', -) +__all__ = ["HistoryLayout", "PythonHistory"] HELP_TEXT = """ This interface is meant to select multiple lines from the @@ -85,107 +86,128 @@ class BORDER: " Box drawing characters. " - HORIZONTAL = '\u2501' - VERTICAL = '\u2503' - TOP_LEFT = '\u250f' - TOP_RIGHT = '\u2513' - BOTTOM_LEFT = '\u2517' - BOTTOM_RIGHT = '\u251b' - LIGHT_VERTICAL = '\u2502' + HORIZONTAL = "\u2501" + VERTICAL = "\u2503" + TOP_LEFT = "\u250f" + TOP_RIGHT = "\u2513" + BOTTOM_LEFT = "\u2517" + BOTTOM_RIGHT = "\u251b" + LIGHT_VERTICAL = "\u2502" -def _create_popup_window(title, body): +def _create_popup_window(title: str, body: Container) -> Frame: """ Return the layout for a pop-up window. It consists of a title bar showing the `title` text, and a body layout. The window is surrounded by borders. """ - assert isinstance(title, six.text_type) - assert isinstance(body, Container) return Frame(body=body, title=title) -class HistoryLayout(object): +class HistoryLayout: """ Create and return a `Container` instance for the history application. """ + def __init__(self, history): search_toolbar = SearchToolbar() self.help_buffer_control = BufferControl( - buffer=history.help_buffer, - lexer=PygmentsLexer(RstLexer)) + buffer=history.help_buffer, lexer=PygmentsLexer(RstLexer) + ) help_window = _create_popup_window( - title='History Help', + title="History Help", body=Window( content=self.help_buffer_control, right_margins=[ScrollbarMargin(display_arrows=True)], - scroll_offsets=ScrollOffsets(top=2, bottom=2))) + scroll_offsets=ScrollOffsets(top=2, bottom=2), + ), + ) self.default_buffer_control = BufferControl( buffer=history.default_buffer, input_processors=[GrayExistingText(history.history_mapping)], - lexer=PygmentsLexer(PythonLexer)) + lexer=PygmentsLexer(PythonLexer), + ) self.history_buffer_control = BufferControl( buffer=history.history_buffer, lexer=PygmentsLexer(PythonLexer), search_buffer_control=search_toolbar.control, - preview_search=True) + preview_search=True, + ) history_window = Window( content=self.history_buffer_control, wrap_lines=False, left_margins=[HistoryMargin(history)], - scroll_offsets=ScrollOffsets(top=2, bottom=2)) - - self.root_container = HSplit([ - # Top title bar. - Window( - content=FormattedTextControl(_get_top_toolbar_fragments), - align=WindowAlign.CENTER, - style='class:status-toolbar'), - FloatContainer( - content=VSplit([ - # Left side: history. - history_window, - # Separator. - Window(width=D.exact(1), - char=BORDER.LIGHT_VERTICAL, - style='class:separator'), - # Right side: result. - Window( - content=self.default_buffer_control, - wrap_lines=False, - left_margins=[ResultMargin(history)], - scroll_offsets=ScrollOffsets(top=2, bottom=2)), - ]), - floats=[ - # Help text as a float. - Float(width=60, top=3, bottom=2, - content=ConditionalContainer( - content=help_window, filter=has_focus(history.help_buffer))), - ] - ), - # Bottom toolbars. - ArgToolbar(), - search_toolbar, - Window( - content=FormattedTextControl( - partial(_get_bottom_toolbar_fragments, history=history)), - style='class:status-toolbar'), - ]) + scroll_offsets=ScrollOffsets(top=2, bottom=2), + ) + + self.root_container = HSplit( + [ + # Top title bar. + Window( + content=FormattedTextControl(_get_top_toolbar_fragments), + align=WindowAlign.CENTER, + style="class:status-toolbar", + ), + FloatContainer( + content=VSplit( + [ + # Left side: history. + history_window, + # Separator. + Window( + width=D.exact(1), + char=BORDER.LIGHT_VERTICAL, + style="class:separator", + ), + # Right side: result. + Window( + content=self.default_buffer_control, + wrap_lines=False, + left_margins=[ResultMargin(history)], + scroll_offsets=ScrollOffsets(top=2, bottom=2), + ), + ] + ), + floats=[ + # Help text as a float. + Float( + width=60, + top=3, + bottom=2, + content=ConditionalContainer( + content=help_window, + filter=has_focus(history.help_buffer), + ), + ) + ], + ), + # Bottom toolbars. + ArgToolbar(), + search_toolbar, + Window( + content=FormattedTextControl( + partial(_get_bottom_toolbar_fragments, history=history) + ), + style="class:status-toolbar", + ), + ] + ) self.layout = Layout(self.root_container, history_window) def _get_top_toolbar_fragments(): - return [('class:status-bar.title', 'History browser - Insert from history')] + return [("class:status-bar.title", "History browser - Insert from history")] def _get_bottom_toolbar_fragments(history): python_input = history.python_input + @if_mousedown def f1(mouse_event): _toggle_help(history) @@ -194,18 +216,21 @@ def f1(mouse_event): def tab(mouse_event): _select_other_window(history) - return [ - ('class:status-toolbar', ' ') ] + get_inputmode_fragments(python_input) + [ - ('class:status-toolbar', ' '), - ('class:status-toolbar.key', '[Space]'), - ('class:status-toolbar', ' Toggle '), - ('class:status-toolbar.key', '[Tab]', tab), - ('class:status-toolbar', ' Focus ', tab), - ('class:status-toolbar.key', '[Enter]'), - ('class:status-toolbar', ' Accept '), - ('class:status-toolbar.key', '[F1]', f1), - ('class:status-toolbar', ' Help ', f1), - ] + return ( + [("class:status-toolbar", " ")] + + get_inputmode_fragments(python_input) + + [ + ("class:status-toolbar", " "), + ("class:status-toolbar.key", "[Space]"), + ("class:status-toolbar", " Toggle "), + ("class:status-toolbar.key", "[Tab]", tab), + ("class:status-toolbar", " Focus ", tab), + ("class:status-toolbar.key", "[Enter]"), + ("class:status-toolbar", " Accept "), + ("class:status-toolbar.key", "[F1]", f1), + ("class:status-toolbar", " Help ", f1), + ] + ) class HistoryMargin(Margin): @@ -213,6 +238,7 @@ class HistoryMargin(Margin): Margin for the history buffer. This displays a green bar for the selected entries. """ + def __init__(self, history): self.history_buffer = history.history_buffer self.history_mapping = history.history_mapping @@ -237,20 +263,20 @@ def create_margin(self, window_render_info, width, height): # Show stars at the start of each entry. # (Visualises multiline entries.) if line_number in lines_starting_new_entries: - char = '*' + char = "*" else: - char = ' ' + char = " " if line_number in selected_lines: - t = 'class:history-line,selected' + t = "class:history-line,selected" else: - t = 'class:history-line' + t = "class:history-line" if line_number == current_lineno: - t = t + ',current' + t = t + ",current" result.append((t, char)) - result.append(('', '\n')) + result.append(("", "\n")) return result @@ -259,6 +285,7 @@ class ResultMargin(Margin): """ The margin to be shown in the result pane. """ + def __init__(self, history): self.history_mapping = history.history_mapping self.history_buffer = history.history_buffer @@ -270,7 +297,9 @@ def create_margin(self, window_render_info, width, height): document = self.history_buffer.document current_lineno = document.cursor_position_row - offset = self.history_mapping.result_line_offset #original_document.cursor_position_row + offset = ( + self.history_mapping.result_line_offset + ) # original_document.cursor_position_row visible_line_to_input_line = window_render_info.visible_line_to_input_line @@ -279,16 +308,19 @@ def create_margin(self, window_render_info, width, height): for y in range(height): line_number = visible_line_to_input_line.get(y) - if (line_number is None or line_number < offset or - line_number >= offset + len(self.history_mapping.selected_lines)): - t = '' + if ( + line_number is None + or line_number < offset + or line_number >= offset + len(self.history_mapping.selected_lines) + ): + t = "" elif line_number == current_lineno: - t = 'class:history-line,selected,current' + t = "class:history-line,selected,current" else: - t = 'class:history-line,selected' + t = "class:history-line,selected" - result.append((t, ' ')) - result.append(('', '\n')) + result.append((t, " ")) + result.append(("", "\n")) return result @@ -300,26 +332,31 @@ class GrayExistingText(Processor): """ Turn the existing input, before and after the inserted code gray. """ + def __init__(self, history_mapping): self.history_mapping = history_mapping - self._lines_before = len(history_mapping.original_document.text_before_cursor.splitlines()) + self._lines_before = len( + history_mapping.original_document.text_before_cursor.splitlines() + ) def apply_transformation(self, transformation_input): lineno = transformation_input.lineno fragments = transformation_input.fragments - if (lineno < self._lines_before or - lineno >= self._lines_before + len(self.history_mapping.selected_lines)): + if lineno < self._lines_before or lineno >= self._lines_before + len( + self.history_mapping.selected_lines + ): text = fragment_list_to_text(fragments) - return Transformation(fragments=[('class:history.existing-input', text)]) + return Transformation(fragments=[("class:history.existing-input", text)]) else: return Transformation(fragments=fragments) -class HistoryMapping(object): +class HistoryMapping: """ Keep a list of all the lines from the history and the selected lines. """ + def __init__(self, history, python_history, original_document): self.history = history self.python_history = python_history @@ -339,10 +376,12 @@ def __init__(self, history, python_history, original_document): history_lines.append(line) if len(history_strings) > HISTORY_COUNT: - history_lines[0] = '# *** History has been truncated to %s lines ***' % HISTORY_COUNT + history_lines[0] = ( + "# *** History has been truncated to %s lines ***" % HISTORY_COUNT + ) self.history_lines = history_lines - self.concatenated_history = '\n'.join(history_lines) + self.concatenated_history = "\n".join(history_lines) # Line offset. if self.original_document.text_before_cursor: @@ -369,7 +408,7 @@ def get_new_document(self, cursor_pos=None): lines.append(self.original_document.text_after_cursor) # Create `Document` with cursor at the right position. - text = '\n'.join(lines) + text = "\n".join(lines) if cursor_pos is not None and cursor_pos > len(text): cursor_pos = len(text) return Document(text, cursor_pos) @@ -377,8 +416,7 @@ def get_new_document(self, cursor_pos=None): def update_default_buffer(self): b = self.history.default_buffer - b.set_document( - self.get_new_document(b.cursor_position), bypass_readonly=True) + b.set_document(self.get_new_document(b.cursor_position), bypass_readonly=True) def _toggle_help(history): @@ -410,7 +448,7 @@ def create_key_bindings(history, python_input, history_mapping): bindings = KeyBindings() handle = bindings.add - @handle(' ', filter=has_focus(history.history_buffer)) + @handle(" ", filter=has_focus(history.history_buffer)) def _(event): """ Space: select/deselect line from history pane. @@ -433,18 +471,21 @@ def _(event): # Update cursor position default_buffer = history.default_buffer - default_lineno = sorted(history_mapping.selected_lines).index(line_no) + \ - history_mapping.result_line_offset - default_buffer.cursor_position = \ - default_buffer.document.translate_row_col_to_index(default_lineno, 0) + default_lineno = ( + sorted(history_mapping.selected_lines).index(line_no) + + history_mapping.result_line_offset + ) + default_buffer.cursor_position = default_buffer.document.translate_row_col_to_index( + default_lineno, 0 + ) # Also move the cursor to the next line. (This way they can hold # space to select a region.) b.cursor_position = b.document.translate_row_col_to_index(line_no + 1, 0) - @handle(' ', filter=has_focus(DEFAULT_BUFFER)) - @handle('delete', filter=has_focus(DEFAULT_BUFFER)) - @handle('c-h', filter=has_focus(DEFAULT_BUFFER)) + @handle(" ", filter=has_focus(DEFAULT_BUFFER)) + @handle("delete", filter=has_focus(DEFAULT_BUFFER)) + @handle("c-h", filter=has_focus(DEFAULT_BUFFER)) def _(event): """ Space: remove line from default pane. @@ -463,50 +504,52 @@ def _(event): history_mapping.update_default_buffer() help_focussed = has_focus(history.help_buffer) - main_buffer_focussed = has_focus(history.history_buffer) | has_focus(history.default_buffer) - - @handle('tab', filter=main_buffer_focussed) - @handle('c-x', filter=main_buffer_focussed, eager=True) - # Eager: ignore the Emacs [Ctrl-X Ctrl-X] binding. - @handle('c-w', filter=main_buffer_focussed) + main_buffer_focussed = has_focus(history.history_buffer) | has_focus( + history.default_buffer + ) + + @handle("tab", filter=main_buffer_focussed) + @handle("c-x", filter=main_buffer_focussed, eager=True) + # Eager: ignore the Emacs [Ctrl-X Ctrl-X] binding. + @handle("c-w", filter=main_buffer_focussed) def _(event): " Select other window. " _select_other_window(history) - @handle('f4') + @handle("f4") def _(event): " Switch between Emacs/Vi mode. " python_input.vi_mode = not python_input.vi_mode - @handle('f1') + @handle("f1") def _(event): " Display/hide help. " _toggle_help(history) - @handle('enter', filter=help_focussed) - @handle('c-c', filter=help_focussed) - @handle('c-g', filter=help_focussed) - @handle('escape', filter=help_focussed) + @handle("enter", filter=help_focussed) + @handle("c-c", filter=help_focussed) + @handle("c-g", filter=help_focussed) + @handle("escape", filter=help_focussed) def _(event): " Leave help. " event.app.layout.focus_previous() - @handle('q', filter=main_buffer_focussed) - @handle('f3', filter=main_buffer_focussed) - @handle('c-c', filter=main_buffer_focussed) - @handle('c-g', filter=main_buffer_focussed) + @handle("q", filter=main_buffer_focussed) + @handle("f3", filter=main_buffer_focussed) + @handle("c-c", filter=main_buffer_focussed) + @handle("c-g", filter=main_buffer_focussed) def _(event): " Cancel and go back. " event.app.exit(result=None) - @handle('enter', filter=main_buffer_focussed) + @handle("enter", filter=main_buffer_focussed) def _(event): " Accept input. " event.app.exit(result=history.default_buffer.text) enable_system_bindings = Condition(lambda: python_input.enable_system_bindings) - @handle('c-z', filter=enable_system_bindings) + @handle("c-z", filter=enable_system_bindings) def _(event): " Suspend to background. " event.app.suspend_to_background() @@ -514,7 +557,7 @@ def _(event): return bindings -class History(object): +class PythonHistory: def __init__(self, python_input, original_document): """ Create an `Application` for the history screen. @@ -530,26 +573,28 @@ def __init__(self, python_input, original_document): document = Document(history_mapping.concatenated_history) document = Document( document.text, - cursor_position=document.cursor_position + document.get_start_of_line_position()) + cursor_position=document.cursor_position + + document.get_start_of_line_position(), + ) self.history_buffer = Buffer( document=document, on_cursor_position_changed=self._history_buffer_pos_changed, accept_handler=( - lambda buff: get_app().exit(result=self.default_buffer.text)), - read_only=True) + lambda buff: get_app().exit(result=self.default_buffer.text) + ), + read_only=True, + ) self.default_buffer = Buffer( name=DEFAULT_BUFFER, document=history_mapping.get_new_document(), on_cursor_position_changed=self._default_buffer_pos_changed, - read_only=True) - - self.help_buffer = Buffer( - document=Document(HELP_TEXT, 0), - read_only=True + read_only=True, ) + self.help_buffer = Buffer(document=Document(HELP_TEXT, 0), read_only=True) + self.history_layout = HistoryLayout(self) self.app = Application( @@ -557,7 +602,7 @@ def __init__(self, python_input, original_document): full_screen=True, style=python_input._current_style, mouse_support=Condition(lambda: python_input.enable_mouse_support), - key_bindings=create_key_bindings(self, python_input, history_mapping) + key_bindings=create_key_bindings(self, python_input, history_mapping), ) def _default_buffer_pos_changed(self, _): @@ -566,8 +611,10 @@ def _default_buffer_pos_changed(self, _): # Only when this buffer has the focus. if self.app.current_buffer == self.default_buffer: try: - line_no = self.default_buffer.document.cursor_position_row - \ - self.history_mapping.result_line_offset + line_no = ( + self.default_buffer.document.cursor_position_row + - self.history_mapping.result_line_offset + ) if line_no < 0: # When the cursor is above the inserted region. raise IndexError @@ -576,8 +623,9 @@ def _default_buffer_pos_changed(self, _): except IndexError: pass else: - self.history_buffer.cursor_position = \ - self.history_buffer.document.translate_row_col_to_index(history_lineno, 0) + self.history_buffer.cursor_position = self.history_buffer.document.translate_row_col_to_index( + history_lineno, 0 + ) def _history_buffer_pos_changed(self, _): """ When the cursor changes in the history buffer. Synchronize. """ @@ -586,9 +634,11 @@ def _history_buffer_pos_changed(self, _): line_no = self.history_buffer.document.cursor_position_row if line_no in self.history_mapping.selected_lines: - default_lineno = sorted(self.history_mapping.selected_lines).index(line_no) + \ - self.history_mapping.result_line_offset - - self.default_buffer.cursor_position = \ - self.default_buffer.document.translate_row_col_to_index(default_lineno, 0) - + default_lineno = ( + sorted(self.history_mapping.selected_lines).index(line_no) + + self.history_mapping.result_line_offset + ) + + self.default_buffer.cursor_position = self.default_buffer.document.translate_row_col_to_index( + default_lineno, 0 + ) diff --git a/ptpython/ipython.py b/ptpython/ipython.py index 8cc5a36e..20f29bdc 100644 --- a/ptpython/ipython.py +++ b/ptpython/ipython.py @@ -8,10 +8,12 @@ offer. """ -from __future__ import unicode_literals, print_function - -from prompt_toolkit.completion import Completion, Completer -from prompt_toolkit.completion import PathCompleter, WordCompleter +from prompt_toolkit.completion import ( + Completer, + Completion, + PathCompleter, + WordCompleter, +) from prompt_toolkit.contrib.completers import SystemCompleter from prompt_toolkit.contrib.regular_languages.compiler import compile from prompt_toolkit.contrib.regular_languages.completion import GrammarCompleter @@ -20,27 +22,25 @@ from prompt_toolkit.formatted_text import PygmentsTokens from prompt_toolkit.lexers import PygmentsLexer, SimpleLexer from prompt_toolkit.styles import Style +from pygments.lexers import BashLexer, PythonLexer -from .python_input import PythonInput, PythonValidator, PythonCompleter -from .style import default_ui_style - -from IPython.terminal.embed import InteractiveShellEmbed as _InteractiveShellEmbed -from IPython.terminal.ipapp import load_default_config from IPython import utils as ipy_utils from IPython.core.inputsplitter import IPythonInputSplitter - -from pygments.lexers import PythonLexer, BashLexer +from IPython.terminal.embed import InteractiveShellEmbed as _InteractiveShellEmbed +from IPython.terminal.ipapp import load_default_config from ptpython.prompt_style import PromptStyle -__all__ = ( - 'embed', -) +from .python_input import PythonCompleter, PythonInput, PythonValidator +from .style import default_ui_style + +__all__ = ["embed"] class IPythonPrompt(PromptStyle): """ Style for IPython >5.0, use the prompt_toolkit tokens directly. """ + def __init__(self, prompts): self.prompts = prompts @@ -68,7 +68,8 @@ def create_ipython_grammar(): """ Return compiled IPython grammar. """ - return compile(r""" + return compile( + r""" \s* ( (?P%)( @@ -87,24 +88,37 @@ def create_ipython_grammar(): (?![%!]) (?P.+) ) \s* - """) + """ + ) -def create_completer(get_globals, get_locals, magics_manager, alias_manager, - get_enable_dictionary_completion): +def create_completer( + get_globals, + get_locals, + magics_manager, + alias_manager, + get_enable_dictionary_completion, +): g = create_ipython_grammar() - return GrammarCompleter(g, { - 'python': PythonCompleter(get_globals, get_locals, get_enable_dictionary_completion), - 'magic': MagicsCompleter(magics_manager), - 'alias_name': AliasCompleter(alias_manager), - 'pdb_arg': WordCompleter(['on', 'off'], ignore_case=True), - 'autocall_arg': WordCompleter(['0', '1', '2'], ignore_case=True), - 'py_filename': PathCompleter(only_directories=False, file_filter=lambda name: name.endswith('.py')), - 'filename': PathCompleter(only_directories=False), - 'directory': PathCompleter(only_directories=True), - 'system': SystemCompleter(), - }) + return GrammarCompleter( + g, + { + "python": PythonCompleter( + get_globals, get_locals, get_enable_dictionary_completion + ), + "magic": MagicsCompleter(magics_manager), + "alias_name": AliasCompleter(alias_manager), + "pdb_arg": WordCompleter(["on", "off"], ignore_case=True), + "autocall_arg": WordCompleter(["0", "1", "2"], ignore_case=True), + "py_filename": PathCompleter( + only_directories=False, file_filter=lambda name: name.endswith(".py") + ), + "filename": PathCompleter(only_directories=False), + "directory": PathCompleter(only_directories=True), + "system": SystemCompleter(), + }, + ) def create_lexer(): @@ -113,12 +127,13 @@ def create_lexer(): return GrammarLexer( g, lexers={ - 'percent': SimpleLexer('class:pygments.operator'), - 'magic': SimpleLexer('class:pygments.keyword'), - 'filename': SimpleLexer('class:pygments.name'), - 'python': PygmentsLexer(PythonLexer), - 'system': PygmentsLexer(BashLexer), - }) + "percent": SimpleLexer("class:pygments.operator"), + "magic": SimpleLexer("class:pygments.keyword"), + "filename": SimpleLexer("class:pygments.name"), + "python": PygmentsLexer(PythonLexer), + "system": PygmentsLexer(BashLexer), + }, + ) class MagicsCompleter(Completer): @@ -128,9 +143,9 @@ def __init__(self, magics_manager): def get_completions(self, document, complete_event): text = document.text_before_cursor.lstrip() - for m in sorted(self.magics_manager.magics['line']): + for m in sorted(self.magics_manager.magics["line"]): if m.startswith(text): - yield Completion('%s' % m, -len(text)) + yield Completion("%s" % m, -len(text)) class AliasCompleter(Completer): @@ -139,48 +154,50 @@ def __init__(self, alias_manager): def get_completions(self, document, complete_event): text = document.text_before_cursor.lstrip() - #aliases = [a for a, _ in self.alias_manager.aliases] + # aliases = [a for a, _ in self.alias_manager.aliases] aliases = self.alias_manager.aliases for a, cmd in sorted(aliases, key=lambda a: a[0]): if a.startswith(text): - yield Completion('%s' % a, -len(text), - display_meta=cmd) + yield Completion("%s" % a, -len(text), display_meta=cmd) class IPythonInput(PythonInput): """ Override our `PythonCommandLineInterface` to add IPython specific stuff. """ + def __init__(self, ipython_shell, *a, **kw): - kw['_completer'] = create_completer(kw['get_globals'], kw['get_globals'], - ipython_shell.magics_manager, - ipython_shell.alias_manager, - lambda: self.enable_dictionary_completion) - kw['_lexer'] = create_lexer() - kw['_validator'] = IPythonValidator( - get_compiler_flags=self.get_compiler_flags) - - super(IPythonInput, self).__init__(*a, **kw) + kw["_completer"] = create_completer( + kw["get_globals"], + kw["get_globals"], + ipython_shell.magics_manager, + ipython_shell.alias_manager, + lambda: self.enable_dictionary_completion, + ) + kw["_lexer"] = create_lexer() + kw["_validator"] = IPythonValidator(get_compiler_flags=self.get_compiler_flags) + + super().__init__(*a, **kw) self.ipython_shell = ipython_shell - self.all_prompt_styles['ipython'] = IPythonPrompt(ipython_shell.prompts) - self.prompt_style = 'ipython' + self.all_prompt_styles["ipython"] = IPythonPrompt(ipython_shell.prompts) + self.prompt_style = "ipython" # UI style for IPython. Add tokens that are used by IPython>5.0 style_dict = {} style_dict.update(default_ui_style) - style_dict.update({ - 'pygments.prompt': '#009900', - 'pygments.prompt-num': '#00ff00 bold', - 'pygments.out-prompt': '#990000', - 'pygments.out-prompt-num': '#ff0000 bold', - }) + style_dict.update( + { + "pygments.prompt": "#009900", + "pygments.prompt-num": "#00ff00 bold", + "pygments.out-prompt": "#990000", + "pygments.out-prompt-num": "#ff0000 bold", + } + ) - self.ui_styles = { - 'default': Style.from_dict(style_dict), - } - self.use_ui_colorscheme('default') + self.ui_styles = {"default": Style.from_dict(style_dict)} + self.use_ui_colorscheme("default") class InteractiveShellEmbed(_InteractiveShellEmbed): @@ -190,31 +207,34 @@ class InteractiveShellEmbed(_InteractiveShellEmbed): :param configure: Callable for configuring the repl. """ + def __init__(self, *a, **kw): - vi_mode = kw.pop('vi_mode', False) - history_filename = kw.pop('history_filename', None) - configure = kw.pop('configure', None) - title = kw.pop('title', None) + vi_mode = kw.pop("vi_mode", False) + history_filename = kw.pop("history_filename", None) + configure = kw.pop("configure", None) + title = kw.pop("title", None) # Don't ask IPython to confirm for exit. We have our own exit prompt. self.confirm_exit = False - super(InteractiveShellEmbed, self).__init__(*a, **kw) + super().__init__(*a, **kw) def get_globals(): return self.user_ns python_input = IPythonInput( self, - get_globals=get_globals, vi_mode=vi_mode, - history_filename=history_filename) + get_globals=get_globals, + vi_mode=vi_mode, + history_filename=history_filename, + ) if title: python_input.terminal_title = title if configure: configure(python_input) - python_input.prompt_style = 'ipython' # Don't take from config. + python_input.prompt_style = "ipython" # Don't take from config. self.python_input = python_input @@ -223,7 +243,7 @@ def prompt_for_code(self): return self.python_input.app.run() except KeyboardInterrupt: self.python_input.default_buffer.document = Document() - return '' + return "" def initialize_extensions(shell, extensions): @@ -240,8 +260,10 @@ def initialize_extensions(shell, extensions): shell.extension_manager.load_extension(ext) except: ipy_utils.warn.warn( - "Error in loading extension: %s" % ext + - "\nCheck your config files in %s" % ipy_utils.path.get_ipython_dir()) + "Error in loading extension: %s" % ext + + "\nCheck your config files in %s" + % ipy_utils.path.get_ipython_dir() + ) shell.showtraceback() @@ -249,13 +271,13 @@ def embed(**kwargs): """ Copied from `IPython/terminal/embed.py`, but using our `InteractiveShellEmbed` instead. """ - config = kwargs.get('config') - header = kwargs.pop('header', u'') - compile_flags = kwargs.pop('compile_flags', None) + config = kwargs.get("config") + header = kwargs.pop("header", "") + compile_flags = kwargs.pop("compile_flags", None) if config is None: config = load_default_config() config.InteractiveShellEmbed = config.TerminalInteractiveShell - kwargs['config'] = config + kwargs["config"] = config shell = InteractiveShellEmbed.instance(**kwargs) - initialize_extensions(shell, config['InteractiveShellApp']['extensions']) + initialize_extensions(shell, config["InteractiveShellApp"]["extensions"]) shell(header=header, stack_depth=2, compile_flags=compile_flags) diff --git a/ptpython/key_bindings.py b/ptpython/key_bindings.py index 001f59b9..1740caf7 100644 --- a/ptpython/key_bindings.py +++ b/ptpython/key_bindings.py @@ -1,18 +1,24 @@ -from __future__ import unicode_literals - +from prompt_toolkit.application import get_app from prompt_toolkit.document import Document from prompt_toolkit.enums import DEFAULT_BUFFER -from prompt_toolkit.filters import has_selection, has_focus, Condition, vi_insert_mode, emacs_insert_mode, emacs_mode +from prompt_toolkit.filters import ( + Condition, + emacs_insert_mode, + emacs_mode, + has_focus, + has_selection, + vi_insert_mode, +) from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.keys import Keys -from prompt_toolkit.application import get_app + from .utils import document_is_multiline_python -__all__ = ( - 'load_python_bindings', - 'load_sidebar_bindings', - 'load_confirm_exit_bindings', -) +__all__ = [ + "load_python_bindings", + "load_sidebar_bindings", + "load_confirm_exit_bindings", +] @Condition @@ -40,14 +46,14 @@ def load_python_bindings(python_input): sidebar_visible = Condition(lambda: python_input.show_sidebar) handle = bindings.add - @handle('c-l') + @handle("c-l") def _(event): """ Clear whole screen and render again -- also when the sidebar is visible. """ event.app.renderer.clear() - @handle('c-z') + @handle("c-z") def _(event): """ Suspend. @@ -55,7 +61,7 @@ def _(event): if python_input.enable_system_bindings: event.app.suspend_to_background() - @handle('f2') + @handle("f2") def _(event): """ Show/hide sidebar. @@ -66,42 +72,49 @@ def _(event): else: event.app.layout.focus_last() - @handle('f3') + @handle("f3") def _(event): """ Select from the history. """ python_input.enter_history() - @handle('f4') + @handle("f4") def _(event): """ Toggle between Vi and Emacs mode. """ python_input.vi_mode = not python_input.vi_mode - @handle('f6') + @handle("f6") def _(event): """ Enable/Disable paste mode. """ python_input.paste_mode = not python_input.paste_mode - @handle('tab', filter= ~sidebar_visible & ~has_selection & tab_should_insert_whitespace) + @handle( + "tab", filter=~sidebar_visible & ~has_selection & tab_should_insert_whitespace + ) def _(event): """ When tab should insert whitespace, do that instead of completion. """ - event.app.current_buffer.insert_text(' ') + event.app.current_buffer.insert_text(" ") @Condition def is_multiline(): return document_is_multiline_python(python_input.default_buffer.document) - @handle('enter', filter= ~sidebar_visible & ~has_selection & - (vi_insert_mode | emacs_insert_mode) & - has_focus(DEFAULT_BUFFER) & ~is_multiline) - @handle(Keys.Escape, Keys.Enter, filter= ~sidebar_visible & emacs_mode) + @handle( + "enter", + filter=~sidebar_visible + & ~has_selection + & (vi_insert_mode | emacs_insert_mode) + & has_focus(DEFAULT_BUFFER) + & ~is_multiline, + ) + @handle(Keys.Escape, Keys.Enter, filter=~sidebar_visible & emacs_mode) def _(event): """ Accept input (for single line input). @@ -112,14 +125,19 @@ def _(event): # When the cursor is at the end, and we have an empty line: # drop the empty lines, but return the value. b.document = Document( - text=b.text.rstrip(), - cursor_position=len(b.text.rstrip())) + text=b.text.rstrip(), cursor_position=len(b.text.rstrip()) + ) b.validate_and_handle() - @handle('enter', filter= ~sidebar_visible & ~has_selection & - (vi_insert_mode | emacs_insert_mode) & - has_focus(DEFAULT_BUFFER) & is_multiline) + @handle( + "enter", + filter=~sidebar_visible + & ~has_selection + & (vi_insert_mode | emacs_insert_mode) + & has_focus(DEFAULT_BUFFER) + & is_multiline, + ) def _(event): """ Behaviour of the Enter key. @@ -134,30 +152,36 @@ def at_the_end(b): """ we consider the cursor at the end when there is no text after the cursor, or only whitespace. """ text = b.document.text_after_cursor - return text == '' or (text.isspace() and not '\n' in text) + return text == "" or (text.isspace() and not "\n" in text) if python_input.paste_mode: # In paste mode, always insert text. - b.insert_text('\n') + b.insert_text("\n") - elif at_the_end(b) and b.document.text.replace(' ', '').endswith( - '\n' * (empty_lines_required - 1)): + elif at_the_end(b) and b.document.text.replace(" ", "").endswith( + "\n" * (empty_lines_required - 1) + ): # When the cursor is at the end, and we have an empty line: # drop the empty lines, but return the value. if b.validate(): b.document = Document( - text=b.text.rstrip(), - cursor_position=len(b.text.rstrip())) + text=b.text.rstrip(), cursor_position=len(b.text.rstrip()) + ) b.validate_and_handle() else: auto_newline(b) - @handle('c-d', filter=~sidebar_visible & - has_focus(python_input.default_buffer) & - Condition(lambda: - # The current buffer is empty. - not get_app().current_buffer.text)) + @handle( + "c-d", + filter=~sidebar_visible + & has_focus(python_input.default_buffer) + & Condition( + lambda: + # The current buffer is empty. + not get_app().current_buffer.text + ), + ) def _(event): """ Override Control-D exit, to ask for confirmation. @@ -167,10 +191,10 @@ def _(event): else: event.app.exit(exception=EOFError) - @handle('c-c', filter=has_focus(python_input.default_buffer)) + @handle("c-c", filter=has_focus(python_input.default_buffer)) def _(event): " Abort when Control-C has been pressed. " - event.app.exit(exception=KeyboardInterrupt, style='class:aborting') + event.app.exit(exception=KeyboardInterrupt, style="class:aborting") return bindings @@ -184,42 +208,44 @@ def load_sidebar_bindings(python_input): handle = bindings.add sidebar_visible = Condition(lambda: python_input.show_sidebar) - @handle('up', filter=sidebar_visible) - @handle('c-p', filter=sidebar_visible) - @handle('k', filter=sidebar_visible) + @handle("up", filter=sidebar_visible) + @handle("c-p", filter=sidebar_visible) + @handle("k", filter=sidebar_visible) def _(event): " Go to previous option. " python_input.selected_option_index = ( - (python_input.selected_option_index - 1) % python_input.option_count) + python_input.selected_option_index - 1 + ) % python_input.option_count - @handle('down', filter=sidebar_visible) - @handle('c-n', filter=sidebar_visible) - @handle('j', filter=sidebar_visible) + @handle("down", filter=sidebar_visible) + @handle("c-n", filter=sidebar_visible) + @handle("j", filter=sidebar_visible) def _(event): " Go to next option. " python_input.selected_option_index = ( - (python_input.selected_option_index + 1) % python_input.option_count) + python_input.selected_option_index + 1 + ) % python_input.option_count - @handle('right', filter=sidebar_visible) - @handle('l', filter=sidebar_visible) - @handle(' ', filter=sidebar_visible) + @handle("right", filter=sidebar_visible) + @handle("l", filter=sidebar_visible) + @handle(" ", filter=sidebar_visible) def _(event): " Select next value for current option. " option = python_input.selected_option option.activate_next() - @handle('left', filter=sidebar_visible) - @handle('h', filter=sidebar_visible) + @handle("left", filter=sidebar_visible) + @handle("h", filter=sidebar_visible) def _(event): " Select previous value for current option. " option = python_input.selected_option option.activate_previous() - @handle('c-c', filter=sidebar_visible) - @handle('c-d', filter=sidebar_visible) - @handle('c-d', filter=sidebar_visible) - @handle('enter', filter=sidebar_visible) - @handle('escape', filter=sidebar_visible) + @handle("c-c", filter=sidebar_visible) + @handle("c-d", filter=sidebar_visible) + @handle("c-d", filter=sidebar_visible) + @handle("enter", filter=sidebar_visible) + @handle("escape", filter=sidebar_visible) def _(event): " Hide sidebar. " python_input.show_sidebar = False @@ -237,15 +263,15 @@ def load_confirm_exit_bindings(python_input): handle = bindings.add confirmation_visible = Condition(lambda: python_input.show_exit_confirmation) - @handle('y', filter=confirmation_visible) - @handle('Y', filter=confirmation_visible) - @handle('enter', filter=confirmation_visible) - @handle('c-d', filter=confirmation_visible) + @handle("y", filter=confirmation_visible) + @handle("Y", filter=confirmation_visible) + @handle("enter", filter=confirmation_visible) + @handle("c-d", filter=confirmation_visible) def _(event): """ Really quit. """ - event.app.exit(exception=EOFError, style='class:exiting') + event.app.exit(exception=EOFError, style="class:exiting") @handle(Keys.Any, filter=confirmation_visible) def _(event): @@ -265,14 +291,14 @@ def auto_newline(buffer): if buffer.document.current_line_after_cursor: # When we are in the middle of a line. Always insert a newline. - insert_text('\n') + insert_text("\n") else: # Go to new line, but also add indentation. current_line = buffer.document.current_line_before_cursor.rstrip() - insert_text('\n') + insert_text("\n") # Unident if the last line ends with 'pass', remove four spaces. - unindent = current_line.rstrip().endswith(' pass') + unindent = current_line.rstrip().endswith(" pass") # Copy whitespace from current line current_line2 = current_line[4:] if unindent else current_line @@ -284,6 +310,6 @@ def auto_newline(buffer): break # If the last line ends with a colon, add four extra spaces. - if current_line[-1:] == ':': + if current_line[-1:] == ":": for x in range(4): - insert_text(' ') + insert_text(" ") diff --git a/ptpython/layout.py b/ptpython/layout.py index 3cc230f0..7b68b2d4 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -1,120 +1,147 @@ """ Creation of the `Layout` instance for the Python input/REPL. """ -from __future__ import unicode_literals +import platform +import sys +from enum import Enum +from typing import TYPE_CHECKING, Optional from prompt_toolkit.application import get_app from prompt_toolkit.enums import DEFAULT_BUFFER, SEARCH_BUFFER -from prompt_toolkit.filters import is_done, has_completions, renderer_height_is_known, has_focus, Condition +from prompt_toolkit.filters import ( + Condition, + has_completions, + has_focus, + is_done, + renderer_height_is_known, +) from prompt_toolkit.formatted_text import fragment_list_width, to_formatted_text +from prompt_toolkit.formatted_text.base import StyleAndTextTuples from prompt_toolkit.key_binding.vi_state import InputMode -from prompt_toolkit.layout.containers import Window, HSplit, VSplit, FloatContainer, Float, ConditionalContainer, ScrollOffsets +from prompt_toolkit.layout.containers import ( + ConditionalContainer, + Container, + Float, + FloatContainer, + HSplit, + ScrollOffsets, + VSplit, + Window, +) from prompt_toolkit.layout.controls import BufferControl, FormattedTextControl -from prompt_toolkit.layout.dimension import Dimension +from prompt_toolkit.layout.dimension import AnyDimension, Dimension from prompt_toolkit.layout.layout import Layout from prompt_toolkit.layout.margins import PromptMargin from prompt_toolkit.layout.menus import CompletionsMenu, MultiColumnCompletionsMenu -from prompt_toolkit.layout.processors import ConditionalProcessor, AppendAutoSuggestion, HighlightIncrementalSearchProcessor, HighlightSelectionProcessor, HighlightMatchingBracketProcessor, Processor, Transformation +from prompt_toolkit.layout.processors import ( + AppendAutoSuggestion, + ConditionalProcessor, + DisplayMultipleCursors, + HighlightIncrementalSearchProcessor, + HighlightMatchingBracketProcessor, + HighlightSelectionProcessor, +) from prompt_toolkit.lexers import SimpleLexer +from prompt_toolkit.mouse_events import MouseEvent from prompt_toolkit.selection import SelectionType -from prompt_toolkit.widgets.toolbars import CompletionsToolbar, ArgToolbar, SearchToolbar, ValidationToolbar, SystemToolbar - -from .filters import HasSignature, ShowSidebar, ShowSignature, ShowDocstring -from .utils import if_mousedown - -from pygments.lexers import PythonLexer - -import platform -import sys - -__all__ = ( - 'PtPythonLayout', - 'CompletionVisualisation', +from prompt_toolkit.widgets.toolbars import ( + ArgToolbar, + CompletionsToolbar, + SearchToolbar, + SystemToolbar, + ValidationToolbar, ) +from pygments.lexers import PythonLexer +from .filters import HasSignature, ShowDocstring, ShowSidebar, ShowSignature +from .utils import if_mousedown -# DisplayMultipleCursors: Only for prompt_toolkit>=1.0.8 -try: - from prompt_toolkit.layout.processors import DisplayMultipleCursors -except ImportError: - class DisplayMultipleCursors(Processor): - " Dummy. " - def __init__(self, *a): - pass +if TYPE_CHECKING: + from .python_input import PythonInput, OptionCategory - def apply_transformation(self, document, lineno, - source_to_display, tokens): - return Transformation(tokens) +__all__ = ["PtPythonLayout", "CompletionVisualisation"] -class CompletionVisualisation: +class CompletionVisualisation(Enum): " Visualisation method for the completions. " - NONE = 'none' - POP_UP = 'pop-up' - MULTI_COLUMN = 'multi-column' - TOOLBAR = 'toolbar' + NONE = "none" + POP_UP = "pop-up" + MULTI_COLUMN = "multi-column" + TOOLBAR = "toolbar" -def show_completions_toolbar(python_input): - return Condition(lambda: python_input.completion_visualisation == CompletionVisualisation.TOOLBAR) +def show_completions_toolbar(python_input: "PythonInput") -> Condition: + return Condition( + lambda: python_input.completion_visualisation == CompletionVisualisation.TOOLBAR + ) -def show_completions_menu(python_input): - return Condition(lambda: python_input.completion_visualisation == CompletionVisualisation.POP_UP) +def show_completions_menu(python_input: "PythonInput") -> Condition: + return Condition( + lambda: python_input.completion_visualisation == CompletionVisualisation.POP_UP + ) -def show_multi_column_completions_menu(python_input): - return Condition(lambda: python_input.completion_visualisation == CompletionVisualisation.MULTI_COLUMN) +def show_multi_column_completions_menu(python_input: "PythonInput") -> Condition: + return Condition( + lambda: python_input.completion_visualisation + == CompletionVisualisation.MULTI_COLUMN + ) -def python_sidebar(python_input): +def python_sidebar(python_input: "PythonInput") -> Window: """ Create the `Layout` for the sidebar with the configurable options. """ - def get_text_fragments(): - tokens = [] - def append_category(category): - tokens.extend([ - ('class:sidebar', ' '), - ('class:sidebar.title', ' %-36s' % category.title), - ('class:sidebar', '\n'), - ]) + def get_text_fragments() -> StyleAndTextTuples: + tokens: StyleAndTextTuples = [] - def append(index, label, status): + def append_category(category: "OptionCategory") -> None: + tokens.extend( + [ + ("class:sidebar", " "), + ("class:sidebar.title", " %-36s" % category.title), + ("class:sidebar", "\n"), + ] + ) + + def append(index: int, label: str, status: str) -> None: selected = index == python_input.selected_option_index @if_mousedown - def select_item(mouse_event): + def select_item(mouse_event: MouseEvent) -> None: python_input.selected_option_index = index @if_mousedown - def goto_next(mouse_event): + def goto_next(mouse_event: MouseEvent) -> None: " Select item and go to next value. " python_input.selected_option_index = index option = python_input.selected_option option.activate_next() - sel = ',selected' if selected else '' + sel = ",selected" if selected else "" - tokens.append(('class:sidebar' + sel, ' >' if selected else ' ')) - tokens.append(('class:sidebar.label' + sel, '%-24s' % label, select_item)) - tokens.append(('class:sidebar.status' + sel, ' ', select_item)) - tokens.append(('class:sidebar.status' + sel, '%s' % status, goto_next)) + tokens.append(("class:sidebar" + sel, " >" if selected else " ")) + tokens.append(("class:sidebar.label" + sel, "%-24s" % label, select_item)) + tokens.append(("class:sidebar.status" + sel, " ", select_item)) + tokens.append(("class:sidebar.status" + sel, "%s" % status, goto_next)) if selected: - tokens.append(('[SetCursorPosition]', '')) + tokens.append(("[SetCursorPosition]", "")) - tokens.append(('class:sidebar.status' + sel, ' ' * (13 - len(status)), goto_next)) - tokens.append(('class:sidebar', '<' if selected else '')) - tokens.append(('class:sidebar', '\n')) + tokens.append( + ("class:sidebar.status" + sel, " " * (13 - len(status)), goto_next) + ) + tokens.append(("class:sidebar", "<" if selected else "")) + tokens.append(("class:sidebar", "\n")) i = 0 for category in python_input.options: append_category(category) for option in category.options: - append(i, option.title, '%s' % option.get_current_value()) + append(i, option.title, "%s" % option.get_current_value()) i += 1 tokens.pop() # Remove last newline. @@ -130,45 +157,44 @@ def move_cursor_up(self): return Window( Control(get_text_fragments), - style='class:sidebar', + style="class:sidebar", width=Dimension.exact(43), height=Dimension(min=3), - scroll_offsets=ScrollOffsets(top=1, bottom=1)) + scroll_offsets=ScrollOffsets(top=1, bottom=1), + ) def python_sidebar_navigation(python_input): """ Create the `Layout` showing the navigation information for the sidebar. """ - def get_text_fragments(): - tokens = [] + def get_text_fragments(): # Show navigation info. - tokens.extend([ - ('class:sidebar', ' '), - ('class:sidebar.key', '[Arrows]'), - ('class:sidebar', ' '), - ('class:sidebar.description', 'Navigate'), - ('class:sidebar', ' '), - ('class:sidebar.key', '[Enter]'), - ('class:sidebar', ' '), - ('class:sidebar.description', 'Hide menu'), - ]) - - return tokens + return [ + ("class:sidebar", " "), + ("class:sidebar.key", "[Arrows]"), + ("class:sidebar", " "), + ("class:sidebar.description", "Navigate"), + ("class:sidebar", " "), + ("class:sidebar.key", "[Enter]"), + ("class:sidebar", " "), + ("class:sidebar.description", "Hide menu"), + ] return Window( FormattedTextControl(get_text_fragments), - style='class:sidebar', + style="class:sidebar", width=Dimension.exact(43), - height=Dimension.exact(1)) + height=Dimension.exact(1), + ) def python_sidebar_help(python_input): """ Create the `Layout` for the help text for the current item in the sidebar. """ - token = 'class:sidebar.helptext' + token = "class:sidebar.helptext" def get_current_description(): """ @@ -180,33 +206,35 @@ def get_current_description(): if i == python_input.selected_option_index: return option.description i += 1 - return '' + return "" def get_help_text(): return [(token, get_current_description())] return ConditionalContainer( content=Window( - FormattedTextControl(get_help_text), - style=token, - height=Dimension(min=3)), - filter=ShowSidebar(python_input) & - Condition(lambda: python_input.show_sidebar_help) & ~is_done) + FormattedTextControl(get_help_text), style=token, height=Dimension(min=3) + ), + filter=ShowSidebar(python_input) + & Condition(lambda: python_input.show_sidebar_help) + & ~is_done, + ) def signature_toolbar(python_input): """ Return the `Layout` for the signature. """ + def get_text_fragments(): result = [] append = result.append - Signature = 'class:signature-toolbar' + Signature = "class:signature-toolbar" if python_input.signatures: sig = python_input.signatures[0] # Always take the first one. - append((Signature, ' ')) + append((Signature, " ")) try: append((Signature, sig.full_name)) except IndexError: @@ -214,7 +242,7 @@ def get_text_fragments(): # See also: https://github.com/davidhalter/jedi/issues/490 return [] - append((Signature + ',operator', '(')) + append((Signature + ",operator", "(")) try: enumerated_params = enumerate(sig.params) @@ -228,39 +256,45 @@ def get_text_fragments(): # and sig has no 'index' attribute. # See: https://github.com/jonathanslenders/ptpython/issues/47 # https://github.com/davidhalter/jedi/issues/598 - description = (p.description if p else '*') #or '*' - sig_index = getattr(sig, 'index', 0) + description = p.description if p else "*" # or '*' + sig_index = getattr(sig, "index", 0) if i == sig_index: # Note: we use `_Param.description` instead of # `_Param.name`, that way we also get the '*' before args. - append((Signature + ',current-name', str(description))) + append((Signature + ",current-name", str(description))) else: append((Signature, str(description))) - append((Signature + ',operator', ', ')) + append((Signature + ",operator", ", ")) if sig.params: # Pop last comma result.pop() - append((Signature + ',operator', ')')) - append((Signature, ' ')) + append((Signature + ",operator", ")")) + append((Signature, " ")) return result return ConditionalContainer( content=Window( - FormattedTextControl(get_text_fragments), - height=Dimension.exact(1)), + FormattedTextControl(get_text_fragments), height=Dimension.exact(1) + ), filter= - # Show only when there is a signature - HasSignature(python_input) & - # And there are no completions to be shown. (would cover signature pop-up.) - ~(has_completions & (show_completions_menu(python_input) | - show_multi_column_completions_menu(python_input))) - # Signature needs to be shown. - & ShowSignature(python_input) & - # Not done yet. - ~is_done) + # Show only when there is a signature + HasSignature(python_input) & + # And there are no completions to be shown. (would cover signature pop-up.) + ~( + has_completions + & ( + show_completions_menu(python_input) + | show_multi_column_completions_menu(python_input) + ) + ) + # Signature needs to be shown. + & ShowSignature(python_input) & + # Not done yet. + ~is_done, + ) class PythonPromptMargin(PromptMargin): @@ -268,6 +302,7 @@ class PythonPromptMargin(PromptMargin): Create margin that displays the prompt. It shows something like "In [1]:". """ + def __init__(self, python_input): self.python_input = python_input @@ -279,211 +314,254 @@ def get_prompt(): def get_continuation(width, line_number, is_soft_wrap): if python_input.show_line_numbers and not is_soft_wrap: - text = ('%i ' % (line_number + 1)).rjust(width) - return [('class:line-number', text)] + text = ("%i " % (line_number + 1)).rjust(width) + return [("class:line-number", text)] else: return get_prompt_style().in2_prompt(width) - super(PythonPromptMargin, self).__init__(get_prompt, get_continuation) + super().__init__(get_prompt, get_continuation) -def status_bar(python_input): +def status_bar(python_input: "PythonInput") -> Container: """ Create the `Layout` for the status bar. """ - TB = 'class:status-toolbar' + TB = "class:status-toolbar" @if_mousedown - def toggle_paste_mode(mouse_event): + def toggle_paste_mode(mouse_event: MouseEvent) -> None: python_input.paste_mode = not python_input.paste_mode @if_mousedown - def enter_history(mouse_event): + def enter_history(mouse_event: MouseEvent) -> None: python_input.enter_history() - def get_text_fragments(): + def get_text_fragments() -> StyleAndTextTuples: python_buffer = python_input.default_buffer - result = [] + result: StyleAndTextTuples = [] append = result.append - append((TB, ' ')) + append((TB, " ")) result.extend(get_inputmode_fragments(python_input)) - append((TB, ' ')) + append((TB, " ")) # Position in history. - append((TB, '%i/%i ' % (python_buffer.working_index + 1, - len(python_buffer._working_lines)))) + append( + ( + TB, + "%i/%i " + % (python_buffer.working_index + 1, len(python_buffer._working_lines)), + ) + ) # Shortcuts. app = get_app() - if not python_input.vi_mode and app.current_buffer == python_input.search_buffer: - append((TB, '[Ctrl-G] Cancel search [Enter] Go to this position.')) + if ( + not python_input.vi_mode + and app.current_buffer == python_input.search_buffer + ): + append((TB, "[Ctrl-G] Cancel search [Enter] Go to this position.")) elif bool(app.current_buffer.selection_state) and not python_input.vi_mode: # Emacs cut/copy keys. - append((TB, '[Ctrl-W] Cut [Meta-W] Copy [Ctrl-Y] Paste [Ctrl-G] Cancel')) + append((TB, "[Ctrl-W] Cut [Meta-W] Copy [Ctrl-Y] Paste [Ctrl-G] Cancel")) else: - result.extend([ - (TB + ' class:key', '[F3]', enter_history), - (TB, ' History ', enter_history), - (TB + ' class:key', '[F6]', toggle_paste_mode), - (TB, ' ', toggle_paste_mode), - ]) + result.extend( + [ + (TB + " class:key", "[F3]", enter_history), + (TB, " History ", enter_history), + (TB + " class:key", "[F6]", toggle_paste_mode), + (TB, " ", toggle_paste_mode), + ] + ) if python_input.paste_mode: - append((TB + ' class:paste-mode-on', 'Paste mode (on)', toggle_paste_mode)) + append( + (TB + " class:paste-mode-on", "Paste mode (on)", toggle_paste_mode) + ) else: - append((TB, 'Paste mode', toggle_paste_mode)) + append((TB, "Paste mode", toggle_paste_mode)) return result return ConditionalContainer( - content=Window(content=FormattedTextControl(get_text_fragments), style=TB), - filter=~is_done & renderer_height_is_known & - Condition(lambda: python_input.show_status_bar and - not python_input.show_exit_confirmation)) + content=Window(content=FormattedTextControl(get_text_fragments), style=TB), + filter=~is_done + & renderer_height_is_known + & Condition( + lambda: python_input.show_status_bar + and not python_input.show_exit_confirmation + ), + ) -def get_inputmode_fragments(python_input): +def get_inputmode_fragments(python_input: "PythonInput") -> StyleAndTextTuples: """ Return current input mode as a list of (token, text) tuples for use in a toolbar. """ app = get_app() + @if_mousedown - def toggle_vi_mode(mouse_event): + def toggle_vi_mode(mouse_event: MouseEvent) -> None: python_input.vi_mode = not python_input.vi_mode - token = 'class:status-toolbar' - input_mode_t = 'class:status-toolbar.input-mode' + token = "class:status-toolbar" + input_mode_t = "class:status-toolbar.input-mode" mode = app.vi_state.input_mode - result = [] + result: StyleAndTextTuples = [] append = result.append - append((input_mode_t, '[F4] ', toggle_vi_mode)) + append((input_mode_t, "[F4] ", toggle_vi_mode)) # InputMode if python_input.vi_mode: recording_register = app.vi_state.recording_register if recording_register: - append((token, ' ')) - append((token + ' class:record', 'RECORD({})'.format(recording_register))) - append((token, ' - ')) + append((token, " ")) + append((token + " class:record", "RECORD({})".format(recording_register))) + append((token, " - ")) - if bool(app.current_buffer.selection_state): + if app.current_buffer.selection_state is not None: if app.current_buffer.selection_state.type == SelectionType.LINES: - append((input_mode_t, 'Vi (VISUAL LINE)', toggle_vi_mode)) + append((input_mode_t, "Vi (VISUAL LINE)", toggle_vi_mode)) elif app.current_buffer.selection_state.type == SelectionType.CHARACTERS: - append((input_mode_t, 'Vi (VISUAL)', toggle_vi_mode)) - append((token, ' ')) - elif app.current_buffer.selection_state.type == 'BLOCK': - append((input_mode_t, 'Vi (VISUAL BLOCK)', toggle_vi_mode)) - append((token, ' ')) - elif mode in (InputMode.INSERT, 'vi-insert-multiple'): - append((input_mode_t, 'Vi (INSERT)', toggle_vi_mode)) - append((token, ' ')) + append((input_mode_t, "Vi (VISUAL)", toggle_vi_mode)) + append((token, " ")) + elif app.current_buffer.selection_state.type == "BLOCK": + append((input_mode_t, "Vi (VISUAL BLOCK)", toggle_vi_mode)) + append((token, " ")) + elif mode in (InputMode.INSERT, "vi-insert-multiple"): + append((input_mode_t, "Vi (INSERT)", toggle_vi_mode)) + append((token, " ")) elif mode == InputMode.NAVIGATION: - append((input_mode_t, 'Vi (NAV)', toggle_vi_mode)) - append((token, ' ')) + append((input_mode_t, "Vi (NAV)", toggle_vi_mode)) + append((token, " ")) elif mode == InputMode.REPLACE: - append((input_mode_t, 'Vi (REPLACE)', toggle_vi_mode)) - append((token, ' ')) + append((input_mode_t, "Vi (REPLACE)", toggle_vi_mode)) + append((token, " ")) else: if app.emacs_state.is_recording: - append((token, ' ')) - append((token + ' class:record', 'RECORD')) - append((token, ' - ')) + append((token, " ")) + append((token + " class:record", "RECORD")) + append((token, " - ")) - append((input_mode_t, 'Emacs', toggle_vi_mode)) - append((token, ' ')) + append((input_mode_t, "Emacs", toggle_vi_mode)) + append((token, " ")) return result -def show_sidebar_button_info(python_input): +def show_sidebar_button_info(python_input: "PythonInput") -> Container: """ Create `Layout` for the information in the right-bottom corner. (The right part of the status bar.) """ + @if_mousedown - def toggle_sidebar(mouse_event): + def toggle_sidebar(mouse_event: MouseEvent) -> None: " Click handler for the menu. " python_input.show_sidebar = not python_input.show_sidebar version = sys.version_info - tokens = [ - ('class:status-toolbar.key', '[F2]', toggle_sidebar), - ('class:status-toolbar', ' Menu', toggle_sidebar), - ('class:status-toolbar', ' - '), - ('class:status-toolbar.python-version', '%s %i.%i.%i' % (platform.python_implementation(), - version[0], version[1], version[2])), - ('class:status-toolbar', ' '), + tokens: StyleAndTextTuples = [ + ("class:status-toolbar.key", "[F2]", toggle_sidebar), + ("class:status-toolbar", " Menu", toggle_sidebar), + ("class:status-toolbar", " - "), + ( + "class:status-toolbar.python-version", + "%s %i.%i.%i" + % (platform.python_implementation(), version[0], version[1], version[2]), + ), + ("class:status-toolbar", " "), ] width = fragment_list_width(tokens) - def get_text_fragments(): + def get_text_fragments() -> StyleAndTextTuples: # Python version return tokens return ConditionalContainer( content=Window( FormattedTextControl(get_text_fragments), - style='class:status-toolbar', + style="class:status-toolbar", height=Dimension.exact(1), - width=Dimension.exact(width)), - filter=~is_done & renderer_height_is_known & - Condition(lambda: python_input.show_status_bar and - not python_input.show_exit_confirmation)) - - -def exit_confirmation(python_input, style='class:exit-confirmation'): + width=Dimension.exact(width), + ), + filter=~is_done + & renderer_height_is_known + & Condition( + lambda: python_input.show_status_bar + and not python_input.show_exit_confirmation + ), + ) + + +def exit_confirmation( + python_input: "PythonInput", style="class:exit-confirmation" +) -> Container: """ Create `Layout` for the exit message. """ + def get_text_fragments(): # Show "Do you really want to exit?" return [ - (style, '\n %s ([y]/n)' % python_input.exit_message), - ('[SetCursorPosition]', ''), - (style, ' \n'), + (style, "\n %s ([y]/n)" % python_input.exit_message), + ("[SetCursorPosition]", ""), + (style, " \n"), ] visible = ~is_done & Condition(lambda: python_input.show_exit_confirmation) return ConditionalContainer( - content=Window(FormattedTextControl(get_text_fragments), style=style), # , has_focus=visible)), - filter=visible) + content=Window( + FormattedTextControl(get_text_fragments), style=style + ), # , has_focus=visible)), + filter=visible, + ) -def meta_enter_message(python_input): +def meta_enter_message(python_input: "PythonInput") -> Container: """ Create the `Layout` for the 'Meta+Enter` message. """ - def get_text_fragments(): - return [('class:accept-message', ' [Meta+Enter] Execute ')] - def extra_condition(): + def get_text_fragments() -> StyleAndTextTuples: + return [("class:accept-message", " [Meta+Enter] Execute ")] + + @Condition + def extra_condition() -> bool: " Only show when... " b = python_input.default_buffer return ( - python_input.show_meta_enter_message and - (not b.document.is_cursor_at_the_end or - python_input.accept_input_on_enter is None) and - '\n' in b.text) + python_input.show_meta_enter_message + and ( + not b.document.is_cursor_at_the_end + or python_input.accept_input_on_enter is None + ) + and "\n" in b.text + ) - visible = ~is_done & has_focus(DEFAULT_BUFFER) & Condition(extra_condition) + visible = ~is_done & has_focus(DEFAULT_BUFFER) & extra_condition return ConditionalContainer( - content=Window(FormattedTextControl(get_text_fragments)), - filter=visible) - - -class PtPythonLayout(object): - def __init__(self, python_input, lexer=PythonLexer, extra_body=None, - extra_toolbars=None, extra_buffer_processors=None, - input_buffer_height=None): + content=Window(FormattedTextControl(get_text_fragments)), filter=visible + ) + + +class PtPythonLayout: + def __init__( + self, + python_input: "PythonInput", + lexer=PythonLexer, + extra_body=None, + extra_toolbars=None, + extra_buffer_processors=None, + input_buffer_height: Optional[AnyDimension] = None, + ): D = Dimension extra_body = [extra_body] if extra_body else [] extra_toolbars = extra_toolbars or [] @@ -514,21 +592,26 @@ def menu_position(): input_processors=[ ConditionalProcessor( processor=HighlightIncrementalSearchProcessor(), - filter=has_focus(SEARCH_BUFFER) | has_focus(search_toolbar.control), + filter=has_focus(SEARCH_BUFFER) + | has_focus(search_toolbar.control), ), HighlightSelectionProcessor(), DisplayMultipleCursors(), # Show matching parentheses, but only while editing. ConditionalProcessor( - processor=HighlightMatchingBracketProcessor(chars='[](){}'), - filter=has_focus(DEFAULT_BUFFER) & ~is_done & - Condition(lambda: python_input.highlight_matching_parenthesis)), + processor=HighlightMatchingBracketProcessor(chars="[](){}"), + filter=has_focus(DEFAULT_BUFFER) + & ~is_done + & Condition( + lambda: python_input.highlight_matching_parenthesis + ), + ), ConditionalProcessor( - processor=AppendAutoSuggestion(), - filter=~is_done) - ] + extra_buffer_processors, + processor=AppendAutoSuggestion(), filter=~is_done + ), + ] + + extra_buffer_processors, menu_position=menu_position, - # Make sure that we always see the result of an reverse-i-search: preview_search=True, ), @@ -538,85 +621,134 @@ def menu_position(): # which is a float. scroll_offsets=ScrollOffsets(bottom=1, left=4, right=4), # As long as we're editing, prefer a minimal height of 6. - height=(lambda: ( - None if get_app().is_done or python_input.show_exit_confirmation - else input_buffer_height)), + height=( + lambda: ( + None + if get_app().is_done or python_input.show_exit_confirmation + else input_buffer_height + ) + ), wrap_lines=Condition(lambda: python_input.wrap_lines), ) sidebar = python_sidebar(python_input) - root_container = HSplit([ - VSplit([ - HSplit([ - FloatContainer( - content=HSplit( - [create_python_input_window()] + extra_body + root_container = HSplit( + [ + VSplit( + [ + HSplit( + [ + FloatContainer( + content=HSplit( + [create_python_input_window()] + extra_body + ), + floats=[ + Float( + xcursor=True, + ycursor=True, + content=ConditionalContainer( + content=CompletionsMenu( + scroll_offset=( + lambda: python_input.completion_menu_scroll_offset + ), + max_height=12, + ), + filter=show_completions_menu( + python_input + ), + ), + ), + Float( + xcursor=True, + ycursor=True, + content=ConditionalContainer( + content=MultiColumnCompletionsMenu(), + filter=show_multi_column_completions_menu( + python_input + ), + ), + ), + Float( + xcursor=True, + ycursor=True, + content=signature_toolbar(python_input), + ), + Float( + left=2, + bottom=1, + content=exit_confirmation(python_input), + ), + Float( + bottom=0, + right=0, + height=1, + content=meta_enter_message(python_input), + hide_when_covering_content=True, + ), + Float( + bottom=1, + left=1, + right=0, + content=python_sidebar_help(python_input), + ), + ], + ), + ArgToolbar(), + search_toolbar, + SystemToolbar(), + ValidationToolbar(), + ConditionalContainer( + content=CompletionsToolbar(), + filter=show_completions_toolbar(python_input) + & ~is_done, + ), + # Docstring region. + ConditionalContainer( + content=Window( + height=D.exact(1), + char="\u2500", + style="class:separator", + ), + filter=HasSignature(python_input) + & ShowDocstring(python_input) + & ~is_done, + ), + ConditionalContainer( + content=Window( + BufferControl( + buffer=python_input.docstring_buffer, + lexer=SimpleLexer(style="class:docstring"), + # lexer=PythonLexer, + ), + height=D(max=12), + ), + filter=HasSignature(python_input) + & ShowDocstring(python_input) + & ~is_done, + ), + ] ), - floats=[ - Float(xcursor=True, - ycursor=True, - content=ConditionalContainer( - content=CompletionsMenu( - scroll_offset=( - lambda: python_input.completion_menu_scroll_offset), - max_height=12), - filter=show_completions_menu(python_input))), - Float(xcursor=True, - ycursor=True, - content=ConditionalContainer( - content=MultiColumnCompletionsMenu(), - filter=show_multi_column_completions_menu(python_input))), - Float(xcursor=True, - ycursor=True, - content=signature_toolbar(python_input)), - Float(left=2, - bottom=1, - content=exit_confirmation(python_input)), - Float(bottom=0, right=0, height=1, - content=meta_enter_message(python_input), - hide_when_covering_content=True), - Float(bottom=1, left=1, right=0, content=python_sidebar_help(python_input)), - ]), - ArgToolbar(), - search_toolbar, - SystemToolbar(), - ValidationToolbar(), - ConditionalContainer( - content=CompletionsToolbar(), - filter=show_completions_toolbar(python_input)), - - # Docstring region. - ConditionalContainer( - content=Window( - height=D.exact(1), - char='\u2500', - style='class:separator'), - filter=HasSignature(python_input) & ShowDocstring(python_input) & ~is_done), - ConditionalContainer( - content=Window( - BufferControl( - buffer=python_input.docstring_buffer, - lexer=SimpleLexer(style='class:docstring'), - #lexer=PythonLexer, + ConditionalContainer( + content=HSplit( + [ + sidebar, + Window(style="class:sidebar,separator", height=1), + python_sidebar_navigation(python_input), + ] ), - height=D(max=12)), - filter=HasSignature(python_input) & ShowDocstring(python_input) & ~is_done), - ]), - ConditionalContainer( - content=HSplit([ - sidebar, - Window(style='class:sidebar,separator', height=1), - python_sidebar_navigation(python_input), - ]), - filter=ShowSidebar(python_input) & ~is_done) - ]), - ] + extra_toolbars + [ - VSplit([ - status_bar(python_input), - show_sidebar_button_info(python_input), - ]) - ]) + filter=ShowSidebar(python_input) & ~is_done, + ), + ] + ) + ] + + extra_toolbars + + [ + VSplit( + [status_bar(python_input), show_sidebar_button_info(python_input)] + ) + ] + ) self.layout = Layout(root_container) self.sidebar = sidebar diff --git a/ptpython/prompt_style.py b/ptpython/prompt_style.py index 58514afe..d5e6ca8c 100644 --- a/ptpython/prompt_style.py +++ b/ptpython/prompt_style.py @@ -1,25 +1,26 @@ -from __future__ import unicode_literals from abc import ABCMeta, abstractmethod -from six import with_metaclass +from typing import TYPE_CHECKING -__all__ = ( - 'PromptStyle', - 'IPythonPrompt', - 'ClassicPrompt', -) +from prompt_toolkit.formatted_text import StyleAndTextTuples +if TYPE_CHECKING: + from .python_input import PythonInput -class PromptStyle(with_metaclass(ABCMeta, object)): +__all__ = ["PromptStyle", "IPythonPrompt", "ClassicPrompt"] + + +class PromptStyle(metaclass=ABCMeta): """ Base class for all prompts. """ + @abstractmethod - def in_prompt(self): + def in_prompt(self) -> StyleAndTextTuples: " Return the input tokens. " return [] @abstractmethod - def in2_prompt(self, width): + def in2_prompt(self, width: int) -> StyleAndTextTuples: """ Tokens for every following input line. @@ -29,7 +30,7 @@ def in2_prompt(self, width): return [] @abstractmethod - def out_prompt(self): + def out_prompt(self) -> StyleAndTextTuples: " Return the output tokens. " return [] @@ -38,27 +39,26 @@ class IPythonPrompt(PromptStyle): """ A prompt resembling the IPython prompt. """ - def __init__(self, python_input): + + def __init__(self, python_input: "PythonInput") -> None: self.python_input = python_input - def in_prompt(self): + def in_prompt(self) -> StyleAndTextTuples: return [ - ('class:in', 'In ['), - ('class:in.number', '%s' % self.python_input.current_statement_index), - ('class:in', ']: '), + ("class:in", "In ["), + ("class:in.number", "%s" % self.python_input.current_statement_index), + ("class:in", "]: "), ] - def in2_prompt(self, width): - return [ - ('class:in', '...: '.rjust(width)), - ] + def in2_prompt(self, width: int) -> StyleAndTextTuples: + return [("class:in", "...: ".rjust(width))] - def out_prompt(self): + def out_prompt(self) -> StyleAndTextTuples: return [ - ('class:out', 'Out['), - ('class:out.number', '%s' % self.python_input.current_statement_index), - ('class:out', ']:'), - ('', ' '), + ("class:out", "Out["), + ("class:out.number", "%s" % self.python_input.current_statement_index), + ("class:out", "]:"), + ("", " "), ] @@ -66,11 +66,12 @@ class ClassicPrompt(PromptStyle): """ The classic Python prompt. """ - def in_prompt(self): - return [('class:prompt', '>>> ')] - def in2_prompt(self, width): - return [('class:prompt.dots', '...')] + def in_prompt(self) -> StyleAndTextTuples: + return [("class:prompt", ">>> ")] + + def in2_prompt(self, width: int) -> StyleAndTextTuples: + return [("class:prompt.dots", "...")] - def out_prompt(self): + def out_prompt(self) -> StyleAndTextTuples: return [] diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 2c855ba9..c4bbbd0c 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -2,66 +2,79 @@ Application for reading Python input. This can be used for creation of Python REPLs. """ -from __future__ import unicode_literals +import __future__ + +from asyncio import get_event_loop +from functools import partial +from typing import Any, Callable, Dict, Generic, List, Optional, TypeVar from prompt_toolkit.application import Application, get_app -from prompt_toolkit.application.run_in_terminal import run_coroutine_in_terminal -from prompt_toolkit.auto_suggest import AutoSuggestFromHistory, ConditionalAutoSuggest, ThreadedAutoSuggest +from prompt_toolkit.auto_suggest import ( + AutoSuggestFromHistory, + ConditionalAutoSuggest, + ThreadedAutoSuggest, +) from prompt_toolkit.buffer import Buffer -from prompt_toolkit.key_binding.bindings.auto_suggest import load_auto_suggest_bindings -from prompt_toolkit.key_binding.bindings.open_in_editor import load_open_in_editor_bindings -from prompt_toolkit.completion import ThreadedCompleter +from prompt_toolkit.completion import Completer, FuzzyCompleter, ThreadedCompleter from prompt_toolkit.document import Document from prompt_toolkit.enums import DEFAULT_BUFFER, EditingMode -from prompt_toolkit.eventloop.defaults import get_event_loop from prompt_toolkit.filters import Condition -from prompt_toolkit.history import FileHistory, InMemoryHistory, ThreadedHistory -from prompt_toolkit.input.defaults import create_input -from prompt_toolkit.key_binding import merge_key_bindings, ConditionalKeyBindings, KeyBindings +from prompt_toolkit.history import ( + FileHistory, + History, + InMemoryHistory, + ThreadedHistory, +) +from prompt_toolkit.input import Input +from prompt_toolkit.key_binding import ( + ConditionalKeyBindings, + KeyBindings, + merge_key_bindings, +) +from prompt_toolkit.key_binding.bindings.auto_suggest import load_auto_suggest_bindings +from prompt_toolkit.key_binding.bindings.open_in_editor import ( + load_open_in_editor_bindings, +) from prompt_toolkit.key_binding.vi_state import InputMode -from prompt_toolkit.lexers import PygmentsLexer, DynamicLexer, SimpleLexer -from prompt_toolkit.output import ColorDepth -from prompt_toolkit.output.defaults import create_output -from prompt_toolkit.styles import DynamicStyle, SwapLightAndDarkStyleTransformation, ConditionalStyleTransformation, AdjustBrightnessStyleTransformation, merge_style_transformations +from prompt_toolkit.lexers import DynamicLexer, Lexer, PygmentsLexer, SimpleLexer +from prompt_toolkit.output import ColorDepth, Output +from prompt_toolkit.styles import ( + AdjustBrightnessStyleTransformation, + BaseStyle, + ConditionalStyleTransformation, + DynamicStyle, + SwapLightAndDarkStyleTransformation, + merge_style_transformations, +) from prompt_toolkit.utils import is_windows -from prompt_toolkit.validation import ConditionalValidator -from prompt_toolkit.completion import FuzzyCompleter +from prompt_toolkit.validation import ConditionalValidator, Validator +from pygments.lexers import Python3Lexer as PythonLexer from .completer import PythonCompleter -from .history_browser import History -from .key_bindings import load_python_bindings, load_sidebar_bindings, load_confirm_exit_bindings -from .layout import PtPythonLayout, CompletionVisualisation -from .prompt_style import IPythonPrompt, ClassicPrompt -from .style import get_all_code_styles, get_all_ui_styles, generate_style +from .history_browser import PythonHistory +from .key_bindings import ( + load_confirm_exit_bindings, + load_python_bindings, + load_sidebar_bindings, +) +from .layout import CompletionVisualisation, PtPythonLayout +from .prompt_style import ClassicPrompt, IPythonPrompt, PromptStyle +from .style import generate_style, get_all_code_styles, get_all_ui_styles from .utils import get_jedi_script_from_document from .validator import PythonValidator -from functools import partial +__all__ = ["PythonInput"] -import sys -import six -import __future__ - -if six.PY2: - from pygments.lexers import PythonLexer -else: - from pygments.lexers import Python3Lexer as PythonLexer +_T = TypeVar("_T") -__all__ = ( - 'PythonInput', -) - - -class OptionCategory(object): - def __init__(self, title, options): - assert isinstance(title, six.text_type) - assert isinstance(options, list) +class OptionCategory: + def __init__(self, title: str, options: List["Option"]) -> None: self.title = title self.options = options -class Option(object): +class Option(Generic[_T]): """ Ptpython configuration option that can be shown and modified from the sidebar. @@ -72,22 +85,26 @@ class Option(object): possible values to callbacks that activate these value. :param get_current_value: Callable that returns the current, active value. """ - def __init__(self, title, description, get_current_value, get_values): - assert isinstance(title, six.text_type) - assert isinstance(description, six.text_type) - assert callable(get_current_value) - assert callable(get_values) + def __init__( + self, + title: str, + description: str, + get_current_value: Callable[[], _T], + # We accept `object` as return type for the select functions, because + # often they return an unused boolean. Maybe this can be improved. + get_values: Callable[[], Dict[_T, Callable[[], object]]], + ) -> None: self.title = title self.description = description self.get_current_value = get_current_value self.get_values = get_values @property - def values(self): + def values(self) -> Dict[_T, Callable[[], object]]: return self.get_values() - def activate_next(self, _previous=False): + def activate_next(self, _previous: bool = False) -> None: """ Activate next value. """ @@ -110,7 +127,7 @@ def activate_next(self, _previous=False): next_option = options[index % len(options)] self.values[next_option]() - def activate_previous(self): + def activate_previous(self) -> None: """ Activate previous value. """ @@ -118,14 +135,17 @@ def activate_previous(self): COLOR_DEPTHS = { - ColorDepth.DEPTH_1_BIT: 'Monochrome', - ColorDepth.DEPTH_4_BIT: 'ANSI Colors', - ColorDepth.DEPTH_8_BIT: '256 colors', - ColorDepth.DEPTH_24_BIT: 'True color', + ColorDepth.DEPTH_1_BIT: "Monochrome", + ColorDepth.DEPTH_4_BIT: "ANSI Colors", + ColorDepth.DEPTH_8_BIT: "256 colors", + ColorDepth.DEPTH_24_BIT: "True color", } +_Namespace = Dict[str, Any] +_GetNamespace = Callable[[], _Namespace] -class PythonInput(object): + +class PythonInput: """ Prompt for reading Python input. @@ -134,31 +154,43 @@ class PythonInput(object): python_input = PythonInput(...) python_code = python_input.app.run() """ - def __init__(self, - get_globals=None, get_locals=None, history_filename=None, - vi_mode=False, - - input=None, - output=None, - color_depth=None, - # For internal use. - extra_key_bindings=None, - _completer=None, _validator=None, - _lexer=None, _extra_buffer_processors=None, - _extra_layout_body=None, _extra_toolbars=None, - _input_buffer_height=None): - - self.get_globals = get_globals or (lambda: {}) - self.get_locals = get_locals or self.get_globals + def __init__( + self, + get_globals: Optional[_GetNamespace] = None, + get_locals: Optional[_GetNamespace] = None, + history_filename: Optional[str] = None, + vi_mode: bool = False, + color_depth: Optional[ColorDepth] = None, + # Input/output. + input: Optional[Input] = None, + output: Optional[Output] = None, + # For internal use. + extra_key_bindings: Optional[KeyBindings] = None, + _completer: Optional[Completer] = None, + _validator: Optional[Validator] = None, + _lexer: Optional[Lexer] = None, + _extra_buffer_processors=None, + _extra_layout_body=None, + _extra_toolbars=None, + _input_buffer_height=None, + ) -> None: + + self.get_globals: _GetNamespace = get_globals or (lambda: {}) + self.get_locals: _GetNamespace = get_locals or self.get_globals self._completer = _completer or FuzzyCompleter( - PythonCompleter(self.get_globals, self.get_locals, - lambda: self.enable_dictionary_completion), - enable_fuzzy=Condition(lambda: self.enable_fuzzy_completion)) + PythonCompleter( + self.get_globals, + self.get_locals, + lambda: self.enable_dictionary_completion, + ), + enable_fuzzy=Condition(lambda: self.enable_fuzzy_completion), + ) self._validator = _validator or PythonValidator(self.get_compiler_flags) self._lexer = _lexer or PygmentsLexer(PythonLexer) + self.history: History if history_filename: self.history = ThreadedHistory(FileHistory(history_filename)) else: @@ -172,128 +204,142 @@ def __init__(self, self.extra_key_bindings = extra_key_bindings or KeyBindings() # Settings. - self.show_signature = False - self.show_docstring = False - self.show_meta_enter_message = True - self.completion_visualisation = CompletionVisualisation.MULTI_COLUMN - self.completion_menu_scroll_offset = 1 - - self.show_line_numbers = False - self.show_status_bar = True - self.wrap_lines = True - self.complete_while_typing = True - self.paste_mode = False # When True, don't insert whitespace after newline. - self.confirm_exit = True # Ask for confirmation when Control-D is pressed. - self.accept_input_on_enter = 2 # Accept when pressing Enter 'n' times. - # 'None' means that meta-enter is always required. - self.enable_open_in_editor = True - self.enable_system_bindings = True - self.enable_input_validation = True - self.enable_auto_suggest = False - self.enable_mouse_support = False - self.enable_history_search = False # When True, like readline, going - # back in history will filter the - # history on the records starting - # with the current input. - - self.enable_syntax_highlighting = True - self.enable_fuzzy_completion = False - self.enable_dictionary_completion = False - self.swap_light_and_dark = False - self.highlight_matching_parenthesis = False - self.show_sidebar = False # Currently show the sidebar. - self.show_sidebar_help = True # When the sidebar is visible, also show the help text. - self.show_exit_confirmation = False # Currently show 'Do you really want to exit?' - self.terminal_title = None # The title to be displayed in the terminal. (None or string.) - self.exit_message = 'Do you really want to exit?' - self.insert_blank_line_after_output = True # (For the REPL.) + self.show_signature: bool = False + self.show_docstring: bool = False + self.show_meta_enter_message: bool = True + self.completion_visualisation: CompletionVisualisation = CompletionVisualisation.MULTI_COLUMN + self.completion_menu_scroll_offset: int = 1 + + self.show_line_numbers: bool = False + self.show_status_bar: bool = True + self.wrap_lines: bool = True + self.complete_while_typing: bool = True + self.paste_mode: bool = False # When True, don't insert whitespace after newline. + self.confirm_exit: bool = True # Ask for confirmation when Control-D is pressed. + self.accept_input_on_enter: int = 2 # Accept when pressing Enter 'n' times. + # 'None' means that meta-enter is always required. + self.enable_open_in_editor: bool = True + self.enable_system_bindings: bool = True + self.enable_input_validation: bool = True + self.enable_auto_suggest: bool = False + self.enable_mouse_support: bool = False + self.enable_history_search: bool = False # When True, like readline, going + # back in history will filter the + # history on the records starting + # with the current input. + + self.enable_syntax_highlighting: bool = True + self.enable_fuzzy_completion: bool = False + self.enable_dictionary_completion: bool = False + self.swap_light_and_dark: bool = False + self.highlight_matching_parenthesis: bool = False + self.show_sidebar: bool = False # Currently show the sidebar. + + # When the sidebar is visible, also show the help text. + self.show_sidebar_help: bool = True + + # Currently show 'Do you really want to exit?' + self.show_exit_confirmation: bool = False + + # The title to be displayed in the terminal. (None or string.) + self.terminal_title: Optional[str] = None + + self.exit_message: str = "Do you really want to exit?" + self.insert_blank_line_after_output: bool = True # (For the REPL.) # The buffers. self.default_buffer = self._create_buffer() - self.search_buffer = Buffer() - self.docstring_buffer = Buffer(read_only=True) + self.search_buffer: Buffer = Buffer() + self.docstring_buffer: Buffer = Buffer(read_only=True) # Tokens to be shown at the prompt. - self.prompt_style = 'classic' # The currently active style. + self.prompt_style: str = "classic" # The currently active style. - self.all_prompt_styles = { # Styles selectable from the menu. - 'ipython': IPythonPrompt(self), - 'classic': ClassicPrompt(), + # Styles selectable from the menu. + self.all_prompt_styles: Dict[str, PromptStyle] = { + "ipython": IPythonPrompt(self), + "classic": ClassicPrompt(), } - self.get_input_prompt = lambda: \ - self.all_prompt_styles[self.prompt_style].in_prompt() + self.get_input_prompt = lambda: self.all_prompt_styles[ + self.prompt_style + ].in_prompt() - self.get_output_prompt = lambda: \ - self.all_prompt_styles[self.prompt_style].out_prompt() + self.get_output_prompt = lambda: self.all_prompt_styles[ + self.prompt_style + ].out_prompt() #: Load styles. - self.code_styles = get_all_code_styles() + self.code_styles: Dict[str, BaseStyle] = get_all_code_styles() self.ui_styles = get_all_ui_styles() - self._current_code_style_name = 'default' - self._current_ui_style_name = 'default' + self._current_code_style_name: str = "default" + self._current_ui_style_name: str = "default" if is_windows(): - self._current_code_style_name = 'win32' + self._current_code_style_name = "win32" self._current_style = self._generate_style() - self.color_depth = color_depth or ColorDepth.default() + self.color_depth: ColorDepth = color_depth or ColorDepth.default() - self.max_brightness = 1.0 - self.min_brightness = 0.0 + self.max_brightness: float = 1.0 + self.min_brightness: float = 0.0 # Options to be configurable from the sidebar. self.options = self._create_options() - self.selected_option_index = 0 + self.selected_option_index: int = 0 #: Incremeting integer counting the current statement. - self.current_statement_index = 1 + self.current_statement_index: int = 1 # Code signatures. (This is set asynchronously after a timeout.) - self.signatures = [] + self.signatures: List[Any] = [] # Boolean indicating whether we have a signatures thread running. # (Never run more than one at the same time.) - self._get_signatures_thread_running = False - - self.output = output or create_output() - self.input = input or create_input(sys.stdin) - - self.style_transformation = merge_style_transformations([ - ConditionalStyleTransformation( - SwapLightAndDarkStyleTransformation(), - filter=Condition(lambda: self.swap_light_and_dark)), - AdjustBrightnessStyleTransformation( - lambda: self.min_brightness, - lambda: self.max_brightness), - ]) + self._get_signatures_thread_running: bool = False + + self.style_transformation = merge_style_transformations( + [ + ConditionalStyleTransformation( + SwapLightAndDarkStyleTransformation(), + filter=Condition(lambda: self.swap_light_and_dark), + ), + AdjustBrightnessStyleTransformation( + lambda: self.min_brightness, lambda: self.max_brightness + ), + ] + ) self.ptpython_layout = PtPythonLayout( self, lexer=DynamicLexer( - lambda: self._lexer if self.enable_syntax_highlighting else SimpleLexer()), + lambda: self._lexer + if self.enable_syntax_highlighting + else SimpleLexer() + ), input_buffer_height=self._input_buffer_height, extra_buffer_processors=self._extra_buffer_processors, extra_body=self._extra_layout_body, - extra_toolbars=self._extra_toolbars) + extra_toolbars=self._extra_toolbars, + ) self.app = self._create_application() if vi_mode: self.app.editing_mode = EditingMode.VI - def _accept_handler(self, buff): + def _accept_handler(self, buff: Buffer) -> bool: app = get_app() app.exit(result=buff.text) app.pre_run_callables.append(buff.reset) return True # Keep text, we call 'reset' later on. @property - def option_count(self): + def option_count(self) -> int: " Return the total amount of options. (In all categories together.) " return sum(len(category.options) for category in self.options) @property - def selected_option(self): + def selected_option(self) -> Option: " Return the currently selected option. " i = 0 for category in self.options: @@ -303,7 +349,9 @@ def selected_option(self): else: i += 1 - def get_compiler_flags(self): + raise ValueError("Nothing selected") + + def get_compiler_flags(self) -> int: """ Give the current compiler flags by looking for _Feature instances in the globals. @@ -317,7 +365,7 @@ def get_compiler_flags(self): return flags @property - def add_key_binding(self): + def add_key_binding(self) -> Callable[[_T], _T]: """ Shortcut for adding new key bindings. (Mostly useful for a config.py file, that receives @@ -329,20 +377,19 @@ def add_key_binding(self): def handler(event): ... """ + def add_binding_decorator(*k, **kw): return self.extra_key_bindings.add(*k, **kw) + return add_binding_decorator - def install_code_colorscheme(self, name, style_dict): + def install_code_colorscheme(self, name: str, style: BaseStyle) -> None: """ Install a new code color scheme. """ - assert isinstance(name, six.text_type) - assert isinstance(style_dict, dict) + self.code_styles[name] = style - self.code_styles[name] = style_dict - - def use_code_colorscheme(self, name): + def use_code_colorscheme(self, name: str) -> None: """ Apply new colorscheme. (By name.) """ @@ -351,16 +398,13 @@ def use_code_colorscheme(self, name): self._current_code_style_name = name self._current_style = self._generate_style() - def install_ui_colorscheme(self, name, style_dict): + def install_ui_colorscheme(self, name: str, style: BaseStyle) -> None: """ Install a new UI color scheme. """ - assert isinstance(name, six.text_type) - assert isinstance(style_dict, dict) - - self.ui_styles[name] = style_dict + self.ui_styles[name] = style - def use_ui_colorscheme(self, name): + def use_ui_colorscheme(self, name: str) -> None: """ Apply new colorscheme. (By name.) """ @@ -369,43 +413,48 @@ def use_ui_colorscheme(self, name): self._current_ui_style_name = name self._current_style = self._generate_style() - def _use_color_depth(self, depth): + def _use_color_depth(self, depth: ColorDepth) -> None: self.color_depth = depth - def _set_min_brightness(self, value): + def _set_min_brightness(self, value: float) -> None: self.min_brightness = value self.max_brightness = max(self.max_brightness, value) - def _set_max_brightness(self, value): + def _set_max_brightness(self, value: float) -> None: self.max_brightness = value self.min_brightness = min(self.min_brightness, value) - def _generate_style(self): + def _generate_style(self) -> BaseStyle: """ Create new Style instance. (We don't want to do this on every key press, because each time the renderer receives a new style class, he will redraw everything.) """ - return generate_style(self.code_styles[self._current_code_style_name], - self.ui_styles[self._current_ui_style_name]) + return generate_style( + self.code_styles[self._current_code_style_name], + self.ui_styles[self._current_ui_style_name], + ) - def _create_options(self): + def _create_options(self) -> List[OptionCategory]: """ Create a list of `Option` instances for the options sidebar. """ - def enable(attribute, value=True): + + def enable(attribute: str, value: Any = True) -> bool: setattr(self, attribute, value) # Return `True`, to be able to chain this in the lambdas below. return True - def disable(attribute): + def disable(attribute: str) -> bool: setattr(self, attribute, False) return True - def simple_option(title, description, field_name, values=None): + def simple_option( + title: str, description: str, field_name: str, values: Optional[List] = None + ) -> Option: " Create Simple on/of option. " - values = values or ['off', 'on'] + values = values or ["off", "on"] def get_current_value(): return values[bool(getattr(self, field_name))] @@ -416,195 +465,300 @@ def get_values(): values[0]: lambda: disable(field_name), } - return Option(title=title, description=description, - get_values=get_values, - get_current_value=get_current_value) + return Option( + title=title, + description=description, + get_values=get_values, + get_current_value=get_current_value, + ) brightness_values = [1.0 / 20 * value for value in range(0, 21)] return [ - OptionCategory('Input', [ - simple_option(title='Editing mode', - description='Vi or emacs key bindings.', - field_name='vi_mode', - values=[EditingMode.EMACS, EditingMode.VI]), - simple_option(title='Paste mode', - description="When enabled, don't indent automatically.", - field_name='paste_mode'), - Option(title='Complete while typing', - description="Generate autocompletions automatically while typing. " - 'Don\'t require pressing TAB. (Not compatible with "History search".)', - get_current_value=lambda: ['off', 'on'][self.complete_while_typing], - get_values=lambda: { - 'on': lambda: enable('complete_while_typing') and disable('enable_history_search'), - 'off': lambda: disable('complete_while_typing'), - }), - Option(title='Enable fuzzy completion', - description="Enable fuzzy completion.", - get_current_value=lambda: ['off', 'on'][self.enable_fuzzy_completion], - get_values=lambda: { - 'on': lambda: enable('enable_fuzzy_completion'), - 'off': lambda: disable('enable_fuzzy_completion'), - }), - Option(title='Dictionary completion', - description='Enable experimental dictionary completion.\n' - 'WARNING: this does "eval" on fragments of\n' - ' your Python input and is\n' - ' potentially unsafe.', - get_current_value=lambda: ['off', 'on'][self.enable_dictionary_completion], - get_values=lambda: { - 'on': lambda: enable('enable_dictionary_completion'), - 'off': lambda: disable('enable_dictionary_completion'), - }), - Option(title='History search', - description='When pressing the up-arrow, filter the history on input starting ' - 'with the current text. (Not compatible with "Complete while typing".)', - get_current_value=lambda: ['off', 'on'][self.enable_history_search], - get_values=lambda: { - 'on': lambda: enable('enable_history_search') and disable('complete_while_typing'), - 'off': lambda: disable('enable_history_search'), - }), - simple_option(title='Mouse support', - description='Respond to mouse clicks and scrolling for positioning the cursor, ' - 'selecting text and scrolling through windows.', - field_name='enable_mouse_support'), - simple_option(title='Confirm on exit', - description='Require confirmation when exiting.', - field_name='confirm_exit'), - simple_option(title='Input validation', - description='In case of syntax errors, move the cursor to the error ' - 'instead of showing a traceback of a SyntaxError.', - field_name='enable_input_validation'), - simple_option(title='Auto suggestion', - description='Auto suggest inputs by looking at the history. ' - 'Pressing right arrow or Ctrl-E will complete the entry.', - field_name='enable_auto_suggest'), - Option(title='Accept input on enter', - description='Amount of ENTER presses required to execute input when the cursor ' - 'is at the end of the input. (Note that META+ENTER will always execute.)', - get_current_value=lambda: str(self.accept_input_on_enter or 'meta-enter'), - get_values=lambda: { - '2': lambda: enable('accept_input_on_enter', 2), - '3': lambda: enable('accept_input_on_enter', 3), - '4': lambda: enable('accept_input_on_enter', 4), - 'meta-enter': lambda: enable('accept_input_on_enter', None), - }), - ]), - OptionCategory('Display', [ - Option(title='Completions', - description='Visualisation to use for displaying the completions. (Multiple columns, one column, a toolbar or nothing.)', - get_current_value=lambda: self.completion_visualisation, - get_values=lambda: { - CompletionVisualisation.NONE: lambda: enable('completion_visualisation', CompletionVisualisation.NONE), - CompletionVisualisation.POP_UP: lambda: enable('completion_visualisation', CompletionVisualisation.POP_UP), - CompletionVisualisation.MULTI_COLUMN: lambda: enable('completion_visualisation', CompletionVisualisation.MULTI_COLUMN), - CompletionVisualisation.TOOLBAR: lambda: enable('completion_visualisation', CompletionVisualisation.TOOLBAR), - }), - Option(title='Prompt', - description="Visualisation of the prompt. ('>>>' or 'In [1]:')", - get_current_value=lambda: self.prompt_style, - get_values=lambda: dict((s, partial(enable, 'prompt_style', s)) for s in self.all_prompt_styles)), - simple_option(title='Blank line after output', - description='Insert a blank line after the output.', - field_name='insert_blank_line_after_output'), - simple_option(title='Show signature', - description='Display function signatures.', - field_name='show_signature'), - simple_option(title='Show docstring', - description='Display function docstrings.', - field_name='show_docstring'), - simple_option(title='Show line numbers', - description='Show line numbers when the input consists of multiple lines.', - field_name='show_line_numbers'), - simple_option(title='Show Meta+Enter message', - description='Show the [Meta+Enter] message when this key combination is required to execute commands. ' + - '(This is the case when a simple [Enter] key press will insert a newline.', - field_name='show_meta_enter_message'), - simple_option(title='Wrap lines', - description='Wrap lines instead of scrolling horizontally.', - field_name='wrap_lines'), - simple_option(title='Show status bar', - description='Show the status bar at the bottom of the terminal.', - field_name='show_status_bar'), - simple_option(title='Show sidebar help', - description='When the sidebar is visible, also show this help text.', - field_name='show_sidebar_help'), - simple_option(title='Highlight parenthesis', - description='Highlight matching parenthesis, when the cursor is on or right after one.', - field_name='highlight_matching_parenthesis'), - ]), - OptionCategory('Colors', [ - simple_option(title='Syntax highlighting', - description='Use colors for syntax highligthing', - field_name='enable_syntax_highlighting'), - simple_option(title='Swap light/dark colors', - description='Swap light and dark colors.', - field_name='swap_light_and_dark'), - Option(title='Code', - description='Color scheme to use for the Python code.', - get_current_value=lambda: self._current_code_style_name, - get_values=lambda: dict( - (name, partial(self.use_code_colorscheme, name)) for name in self.code_styles) - ), - Option(title='User interface', - description='Color scheme to use for the user interface.', - get_current_value=lambda: self._current_ui_style_name, - get_values=lambda: dict( - (name, partial(self.use_ui_colorscheme, name)) for name in self.ui_styles) - ), - Option(title='Color depth', - description='Monochrome (1 bit), 16 ANSI colors (4 bit),\n256 colors (8 bit), or 24 bit.', - get_current_value=lambda: COLOR_DEPTHS[self.color_depth], - get_values=lambda: dict( - (name, partial(self._use_color_depth, depth)) for depth, name in COLOR_DEPTHS.items()) - ), - Option(title='Min brightness', - description='Minimum brightness for the color scheme (default=0.0).', - get_current_value=lambda: '%.2f' % self.min_brightness, - get_values=lambda: dict( - ('%.2f' % value, partial(self._set_min_brightness, value)) - for value in brightness_values) - ), - Option(title='Max brightness', - description='Maximum brightness for the color scheme (default=1.0).', - get_current_value=lambda: '%.2f' % self.max_brightness, - get_values=lambda: dict( - ('%.2f' % value, partial(self._set_max_brightness, value)) - for value in brightness_values) - ), - ]), + OptionCategory( + "Input", + [ + Option( + title="Editing mode", + description="Vi or emacs key bindings.", + get_current_value=lambda: ["Emacs", "Vi"][self.vi_mode], + get_values=lambda: { + "Emacs": lambda: disable("vi_mode"), + "Vi": lambda: enable("vi_mode"), + }, + ), + simple_option( + title="Paste mode", + description="When enabled, don't indent automatically.", + field_name="paste_mode", + ), + Option( + title="Complete while typing", + description="Generate autocompletions automatically while typing. " + 'Don\'t require pressing TAB. (Not compatible with "History search".)', + get_current_value=lambda: ["off", "on"][ + self.complete_while_typing + ], + get_values=lambda: { + "on": lambda: enable("complete_while_typing") + and disable("enable_history_search"), + "off": lambda: disable("complete_while_typing"), + }, + ), + Option( + title="Enable fuzzy completion", + description="Enable fuzzy completion.", + get_current_value=lambda: ["off", "on"][ + self.enable_fuzzy_completion + ], + get_values=lambda: { + "on": lambda: enable("enable_fuzzy_completion"), + "off": lambda: disable("enable_fuzzy_completion"), + }, + ), + Option( + title="Dictionary completion", + description="Enable experimental dictionary completion.\n" + 'WARNING: this does "eval" on fragments of\n' + " your Python input and is\n" + " potentially unsafe.", + get_current_value=lambda: ["off", "on"][ + self.enable_dictionary_completion + ], + get_values=lambda: { + "on": lambda: enable("enable_dictionary_completion"), + "off": lambda: disable("enable_dictionary_completion"), + }, + ), + Option( + title="History search", + description="When pressing the up-arrow, filter the history on input starting " + 'with the current text. (Not compatible with "Complete while typing".)', + get_current_value=lambda: ["off", "on"][ + self.enable_history_search + ], + get_values=lambda: { + "on": lambda: enable("enable_history_search") + and disable("complete_while_typing"), + "off": lambda: disable("enable_history_search"), + }, + ), + simple_option( + title="Mouse support", + description="Respond to mouse clicks and scrolling for positioning the cursor, " + "selecting text and scrolling through windows.", + field_name="enable_mouse_support", + ), + simple_option( + title="Confirm on exit", + description="Require confirmation when exiting.", + field_name="confirm_exit", + ), + simple_option( + title="Input validation", + description="In case of syntax errors, move the cursor to the error " + "instead of showing a traceback of a SyntaxError.", + field_name="enable_input_validation", + ), + simple_option( + title="Auto suggestion", + description="Auto suggest inputs by looking at the history. " + "Pressing right arrow or Ctrl-E will complete the entry.", + field_name="enable_auto_suggest", + ), + Option( + title="Accept input on enter", + description="Amount of ENTER presses required to execute input when the cursor " + "is at the end of the input. (Note that META+ENTER will always execute.)", + get_current_value=lambda: str( + self.accept_input_on_enter or "meta-enter" + ), + get_values=lambda: { + "2": lambda: enable("accept_input_on_enter", 2), + "3": lambda: enable("accept_input_on_enter", 3), + "4": lambda: enable("accept_input_on_enter", 4), + "meta-enter": lambda: enable("accept_input_on_enter", None), + }, + ), + ], + ), + OptionCategory( + "Display", + [ + Option( + title="Completions", + description="Visualisation to use for displaying the completions. (Multiple columns, one column, a toolbar or nothing.)", + get_current_value=lambda: self.completion_visualisation.value, + get_values=lambda: { + CompletionVisualisation.NONE.value: lambda: enable( + "completion_visualisation", CompletionVisualisation.NONE + ), + CompletionVisualisation.POP_UP.value: lambda: enable( + "completion_visualisation", + CompletionVisualisation.POP_UP, + ), + CompletionVisualisation.MULTI_COLUMN.value: lambda: enable( + "completion_visualisation", + CompletionVisualisation.MULTI_COLUMN, + ), + CompletionVisualisation.TOOLBAR.value: lambda: enable( + "completion_visualisation", + CompletionVisualisation.TOOLBAR, + ), + }, + ), + Option( + title="Prompt", + description="Visualisation of the prompt. ('>>>' or 'In [1]:')", + get_current_value=lambda: self.prompt_style, + get_values=lambda: dict( + (s, partial(enable, "prompt_style", s)) + for s in self.all_prompt_styles + ), + ), + simple_option( + title="Blank line after output", + description="Insert a blank line after the output.", + field_name="insert_blank_line_after_output", + ), + simple_option( + title="Show signature", + description="Display function signatures.", + field_name="show_signature", + ), + simple_option( + title="Show docstring", + description="Display function docstrings.", + field_name="show_docstring", + ), + simple_option( + title="Show line numbers", + description="Show line numbers when the input consists of multiple lines.", + field_name="show_line_numbers", + ), + simple_option( + title="Show Meta+Enter message", + description="Show the [Meta+Enter] message when this key combination is required to execute commands. " + + "(This is the case when a simple [Enter] key press will insert a newline.", + field_name="show_meta_enter_message", + ), + simple_option( + title="Wrap lines", + description="Wrap lines instead of scrolling horizontally.", + field_name="wrap_lines", + ), + simple_option( + title="Show status bar", + description="Show the status bar at the bottom of the terminal.", + field_name="show_status_bar", + ), + simple_option( + title="Show sidebar help", + description="When the sidebar is visible, also show this help text.", + field_name="show_sidebar_help", + ), + simple_option( + title="Highlight parenthesis", + description="Highlight matching parenthesis, when the cursor is on or right after one.", + field_name="highlight_matching_parenthesis", + ), + ], + ), + OptionCategory( + "Colors", + [ + simple_option( + title="Syntax highlighting", + description="Use colors for syntax highligthing", + field_name="enable_syntax_highlighting", + ), + simple_option( + title="Swap light/dark colors", + description="Swap light and dark colors.", + field_name="swap_light_and_dark", + ), + Option( + title="Code", + description="Color scheme to use for the Python code.", + get_current_value=lambda: self._current_code_style_name, + get_values=lambda: { + name: partial(self.use_code_colorscheme, name) + for name in self.code_styles + }, + ), + Option( + title="User interface", + description="Color scheme to use for the user interface.", + get_current_value=lambda: self._current_ui_style_name, + get_values=lambda: dict( + (name, partial(self.use_ui_colorscheme, name)) + for name in self.ui_styles + ), + ), + Option( + title="Color depth", + description="Monochrome (1 bit), 16 ANSI colors (4 bit),\n256 colors (8 bit), or 24 bit.", + get_current_value=lambda: COLOR_DEPTHS[self.color_depth], + get_values=lambda: { + name: partial(self._use_color_depth, depth) + for depth, name in COLOR_DEPTHS.items() + }, + ), + Option( + title="Min brightness", + description="Minimum brightness for the color scheme (default=0.0).", + get_current_value=lambda: "%.2f" % self.min_brightness, + get_values=lambda: { + "%.2f" % value: partial(self._set_min_brightness, value) + for value in brightness_values + }, + ), + Option( + title="Max brightness", + description="Maximum brightness for the color scheme (default=1.0).", + get_current_value=lambda: "%.2f" % self.max_brightness, + get_values=lambda: { + "%.2f" % value: partial(self._set_max_brightness, value) + for value in brightness_values + }, + ), + ], + ), ] - def _create_application(self): + def _create_application(self) -> Application: """ Create an `Application` instance. """ return Application( - input=self.input, - output=self.output, layout=self.ptpython_layout.layout, - key_bindings=merge_key_bindings([ - load_python_bindings(self), - load_auto_suggest_bindings(), - load_sidebar_bindings(self), - load_confirm_exit_bindings(self), - ConditionalKeyBindings( - load_open_in_editor_bindings(), - Condition(lambda: self.enable_open_in_editor)), - # Extra key bindings should not be active when the sidebar is visible. - ConditionalKeyBindings( - self.extra_key_bindings, - Condition(lambda: not self.show_sidebar)) - ]), + key_bindings=merge_key_bindings( + [ + load_python_bindings(self), + load_auto_suggest_bindings(), + load_sidebar_bindings(self), + load_confirm_exit_bindings(self), + ConditionalKeyBindings( + load_open_in_editor_bindings(), + Condition(lambda: self.enable_open_in_editor), + ), + # Extra key bindings should not be active when the sidebar is visible. + ConditionalKeyBindings( + self.extra_key_bindings, + Condition(lambda: not self.show_sidebar), + ), + ] + ), color_depth=lambda: self.color_depth, paste_mode=Condition(lambda: self.paste_mode), mouse_support=Condition(lambda: self.enable_mouse_support), style=DynamicStyle(lambda: self._current_style), style_transformation=self.style_transformation, include_default_pygments_style=False, - reverse_vi_search_direction=True) + reverse_vi_search_direction=True, + ) - def _create_buffer(self): + def _create_buffer(self) -> Buffer: """ Create the `Buffer` for the Python input. """ @@ -612,45 +766,46 @@ def _create_buffer(self): name=DEFAULT_BUFFER, complete_while_typing=Condition(lambda: self.complete_while_typing), enable_history_search=Condition(lambda: self.enable_history_search), - tempfile_suffix='.py', + tempfile_suffix=".py", history=self.history, completer=ThreadedCompleter(self._completer), validator=ConditionalValidator( - self._validator, - Condition(lambda: self.enable_input_validation)), + self._validator, Condition(lambda: self.enable_input_validation) + ), auto_suggest=ConditionalAutoSuggest( ThreadedAutoSuggest(AutoSuggestFromHistory()), - Condition(lambda: self.enable_auto_suggest)), + Condition(lambda: self.enable_auto_suggest), + ), accept_handler=self._accept_handler, - on_text_changed=self._on_input_timeout) + on_text_changed=self._on_input_timeout, + ) return python_buffer @property - def editing_mode(self): + def editing_mode(self) -> EditingMode: return self.app.editing_mode @editing_mode.setter - def editing_mode(self, value): + def editing_mode(self, value: EditingMode) -> None: self.app.editing_mode = value @property - def vi_mode(self): + def vi_mode(self) -> bool: return self.editing_mode == EditingMode.VI @vi_mode.setter - def vi_mode(self, value): + def vi_mode(self, value: bool) -> None: if value: self.editing_mode = EditingMode.VI else: self.editing_mode = EditingMode.EMACS - def _on_input_timeout(self, buff): + def _on_input_timeout(self, buff: Buffer, loop=None) -> None: """ When there is no input activity, in another thread, get the signature of the current code. """ - assert isinstance(buff, Buffer) app = self.app # Never run multiple get-signature threads. @@ -660,8 +815,12 @@ def _on_input_timeout(self, buff): document = buff.document + loop = loop or get_event_loop() + def run(): - script = get_jedi_script_from_document(document, self.get_locals(), self.get_globals()) + script = get_jedi_script_from_document( + document, self.get_locals(), self.get_globals() + ) # Show signatures in help text. if script: @@ -700,37 +859,41 @@ def run(): # Set docstring in docstring buffer. if signatures: string = signatures[0].docstring() - if not isinstance(string, six.text_type): - string = string.decode('utf-8') + if not isinstance(string, str): + string = string.decode("utf-8") self.docstring_buffer.reset( - document=Document(string, cursor_position=0)) + document=Document(string, cursor_position=0) + ) else: self.docstring_buffer.reset() app.invalidate() else: - self._on_input_timeout(buff) + self._on_input_timeout(buff, loop=loop) - get_event_loop().run_in_executor(run) + loop.run_in_executor(None, run) - def on_reset(self): + def on_reset(self) -> None: self.signatures = [] - def enter_history(self): + def enter_history(self) -> None: """ Display the history. """ app = get_app() app.vi_state.input_mode = InputMode.NAVIGATION - def done(f): - result = f.result() - if result is not None: - self.default_buffer.text = result + history = PythonHistory(self, self.default_buffer.document) + + from prompt_toolkit.application import in_terminal + import asyncio - app.vi_state.input_mode = InputMode.INSERT + async def do_in_terminal() -> None: + async with in_terminal(): + result = await history.app.run_async() + if result is not None: + self.default_buffer.text = result - history = History(self, self.default_buffer.document) + app.vi_state.input_mode = InputMode.INSERT - future = run_coroutine_in_terminal(history.app.run_async) - future.add_done_callback(done) + asyncio.ensure_future(do_in_terminal()) diff --git a/ptpython/repl.py b/ptpython/repl.py index 83cecce1..4b8edf2a 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -7,64 +7,80 @@ embed(globals(), locals(), vi_mode=False) """ -from __future__ import unicode_literals - -from pygments.lexers import PythonTracebackLexer, PythonLexer -from pygments.token import Token +import asyncio +import builtins +import os +import sys +import traceback +import warnings +from typing import Any, Callable, ContextManager, Dict, Optional from prompt_toolkit.document import Document -from prompt_toolkit.eventloop.defaults import use_asyncio_event_loop -from prompt_toolkit.formatted_text import merge_formatted_text, FormattedText +from prompt_toolkit.formatted_text import ( + FormattedText, + PygmentsTokens, + merge_formatted_text, +) from prompt_toolkit.formatted_text.utils import fragment_list_width -from prompt_toolkit.utils import DummyContext -from prompt_toolkit.shortcuts import set_title, clear_title -from prompt_toolkit.shortcuts import print_formatted_text -from prompt_toolkit.formatted_text import PygmentsTokens from prompt_toolkit.patch_stdout import patch_stdout as patch_stdout_context +from prompt_toolkit.shortcuts import clear_title, print_formatted_text, set_title +from prompt_toolkit.utils import DummyContext +from pygments.lexers import PythonLexer, PythonTracebackLexer +from pygments.token import Token -from .python_input import PythonInput from .eventloop import inputhook +from .python_input import PythonInput -import os -import six -import sys -import traceback -import warnings - -__all__ = ( - 'PythonRepl', - 'enable_deprecation_warnings', - 'run_config', - 'embed', -) +__all__ = ["PythonRepl", "enable_deprecation_warnings", "run_config", "embed"] class PythonRepl(PythonInput): - def __init__(self, *a, **kw): - self._startup_paths = kw.pop('startup_paths', None) - super(PythonRepl, self).__init__(*a, **kw) + def __init__(self, *a, **kw) -> None: + self._startup_paths = kw.pop("startup_paths", None) + super().__init__(*a, **kw) self._load_start_paths() + self.pt_loop = asyncio.new_event_loop() - def _load_start_paths(self): + def _load_start_paths(self) -> None: " Start the Read-Eval-Print Loop. " if self._startup_paths: for path in self._startup_paths: if os.path.exists(path): - with open(path, 'rb') as f: - code = compile(f.read(), path, 'exec') - six.exec_(code, self.get_globals(), self.get_locals()) + with open(path, "rb") as f: + code = compile(f.read(), path, "exec") + exec(code, self.get_globals(), self.get_locals()) else: output = self.app.output - output.write('WARNING | File not found: {}\n\n'.format(path)) + output.write("WARNING | File not found: {}\n\n".format(path)) - def run(self): + def run(self) -> None: if self.terminal_title: set_title(self.terminal_title) + def prompt() -> str: + # In order to make sure that asyncio code written in the + # interactive shell doesn't interfere with the prompt, we run the + # prompt in a different event loop. + # If we don't do this, people could spawn coroutine with a + # while/true inside which will freeze the prompt. + + try: + old_loop: Optional[asyncio.AbstractEventLoop] = asyncio.get_event_loop() + except RuntimeError: + # This happens when the user used `asyncio.run()`. + old_loop = None + + asyncio.set_event_loop(self.pt_loop) + try: + return self.app.run() # inputhook=inputhook) + finally: + # Restore the original event loop. + asyncio.set_event_loop(old_loop) + while True: # Run the UI. try: - text = self.app.run(inputhook=inputhook) + text = prompt() except EOFError: return except KeyboardInterrupt: @@ -76,7 +92,12 @@ def run(self): if self.terminal_title: clear_title() - def _process_text(self, line): + async def run_async(self) -> None: + while True: + text = await self.app.run_async() + self._process_text(text) + + def _process_text(self, line: str) -> None: if line and not line.isspace(): try: @@ -88,12 +109,12 @@ def _process_text(self, line): self._handle_exception(e) if self.insert_blank_line_after_output: - self.app.output.write('\n') + self.app.output.write("\n") self.current_statement_index += 1 self.signatures = [] - def _execute(self, line): + def _execute(self, line: str) -> None: """ Evaluate the line and print the result. """ @@ -101,70 +122,81 @@ def _execute(self, line): # WORKAROUND: Due to a bug in Jedi, the current directory is removed # from sys.path. See: https://github.com/davidhalter/jedi/issues/1148 - if '' not in sys.path: - sys.path.insert(0, '') + if "" not in sys.path: + sys.path.insert(0, "") - def compile_with_flags(code, mode): + def compile_with_flags(code: str, mode: str): " Compile code with the right compiler flags. " - return compile(code, '', mode, - flags=self.get_compiler_flags(), - dont_inherit=True) - - if line.lstrip().startswith('\x1a'): + return compile( + code, + "", + mode, + flags=self.get_compiler_flags(), + dont_inherit=True, + ) + + if line.lstrip().startswith("\x1a"): # When the input starts with Ctrl-Z, quit the REPL. self.app.exit() - elif line.lstrip().startswith('!'): + elif line.lstrip().startswith("!"): # Run as shell command os.system(line[1:]) else: # Try eval first try: - code = compile_with_flags(line, 'eval') + code = compile_with_flags(line, "eval") result = eval(code, self.get_globals(), self.get_locals()) - locals = self.get_locals() - locals['_'] = locals['_%i' % self.current_statement_index] = result + locals: Dict[str, Any] = self.get_locals() + locals["_"] = locals["_%i" % self.current_statement_index] = result if result is not None: out_prompt = self.get_output_prompt() try: - result_str = '%r\n' % (result, ) + result_str = "%r\n" % (result,) except UnicodeDecodeError: # In Python 2: `__repr__` should return a bytestring, # so to put it in a unicode context could raise an # exception that the 'ascii' codec can't decode certain # characters. Decode as utf-8 in that case. - result_str = '%s\n' % repr(result).decode('utf-8') + result_str = "%s\n" % repr(result).decode( # type: ignore + "utf-8" + ) # Align every line to the first one. - line_sep = '\n' + ' ' * fragment_list_width(out_prompt) - result_str = line_sep.join(result_str.splitlines()) + '\n' + line_sep = "\n" + " " * fragment_list_width(out_prompt) + result_str = line_sep.join(result_str.splitlines()) + "\n" # Write output tokens. if self.enable_syntax_highlighting: - formatted_output = merge_formatted_text([ - out_prompt, - PygmentsTokens(list(_lex_python_result(result_str))), - ]) + formatted_output = merge_formatted_text( + [ + out_prompt, + PygmentsTokens(list(_lex_python_result(result_str))), + ] + ) else: formatted_output = FormattedText( - out_prompt + [('', result_str)]) + out_prompt + [("", result_str)] + ) print_formatted_text( - formatted_output, style=self._current_style, + formatted_output, + style=self._current_style, style_transformation=self.style_transformation, - include_default_pygments_style=False) + include_default_pygments_style=False, + ) # If not a valid `eval` expression, run using `exec` instead. except SyntaxError: - code = compile_with_flags(line, 'exec') - six.exec_(code, self.get_globals(), self.get_locals()) + code = compile_with_flags(line, "exec") + exec(code, self.get_globals(), self.get_locals()) output.flush() - def _handle_exception(self, e): + def _handle_exception(self, e: Exception) -> None: output = self.app.output # Instead of just calling ``traceback.format_exc``, we take the @@ -174,10 +206,10 @@ def _handle_exception(self, e): # Required for pdb.post_mortem() to work. sys.last_type, sys.last_value, sys.last_traceback = t, v, tb - tblist = traceback.extract_tb(tb) + tblist = list(traceback.extract_tb(tb)) for line_nr, tb_tuple in enumerate(tblist): - if tb_tuple[0] == '': + if tb_tuple[0] == "": tblist = tblist[line_nr:] break @@ -186,33 +218,30 @@ def _handle_exception(self, e): l.insert(0, "Traceback (most recent call last):\n") l.extend(traceback.format_exception_only(t, v)) - # For Python2: `format_list` and `format_exception_only` return - # non-unicode strings. Ensure that everything is unicode. - if six.PY2: - l = [i.decode('utf-8') if isinstance(i, six.binary_type) else i for i in l] - - tb = ''.join(l) + tb_str = "".join(l) # Format exception and write to output. # (We use the default style. Most other styles result # in unreadable colors for the traceback.) if self.enable_syntax_highlighting: - tokens = list(_lex_python_traceback(tb)) + tokens = list(_lex_python_traceback(tb_str)) else: - tokens = [(Token, tb)] + tokens = [(Token, tb_str)] print_formatted_text( - PygmentsTokens(tokens), style=self._current_style, + PygmentsTokens(tokens), + style=self._current_style, style_transformation=self.style_transformation, - include_default_pygments_style=False) + include_default_pygments_style=False, + ) - output.write('%s\n' % e) + output.write("%s\n" % e) output.flush() - def _handle_keyboard_interrupt(self, e): + def _handle_keyboard_interrupt(self, e: KeyboardInterrupt) -> None: output = self.app.output - output.write('\rKeyboardInterrupt\n\n') + output.write("\rKeyboardInterrupt\n\n") output.flush() @@ -228,7 +257,7 @@ def _lex_python_result(tb): return lexer.get_tokens(tb) -def enable_deprecation_warnings(): +def enable_deprecation_warnings() -> None: """ Show deprecation warnings, when they are triggered directly by actions in the REPL. This is recommended to call, before calling `embed`. @@ -236,53 +265,57 @@ def enable_deprecation_warnings(): e.g. This will show an error message when the user imports the 'sha' library on Python 2.7. """ - warnings.filterwarnings('default', category=DeprecationWarning, - module='__main__') + warnings.filterwarnings("default", category=DeprecationWarning, module="__main__") -def run_config(repl, config_file): +def run_config(repl: PythonInput, config_file: str) -> None: """ Execute REPL config file. :param repl: `PythonInput` instance. :param config_file: Path of the configuration file. """ - assert isinstance(repl, PythonInput) - assert isinstance(config_file, six.text_type) - # Expand tildes. config_file = os.path.expanduser(config_file) - def enter_to_continue(): - six.moves.input('\nPress ENTER to continue...') + def enter_to_continue() -> None: + input("\nPress ENTER to continue...") # Check whether this file exists. if not os.path.exists(config_file): - print('Impossible to read %r' % config_file) + print("Impossible to read %r" % config_file) enter_to_continue() return # Run the config file in an empty namespace. try: - namespace = {} + namespace: Dict[str, Any] = {} - with open(config_file, 'rb') as f: - code = compile(f.read(), config_file, 'exec') - six.exec_(code, namespace, namespace) + with open(config_file, "rb") as f: + code = compile(f.read(), config_file, "exec") + exec(code, namespace, namespace) # Now we should have a 'configure' method in this namespace. We call this # method with the repl as an argument. - if 'configure' in namespace: - namespace['configure'](repl) + if "configure" in namespace: + namespace["configure"](repl) except Exception: - traceback.print_exc() - enter_to_continue() + traceback.print_exc() + enter_to_continue() -def embed(globals=None, locals=None, configure=None, - vi_mode=False, history_filename=None, title=None, - startup_paths=None, patch_stdout=False, return_asyncio_coroutine=False): +def embed( + globals=None, + locals=None, + configure: Optional[Callable] = None, + vi_mode: bool = False, + history_filename: Optional[str] = None, + title: Optional[str] = None, + startup_paths=None, + patch_stdout: bool = False, + return_asyncio_coroutine: bool = False, +) -> None: """ Call this to embed Python shell at the current point in your program. It's similar to `IPython.embed` and `bpython.embed`. :: @@ -295,15 +328,13 @@ def embed(globals=None, locals=None, configure=None, argument, to trigger configuration. :param title: Title to be displayed in the terminal titlebar. (None or string.) """ - assert configure is None or callable(configure) - # Default globals/locals if globals is None: globals = { - '__name__': '__main__', - '__package__': None, - '__doc__': None, - '__builtins__': six.moves.builtins, + "__name__": "__main__", + "__package__": None, + "__doc__": None, + "__builtins__": builtins, } locals = locals or globals @@ -314,13 +345,14 @@ def get_globals(): def get_locals(): return locals - # Create eventloop. - if return_asyncio_coroutine: - use_asyncio_event_loop() - # Create REPL. - repl = PythonRepl(get_globals=get_globals, get_locals=get_locals, vi_mode=vi_mode, - history_filename=history_filename, startup_paths=startup_paths) + repl = PythonRepl( + get_globals=get_globals, + get_locals=get_locals, + vi_mode=vi_mode, + history_filename=history_filename, + startup_paths=startup_paths, + ) if title: repl.terminal_title = title @@ -328,22 +360,15 @@ def get_locals(): if configure: configure(repl) - app = repl.app - # Start repl. - patch_context = patch_stdout_context() if patch_stdout else DummyContext() + patch_context: ContextManager = patch_stdout_context() if patch_stdout else DummyContext() - if return_asyncio_coroutine: # XXX - def coroutine(): + if return_asyncio_coroutine: + + async def coroutine(): with patch_context: - while True: - iterator = iter(app.run_async().to_asyncio_future()) - try: - while True: - yield next(iterator) - except StopIteration as exc: - text = exc.args[0] - repl._process_text(text) + await repl.run_async() + return coroutine() else: with patch_context: diff --git a/ptpython/style.py b/ptpython/style.py index 7a2cd2a1..a084c076 100644 --- a/ptpython/style.py +++ b/ptpython/style.py @@ -1,174 +1,151 @@ -from __future__ import unicode_literals +from typing import Dict -from prompt_toolkit.styles import Style, merge_styles +from prompt_toolkit.styles import BaseStyle, Style, merge_styles from prompt_toolkit.styles.pygments import style_from_pygments_cls -from prompt_toolkit.utils import is_windows, is_conemu_ansi, is_windows_vt100_supported -from pygments.styles import get_style_by_name, get_all_styles +from prompt_toolkit.utils import is_conemu_ansi, is_windows, is_windows_vt100_supported +from pygments.styles import get_all_styles, get_style_by_name -__all__ = ( - 'get_all_code_styles', - 'get_all_ui_styles', - 'generate_style', -) +__all__ = ["get_all_code_styles", "get_all_ui_styles", "generate_style"] -def get_all_code_styles(): +def get_all_code_styles() -> Dict[str, BaseStyle]: """ Return a mapping from style names to their classes. """ - result = dict((name, style_from_pygments_cls(get_style_by_name(name))) for name in get_all_styles()) - result['win32'] = Style.from_dict(win32_code_style) + result: Dict[str, BaseStyle] = { + name: style_from_pygments_cls(get_style_by_name(name)) + for name in get_all_styles() + } + result["win32"] = Style.from_dict(win32_code_style) return result -def get_all_ui_styles(): +def get_all_ui_styles() -> Dict[str, BaseStyle]: """ Return a dict mapping {ui_style_name -> style_dict}. """ return { - 'default': Style.from_dict(default_ui_style), - 'blue': Style.from_dict(blue_ui_style), + "default": Style.from_dict(default_ui_style), + "blue": Style.from_dict(blue_ui_style), } -def generate_style(python_style, ui_style): +def generate_style(python_style: BaseStyle, ui_style: BaseStyle) -> BaseStyle: """ Generate Pygments Style class from two dictionaries containing style rules. """ - return merge_styles([ - python_style, - ui_style - ]) + return merge_styles([python_style, ui_style]) # Code style for Windows consoles. They support only 16 colors, # so we choose a combination that displays nicely. win32_code_style = { - 'pygments.comment': "#00ff00", - 'pygments.keyword': '#44ff44', - 'pygments.number': '', - 'pygments.operator': '', - 'pygments.string': '#ff44ff', - - 'pygments.name': '', - 'pygments.name.decorator': '#ff4444', - 'pygments.name.class': '#ff4444', - 'pygments.name.function': '#ff4444', - 'pygments.name.builtin': '#ff4444', - - 'pygments.name.attribute': '', - 'pygments.name.constant': '', - 'pygments.name.entity': '', - 'pygments.name.exception': '', - 'pygments.name.label': '', - 'pygments.name.namespace': '', - 'pygments.name.tag': '', - 'pygments.name.variable': '', + "pygments.comment": "#00ff00", + "pygments.keyword": "#44ff44", + "pygments.number": "", + "pygments.operator": "", + "pygments.string": "#ff44ff", + "pygments.name": "", + "pygments.name.decorator": "#ff4444", + "pygments.name.class": "#ff4444", + "pygments.name.function": "#ff4444", + "pygments.name.builtin": "#ff4444", + "pygments.name.attribute": "", + "pygments.name.constant": "", + "pygments.name.entity": "", + "pygments.name.exception": "", + "pygments.name.label": "", + "pygments.name.namespace": "", + "pygments.name.tag": "", + "pygments.name.variable": "", } default_ui_style = { - 'control-character': 'ansiblue', - + "control-character": "ansiblue", # Classic prompt. - 'prompt': 'bold', - 'prompt.dots': 'noinherit', - + "prompt": "bold", + "prompt.dots": "noinherit", # (IPython <5.0) Prompt: "In [1]:" - 'in': 'bold #008800', - 'in.number': '', - + "in": "bold #008800", + "in.number": "", # Return value. - 'out': '#ff0000', - 'out.number': '#ff0000', - + "out": "#ff0000", + "out.number": "#ff0000", # Completions. - 'completion.builtin': '', - 'completion.keyword': 'fg:#008800', - - 'completion.keyword fuzzymatch.inside': 'fg:#008800', - 'completion.keyword fuzzymatch.outside': 'fg:#44aa44', - + "completion.builtin": "", + "completion.keyword": "fg:#008800", + "completion.keyword fuzzymatch.inside": "fg:#008800", + "completion.keyword fuzzymatch.outside": "fg:#44aa44", # Separator between windows. (Used above docstring.) - 'separator': '#bbbbbb', - + "separator": "#bbbbbb", # System toolbar - 'system-toolbar': '#22aaaa noinherit', - + "system-toolbar": "#22aaaa noinherit", # "arg" toolbar. - 'arg-toolbar': '#22aaaa noinherit', - 'arg-toolbar.text': 'noinherit', - + "arg-toolbar": "#22aaaa noinherit", + "arg-toolbar.text": "noinherit", # Signature toolbar. - 'signature-toolbar': 'bg:#44bbbb #000000', - 'signature-toolbar.currentname': 'bg:#008888 #ffffff bold', - 'signature-toolbar.operator': '#000000 bold', - - 'docstring': '#888888', - + "signature-toolbar": "bg:#44bbbb #000000", + "signature-toolbar.currentname": "bg:#008888 #ffffff bold", + "signature-toolbar.operator": "#000000 bold", + "docstring": "#888888", # Validation toolbar. - 'validation-toolbar': 'bg:#440000 #aaaaaa', - + "validation-toolbar": "bg:#440000 #aaaaaa", # Status toolbar. - 'status-toolbar': 'bg:#222222 #aaaaaa', - 'status-toolbar.title': 'underline', - 'status-toolbar.inputmode': 'bg:#222222 #ffffaa', - 'status-toolbar.key': 'bg:#000000 #888888', - 'status-toolbar.pastemodeon': 'bg:#aa4444 #ffffff', - 'status-toolbar.pythonversion': 'bg:#222222 #ffffff bold', - 'status-toolbar paste-mode-on': 'bg:#aa4444 #ffffff', - 'record': 'bg:#884444 white', - 'status-toolbar.input-mode': '#ffff44', - + "status-toolbar": "bg:#222222 #aaaaaa", + "status-toolbar.title": "underline", + "status-toolbar.inputmode": "bg:#222222 #ffffaa", + "status-toolbar.key": "bg:#000000 #888888", + "status-toolbar.pastemodeon": "bg:#aa4444 #ffffff", + "status-toolbar.pythonversion": "bg:#222222 #ffffff bold", + "status-toolbar paste-mode-on": "bg:#aa4444 #ffffff", + "record": "bg:#884444 white", + "status-toolbar.input-mode": "#ffff44", # The options sidebar. - 'sidebar': 'bg:#bbbbbb #000000', - 'sidebar.title': 'bg:#668866 #ffffff', - 'sidebar.label': 'bg:#bbbbbb #222222', - 'sidebar.status': 'bg:#dddddd #000011', - 'sidebar.label selected': 'bg:#222222 #eeeeee', - 'sidebar.status selected': 'bg:#444444 #ffffff bold', - - 'sidebar.separator': 'underline', - 'sidebar.key': 'bg:#bbddbb #000000 bold', - 'sidebar.key.description': 'bg:#bbbbbb #000000', - 'sidebar.helptext': 'bg:#fdf6e3 #000011', - -# # Styling for the history layout. -# history.line: '', -# history.line.selected: 'bg:#008800 #000000', -# history.line.current: 'bg:#ffffff #000000', -# history.line.selected.current: 'bg:#88ff88 #000000', -# history.existinginput: '#888888', - + "sidebar": "bg:#bbbbbb #000000", + "sidebar.title": "bg:#668866 #ffffff", + "sidebar.label": "bg:#bbbbbb #222222", + "sidebar.status": "bg:#dddddd #000011", + "sidebar.label selected": "bg:#222222 #eeeeee", + "sidebar.status selected": "bg:#444444 #ffffff bold", + "sidebar.separator": "underline", + "sidebar.key": "bg:#bbddbb #000000 bold", + "sidebar.key.description": "bg:#bbbbbb #000000", + "sidebar.helptext": "bg:#fdf6e3 #000011", + # # Styling for the history layout. + # history.line: '', + # history.line.selected: 'bg:#008800 #000000', + # history.line.current: 'bg:#ffffff #000000', + # history.line.selected.current: 'bg:#88ff88 #000000', + # history.existinginput: '#888888', # Help Window. - 'window-border': '#aaaaaa', - 'window-title': 'bg:#bbbbbb #000000', - + "window-border": "#aaaaaa", + "window-title": "bg:#bbbbbb #000000", # Meta-enter message. - 'accept-message': 'bg:#ffff88 #444444', - + "accept-message": "bg:#ffff88 #444444", # Exit confirmation. - 'exit-confirmation': 'bg:#884444 #ffffff', + "exit-confirmation": "bg:#884444 #ffffff", } # Some changes to get a bit more contrast on Windows consoles. # (They only support 16 colors.) if is_windows() and not is_conemu_ansi() and not is_windows_vt100_supported(): - default_ui_style.update({ - 'sidebar.title': 'bg:#00ff00 #ffffff', - 'exitconfirmation': 'bg:#ff4444 #ffffff', - 'toolbar.validation': 'bg:#ff4444 #ffffff', - - 'menu.completions.completion': 'bg:#ffffff #000000', - 'menu.completions.completion.current': 'bg:#aaaaaa #000000', - }) + default_ui_style.update( + { + "sidebar.title": "bg:#00ff00 #ffffff", + "exitconfirmation": "bg:#ff4444 #ffffff", + "toolbar.validation": "bg:#ff4444 #ffffff", + "menu.completions.completion": "bg:#ffffff #000000", + "menu.completions.completion.current": "bg:#aaaaaa #000000", + } + ) blue_ui_style = {} blue_ui_style.update(default_ui_style) -#blue_ui_style.update({ +# blue_ui_style.update({ # # Line numbers. # Token.LineNumber: '#aa6666', # @@ -192,4 +169,4 @@ def generate_style(python_style, ui_style): # Token.Menu.Completions.Meta.Current: 'bg:#00aaaa #000000', # Token.Menu.Completions.ProgressBar: 'bg:#aaaaaa', # Token.Menu.Completions.ProgressButton: 'bg:#000000', -#}) +# }) diff --git a/ptpython/utils.py b/ptpython/utils.py index 2cdf2491..130da34f 100644 --- a/ptpython/utils.py +++ b/ptpython/utils.py @@ -1,19 +1,19 @@ """ For internal use only. """ -from __future__ import unicode_literals - -from prompt_toolkit.mouse_events import MouseEventType import re +from typing import Callable, TypeVar, cast + +from prompt_toolkit.mouse_events import MouseEvent, MouseEventType -__all__ = ( - 'has_unclosed_brackets', - 'get_jedi_script_from_document', - 'document_is_multiline_python', -) +__all__ = [ + "has_unclosed_brackets", + "get_jedi_script_from_document", + "document_is_multiline_python", +] -def has_unclosed_brackets(text): +def has_unclosed_brackets(text: str) -> bool: """ Starting at the end of the string. If we find an opening bracket for which we didn't had a closing one yet, return True. @@ -21,17 +21,19 @@ def has_unclosed_brackets(text): stack = [] # Ignore braces inside strings - text = re.sub(r'''('[^']*'|"[^"]*")''', '', text) # XXX: handle escaped quotes.! + text = re.sub(r"""('[^']*'|"[^"]*")""", "", text) # XXX: handle escaped quotes.! for c in reversed(text): - if c in '])}': + if c in "])}": stack.append(c) - elif c in '[({': + elif c in "[({": if stack: - if ((c == '[' and stack[-1] == ']') or - (c == '{' and stack[-1] == '}') or - (c == '(' and stack[-1] == ')')): + if ( + (c == "[" and stack[-1] == "]") + or (c == "{" and stack[-1] == "}") + or (c == "(" and stack[-1] == ")") + ): stack.pop() else: # Opening bracket for which we didn't had a closing one. @@ -42,15 +44,17 @@ def has_unclosed_brackets(text): def get_jedi_script_from_document(document, locals, globals): import jedi # We keep this import in-line, to improve start-up time. - # Importing Jedi is 'slow'. + + # Importing Jedi is 'slow'. try: return jedi.Interpreter( document.text, column=document.cursor_position_col, line=document.cursor_position_row + 1, - path='input-text', - namespaces=[locals, globals]) + path="input-text", + namespaces=[locals, globals], + ) except ValueError: # Invalid cursor position. # ValueError('`column` parameter is not in a valid range.') @@ -70,14 +74,15 @@ def get_jedi_script_from_document(document, locals, globals): return None -_multiline_string_delims = re.compile('''[']{3}|["]{3}''') +_multiline_string_delims = re.compile("""[']{3}|["]{3}""") def document_is_multiline_python(document): """ Determine whether this is a multiline Python document. """ - def ends_in_multiline_string(): + + def ends_in_multiline_string() -> bool: """ ``True`` if we're inside a multiline string at the end of the text. """ @@ -90,28 +95,35 @@ def ends_in_multiline_string(): opening = None return bool(opening) - if '\n' in document.text or ends_in_multiline_string(): + if "\n" in document.text or ends_in_multiline_string(): return True - def line_ends_with_colon(): - return document.current_line.rstrip()[-1:] == ':' + def line_ends_with_colon() -> bool: + return document.current_line.rstrip()[-1:] == ":" # If we just typed a colon, or still have open brackets, always insert a real newline. - if line_ends_with_colon() or \ - (document.is_cursor_at_the_end and - has_unclosed_brackets(document.text_before_cursor)) or \ - document.text.startswith('@'): + if ( + line_ends_with_colon() + or ( + document.is_cursor_at_the_end + and has_unclosed_brackets(document.text_before_cursor) + ) + or document.text.startswith("@") + ): return True # If the character before the cursor is a backslash (line continuation # char), insert a new line. - elif document.text_before_cursor[-1:] == '\\': + elif document.text_before_cursor[-1:] == "\\": return True return False -def if_mousedown(handler): +_T = TypeVar("_T", bound=Callable[[MouseEvent], None]) + + +def if_mousedown(handler: _T) -> _T: """ Decorator for mouse handlers. Only handle event when the user pressed mouse down. @@ -119,9 +131,11 @@ def if_mousedown(handler): (When applied to a token list. Scroll events will bubble up and are handled by the Window.) """ - def handle_if_mouse_down(mouse_event): + + def handle_if_mouse_down(mouse_event: MouseEvent): if mouse_event.event_type == MouseEventType.MOUSE_DOWN: return handler(mouse_event) else: return NotImplemented - return handle_if_mouse_down + + return cast(_T, handle_if_mouse_down) diff --git a/ptpython/validator.py b/ptpython/validator.py index 80cc3fb1..b7880bf6 100644 --- a/ptpython/validator.py +++ b/ptpython/validator.py @@ -1,10 +1,7 @@ -from __future__ import unicode_literals +from prompt_toolkit.validation import ValidationError, Validator -from prompt_toolkit.validation import Validator, ValidationError +__all__ = ["PythonValidator"] -__all__ = ( - 'PythonValidator', -) class PythonValidator(Validator): """ @@ -13,6 +10,7 @@ class PythonValidator(Validator): :param get_compiler_flags: Callable that returns the currently active compiler flags. """ + def __init__(self, get_compiler_flags=None): self.get_compiler_flags = get_compiler_flags @@ -22,7 +20,7 @@ def validate(self, document): """ # When the input starts with Ctrl-Z, always accept. This means EOF in a # Python REPL. - if document.text.startswith('\x1a'): + if document.text.startswith("\x1a"): return try: @@ -31,17 +29,19 @@ def validate(self, document): else: flags = 0 - compile(document.text, '', 'exec', flags=flags, dont_inherit=True) + compile(document.text, "", "exec", flags=flags, dont_inherit=True) except SyntaxError as e: # Note, the 'or 1' for offset is required because Python 2.7 # gives `None` as offset in case of '4=4' as input. (Looks like # fixed in Python 3.) - index = document.translate_row_col_to_index(e.lineno - 1, (e.offset or 1) - 1) - raise ValidationError(index, 'Syntax Error') + index = document.translate_row_col_to_index( + e.lineno - 1, (e.offset or 1) - 1 + ) + raise ValidationError(index, "Syntax Error") except TypeError as e: # e.g. "compile() expected string without null bytes" raise ValidationError(0, str(e)) except ValueError as e: # In Python 2, compiling "\x9" (an invalid escape sequence) raises # ValueError instead of SyntaxError. - raise ValidationError(0, 'Syntax Error: %s' % e) + raise ValidationError(0, "Syntax Error: %s" % e) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..b356239f --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,13 @@ +[tool.black] +target-version = ['py36'] + + +[tool.isort] +# isort configuration that is compatible with Black. +multi_line_output = 3 +include_trailing_comma = true +known_first_party = "ptpython" +known_third_party = "prompt_toolkit,pygments,asyncssh" +force_grid_wrap = 0 +use_parentheses = true +line_length = 88 diff --git a/setup.py b/setup.py index e884f3c4..b652877a 100644 --- a/setup.py +++ b/setup.py @@ -1,43 +1,48 @@ #!/usr/bin/env python import os import sys -from setuptools import setup, find_packages -with open(os.path.join(os.path.dirname(__file__), 'README.rst')) as f: +from setuptools import find_packages, setup + +with open(os.path.join(os.path.dirname(__file__), "README.rst")) as f: long_description = f.read() setup( - name='ptpython', - author='Jonathan Slenders', - version='2.0.5', - url='https://github.com/jonathanslenders/ptpython', - description='Python REPL build on top of prompt_toolkit', + name="ptpython", + author="Jonathan Slenders", + version="2.0.5", + url="https://github.com/prompt-toolkit/ptpython", + description="Python REPL build on top of prompt_toolkit", long_description=long_description, - packages=find_packages('.'), - install_requires = [ - 'appdirs', - 'docopt', - 'jedi>=0.9.0', - 'prompt_toolkit>=2.0.8,<2.1.0', - 'pygments', + packages=find_packages("."), + install_requires=[ + "appdirs", + "jedi>=0.9.0", + "prompt_toolkit>=3.0.0,<3.1.0", + "pygments", ], + python_requires=">=3.6", classifiers=[ - 'Programming Language :: Python', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 2', + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python", ], entry_points={ - 'console_scripts': [ - 'ptpython = ptpython.entry_points.run_ptpython:run', - 'ptipython = ptpython.entry_points.run_ptipython:run', - 'ptpython%s = ptpython.entry_points.run_ptpython:run' % sys.version_info[0], - 'ptpython%s.%s = ptpython.entry_points.run_ptpython:run' % sys.version_info[:2], - 'ptipython%s = ptpython.entry_points.run_ptipython:run' % sys.version_info[0], - 'ptipython%s.%s = ptpython.entry_points.run_ptipython:run' % sys.version_info[:2], + "console_scripts": [ + "ptpython = ptpython.entry_points.run_ptpython:run", + "ptipython = ptpython.entry_points.run_ptipython:run", + "ptpython%s = ptpython.entry_points.run_ptpython:run" % sys.version_info[0], + "ptpython%s.%s = ptpython.entry_points.run_ptpython:run" + % sys.version_info[:2], + "ptipython%s = ptpython.entry_points.run_ptipython:run" + % sys.version_info[0], + "ptipython%s.%s = ptpython.entry_points.run_ptipython:run" + % sys.version_info[:2], ] }, - extras_require={ - 'ptipython': ['ipython'] # For ptipython, we need to have IPython - } + extras_require={"ptipython": ["ipython"]}, # For ptipython, we need to have IPython ) diff --git a/tests/run_tests.py b/tests/run_tests.py index a23fddec..2f945163 100755 --- a/tests/run_tests.py +++ b/tests/run_tests.py @@ -1,17 +1,9 @@ #!/usr/bin/env python -from __future__ import unicode_literals - import unittest - -# For now there are no tests here. -# However this is sufficient for Travis to do at least a syntax check. -# That way we are at least sure to restrict to the Python 2.6 syntax. - import ptpython.completer -import ptpython.filters -#import ptpython.ipython import ptpython.eventloop +import ptpython.filters import ptpython.history_browser import ptpython.key_bindings import ptpython.layout @@ -21,6 +13,10 @@ import ptpython.utils import ptpython.validator +# For now there are no tests here. +# However this is sufficient for Travis to do at least a syntax check. +# That way we are at least sure to restrict to the Python 2.6 syntax. + -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() From 6569e2451b1cdb60ee6737b091ae153a6e93f2cb Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Mon, 27 Jan 2020 22:41:54 +0100 Subject: [PATCH 253/470] Create parent directories for configuration. --- ptpython/entry_points/run_ptpython.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ptpython/entry_points/run_ptpython.py b/ptpython/entry_points/run_ptpython.py index a8710792..204a94aa 100644 --- a/ptpython/entry_points/run_ptpython.py +++ b/ptpython/entry_points/run_ptpython.py @@ -17,6 +17,7 @@ """ import argparse import os +import pathlib import sys from typing import Tuple @@ -66,8 +67,7 @@ def get_config_and_history_file(namespace: argparse.Namespace) -> Tuple[str, str # Create directories. for d in (config_dir, data_dir): - if not os.path.isdir(d) and not os.path.islink(d): - os.mkdir(d) + pathlib.Path(d).mkdir(parents=True, exist_ok=True) # Determine config file to be used. config_file = os.path.join(config_dir, "config.py") From 27f5bcd8493df726277a1562ff52b1c8ff5b5285 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Wed, 29 Jan 2020 22:26:41 +0100 Subject: [PATCH 254/470] Release 3.0.0 --- CHANGELOG | 9 +++++++++ README.rst | 3 +++ setup.py | 2 +- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index c64a87d1..6dfbcc61 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,15 @@ CHANGELOG ========= +3.0.0: 2020-01-29 +----------------- + +Upgrade to prompt_toolkit 3.0. +Requires at least Python 3.6. + +New features: +- Uses XDG base directory specification. + 2.0.5: 2019-10-09 ----------------- diff --git a/README.rst b/README.rst index f394054d..da60c1e6 100644 --- a/README.rst +++ b/README.rst @@ -15,6 +15,9 @@ Ptpython is an advanced Python REPL. It should work on all Python versions from 2.6 up to 3.7 and work cross platform (Linux, BSD, OS X and Windows). +Note: this version of ptpython requires at least Python 3.6. Install ptpython +2.0.5 for older Python versions. + Installation ************ diff --git a/setup.py b/setup.py index b652877a..8f4eec58 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ setup( name="ptpython", author="Jonathan Slenders", - version="2.0.5", + version="3.0.0", url="https://github.com/prompt-toolkit/ptpython", description="Python REPL build on top of prompt_toolkit", long_description=long_description, From d21c480ac1f7e9c22dae429f6be73f53601eb455 Mon Sep 17 00:00:00 2001 From: Mikaeil Orfanian Date: Mon, 17 Feb 2020 15:46:59 +0100 Subject: [PATCH 255/470] Retain backwards compatibility Some other libraries (e.g. django-extensions) rely on the previous signature of this function. This change makes the function signature more flexible so current and older usages don't break. --- ptpython/repl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ptpython/repl.py b/ptpython/repl.py index 4b8edf2a..e3f04e5c 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -268,7 +268,7 @@ def enable_deprecation_warnings() -> None: warnings.filterwarnings("default", category=DeprecationWarning, module="__main__") -def run_config(repl: PythonInput, config_file: str) -> None: +def run_config(repl: PythonInput, config_file: str="~/.ptpython/config.py") -> None: """ Execute REPL config file. From fb0f7cb650b4ab23a99354efed0ba446b46d8dd5 Mon Sep 17 00:00:00 2001 From: Mikaeil Orfanian Date: Mon, 17 Feb 2020 15:54:59 +0100 Subject: [PATCH 256/470] Blacken --- ptpython/repl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ptpython/repl.py b/ptpython/repl.py index e3f04e5c..06062dc3 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -268,7 +268,7 @@ def enable_deprecation_warnings() -> None: warnings.filterwarnings("default", category=DeprecationWarning, module="__main__") -def run_config(repl: PythonInput, config_file: str="~/.ptpython/config.py") -> None: +def run_config(repl: PythonInput, config_file: str = "~/.ptpython/config.py") -> None: """ Execute REPL config file. From c6952a8c81ecd197fffe2864fba83f8d5feb5972 Mon Sep 17 00:00:00 2001 From: sblondon Date: Mon, 3 Feb 2020 15:43:33 +0100 Subject: [PATCH 257/470] Emacs commands in lowercases Hello, the commands for Emacs uses lowercases for 'x' and 'e' so this PR changes the cases to fit this. It's like the Emacs documentation (par example https://www.gnu.org/software/emacs/manual/html_node/emacs/Keys.html#Keys). --- examples/ptpython_config/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/ptpython_config/config.py b/examples/ptpython_config/config.py index ff8b8ac1..3a1175ca 100644 --- a/examples/ptpython_config/config.py +++ b/examples/ptpython_config/config.py @@ -91,7 +91,7 @@ def configure(repl): # based on the history.) repl.enable_auto_suggest = False - # Enable open-in-editor. Pressing C-X C-E in emacs mode or 'v' in + # Enable open-in-editor. Pressing C-x C-e in emacs mode or 'v' in # Vi navigation mode will open the input in the current editor. repl.enable_open_in_editor = True From 311c26feca034d4b99728bff20c267bbcd757c53 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sun, 23 Feb 2020 05:56:50 +0100 Subject: [PATCH 258/470] Fix input mode in status bar for block selection. --- ptpython/layout.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ptpython/layout.py b/ptpython/layout.py index 7b68b2d4..bf783c6a 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -429,7 +429,7 @@ def toggle_vi_mode(mouse_event: MouseEvent) -> None: elif app.current_buffer.selection_state.type == SelectionType.CHARACTERS: append((input_mode_t, "Vi (VISUAL)", toggle_vi_mode)) append((token, " ")) - elif app.current_buffer.selection_state.type == "BLOCK": + elif app.current_buffer.selection_state.type == SelectionType.BLOCK: append((input_mode_t, "Vi (VISUAL BLOCK)", toggle_vi_mode)) append((token, " ")) elif mode in (InputMode.INSERT, "vi-insert-multiple"): From ee551a37fcc9f02fb9b29070460ab26364adaad9 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sun, 23 Feb 2020 05:58:56 +0100 Subject: [PATCH 259/470] Release 3.0.1 --- CHANGELOG | 8 ++++++++ setup.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 6dfbcc61..a90d86e1 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,14 @@ CHANGELOG ========= +3.0.1: 2020-02-24 +----------------- + +- Fix backwards-compatibility of the `run_config` function. (used by + django-extensions). +- Fix input mode in status bar for block selection. + + 3.0.0: 2020-01-29 ----------------- diff --git a/setup.py b/setup.py index 8f4eec58..8fbc277c 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ setup( name="ptpython", author="Jonathan Slenders", - version="3.0.0", + version="3.0.1", url="https://github.com/prompt-toolkit/ptpython", description="Python REPL build on top of prompt_toolkit", long_description=long_description, From 7e204a094596bd436a10700ddea42991afa27408 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Wed, 8 Apr 2020 16:44:22 +0200 Subject: [PATCH 260/470] Improved dictionary completion. - Also complete list indexes. - Also complete attributes after doing a dictionary lookup. - Also complete on iterators in a for-loop. --- ptpython/completer.py | 225 +++++++++++++++++++++++++++++++++------ ptpython/python_input.py | 2 +- 2 files changed, 193 insertions(+), 34 deletions(-) diff --git a/ptpython/completer.py b/ptpython/completer.py index 2ffaf62e..46995ba7 100644 --- a/ptpython/completer.py +++ b/ptpython/completer.py @@ -1,7 +1,7 @@ import ast import keyword import re -from typing import TYPE_CHECKING, Iterable +from typing import TYPE_CHECKING, Any, Dict, Iterable from prompt_toolkit.completion import ( CompleteEvent, @@ -129,7 +129,10 @@ def get_completions( for c in self.dictionary_completer.get_completions( document, complete_event ): - has_dict_completions = True + if c.text not in '[.': + # If we get the [ or . completion, still include the other + # completions. + has_dict_completions = True yield c if has_dict_completions: return @@ -206,8 +209,8 @@ class DictionaryCompleter(Completer): """ Experimental completer for Python dictionary keys. - Warning: This does an `eval` on the Python object before the open square - bracket, which is potentially dangerous. It doesn't match on + Warning: This does an `eval` and `repr` on some Python expressions before + the cursor, which is potentially dangerous. It doesn't match on function calls, so it only triggers attribute access. """ @@ -217,31 +220,58 @@ def __init__(self, get_globals, get_locals): self.get_globals = get_globals self.get_locals = get_locals - self.pattern = re.compile( - r""" - # Any expression safe enough to eval while typing. - # No operators, except dot, and only other dict lookups. - # Technically, this can be unsafe of course, if bad code runs - # in `__getattr__` or ``__getitem__``. - ( - # Variable name - [a-zA-Z0-9_]+ - - \s* + # Pattern for expressions that are "safe" to eval for auto-completion. + # These are expressions that contain only attribute and index lookups. + expression = r""" + # Any expression safe enough to eval while typing. + # No operators, except dot, and only other dict lookups. + # Technically, this can be unsafe of course, if bad code runs + # in `__getattr__` or ``__getitem__``. + ( + # Variable name + [a-zA-Z0-9_]+ + + \s* + + (?: + # Attribute access. + \s* \. \s* [a-zA-Z0-9_]+ \s* + + | + + # Item lookup. + # (We match the square brackets. We don't care about + # matching quotes here in the regex. Nested square brackets + # are not supported.) + \s* \[ [a-zA-Z0-9_'"\s]+ \] \s* + )* + ) + """ - (?: - # Attribute access. - \s* \. \s* [a-zA-Z0-9_]+ \s* + # Pattern for recognizing for-loops, so that we can provide + # autocompletion on the iterator of the for-loop. (According to the + # first item of the collection we're iterating over.) + self.for_loop_pattern = re.compile( + rf""" + for \s+ ([a-zA-Z0-9_]+) \s+ in \s+ {expression} \s* : + """, + re.VERBOSE, + ) - | + # Pattern for matching a simple expression (for completing [ or . + # operators). + self.expression_pattern = re.compile( + rf""" + {expression} + $ + """, + re.VERBOSE, + ) - # Item lookup. - # (We match the square brackets. We don't care about - # matching quotes here in the regex. Nested square - # brackets are not supported.) - \s* \[ [a-zA-Z0-9_'"\s]+ \] \s* - )* - ) + # Pattern for matching item lookups. + self.item_lookup_pattern = re.compile( + rf""" + {expression} # Dict loopup to complete (square bracket open + start of # string). @@ -251,19 +281,97 @@ def __init__(self, get_globals, get_locals): re.VERBOSE, ) + # Pattern for matching attribute lookups. + self.attribute_lookup_pattern = re.compile( + rf""" + {expression} + + # Attribute loopup to complete (dot + varname). + \. + \s* ([a-zA-Z0-9_]*)$ + """, + re.VERBOSE, + ) + + def _lookup(self, expression: str, temp_locals: Dict[str, Any]) -> object: + """ + Do lookup of `object_var` in the context. + `temp_locals` is a dictionary, used for the locals. + """ + try: + return eval(expression.strip(), self.get_globals(), temp_locals) + except BaseException: + return # Many exception, like NameError can be thrown here. + def get_completions( self, document: Document, complete_event: CompleteEvent ) -> Iterable[Completion]: - match = self.pattern.search(document.text_before_cursor) + + # First, find all for-loops, and assing the first item of the + # collections they're iterating to the iterator variable, so that we + # can provide code completion on the iterators. + temp_locals = self.get_locals().copy() + + for match in self.for_loop_pattern.finditer(document.text_before_cursor): + varname, expression = match.groups() + expression_val = self._lookup(expression, temp_locals) + + # We do this only for lists and tuples. Calling `next()` on any + # collection would create undesired side effects. + if isinstance(expression_val, (list, tuple)) and expression_val: + temp_locals[varname] = expression_val[0] + + # Get all completions. + yield from self._get_expression_completions( + document, complete_event, temp_locals + ) + yield from self._get_item_lookup_completions( + document, complete_event, temp_locals + ) + yield from self._get_attribute_completions( + document, complete_event, temp_locals + ) + + def _do_repr(self, obj: object) -> str: + try: + return str(repr(obj)) + except BaseException: + raise ReprFailedError + + def _get_expression_completions( + self, + document: Document, + complete_event: CompleteEvent, + temp_locals: Dict[str, Any], + ) -> Iterable[Completion]: + """ + Complete the [ or . operator after an object. + """ + match = self.expression_pattern.search(document.text_before_cursor) + if match is not None: + object_var = match.groups()[0] + result = self._lookup(object_var, temp_locals) + + if isinstance(result, (list, tuple, dict)): + yield Completion("[", 0) + elif result: + yield Completion(".", 0) + + def _get_item_lookup_completions( + self, + document: Document, + complete_event: CompleteEvent, + temp_locals: Dict[str, Any], + ) -> Iterable[Completion]: + """ + Complete dictionary keys. + """ + match = self.item_lookup_pattern.search(document.text_before_cursor) if match is not None: object_var, key = match.groups() - object_var = object_var.strip() # Do lookup of `object_var` in the context. - try: - result = eval(object_var, self.get_globals(), self.get_locals()) - except BaseException: - return # Many exception, like NameError can be thrown here. + result = self._lookup(object_var, temp_locals) # If this object is a dictionary, complete the keys. if isinstance(result, dict): @@ -279,7 +387,58 @@ def get_completions( for k in result: if str(k).startswith(key_obj): - yield Completion(str(repr(k)), -len(key), display=str(repr(k))) + try: + k_repr = self._do_repr(k) + yield Completion( + k_repr + "]", + -len(key), + display=f"[{k_repr}]", + display_meta=self._do_repr(result[k]), + ) + except ReprFailedError: + pass + + # Complete list/tuple index keys. + elif isinstance(result, (list, tuple)): + if not key or key.isdigit(): + for k in range(min(len(result), 1000)): + if str(k).startswith(key): + try: + k_repr = self._do_repr(k) + yield Completion( + k_repr + "]", + -len(key), + display=f"[{k_repr}]", + display_meta=self._do_repr(result[k]), + ) + except ReprFailedError: + pass + + def _get_attribute_completions( + self, + document: Document, + complete_event: CompleteEvent, + temp_locals: Dict[str, Any], + ) -> Iterable[Completion]: + """ + Complete attribute names. + """ + match = self.attribute_lookup_pattern.search(document.text_before_cursor) + if match is not None: + object_var, attr_name = match.groups() + + # Do lookup of `object_var` in the context. + result = self._lookup(object_var, temp_locals) + + for name in dir(result): + if name.startswith(attr_name): + yield Completion( + name, -len(attr_name), + ) + + +class ReprFailedError(Exception): + " Raised when the repr() call in `DictionaryCompleter` fails. " try: diff --git a/ptpython/python_input.py b/ptpython/python_input.py index c4bbbd0c..462e9b0c 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -518,7 +518,7 @@ def get_values(): ), Option( title="Dictionary completion", - description="Enable experimental dictionary completion.\n" + description="Enable experimental dictionary/list completion.\n" 'WARNING: this does "eval" on fragments of\n' " your Python input and is\n" " potentially unsafe.", From 6bf312f0b118bcb2bc0fe58f12af0906ea4af4d7 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 14 Apr 2020 16:48:48 +0200 Subject: [PATCH 261/470] Added 'title' option to ptpython. --- examples/ptpython_config/config.py | 6 ++++++ ptpython/layout.py | 3 +++ ptpython/python_input.py | 2 ++ 3 files changed, 11 insertions(+) diff --git a/examples/ptpython_config/config.py b/examples/ptpython_config/config.py index 3a1175ca..9c7241f8 100644 --- a/examples/ptpython_config/config.py +++ b/examples/ptpython_config/config.py @@ -171,6 +171,12 @@ def _(event): b.insert_text(' ') """ + # Add a custom title to the status bar. This is useful when ptpython is + # embedded in other applications. + """ + repl.title = "My custom prompt." + """ + # Custom colorscheme for the UI. See `ptpython/layout.py` and # `ptpython/style.py` for all possible tokens. diff --git a/ptpython/layout.py b/ptpython/layout.py index bf783c6a..09f2c8ed 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -413,6 +413,9 @@ def toggle_vi_mode(mouse_event: MouseEvent) -> None: result: StyleAndTextTuples = [] append = result.append + if python_input.title: + result.extend(to_formatted_text(python_input.title)) + append((input_mode_t, "[F4] ", toggle_vi_mode)) # InputMode diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 462e9b0c..eaf818d4 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -15,6 +15,7 @@ ThreadedAutoSuggest, ) from prompt_toolkit.buffer import Buffer +from prompt_toolkit.formatted_text import AnyFormattedText from prompt_toolkit.completion import Completer, FuzzyCompleter, ThreadedCompleter from prompt_toolkit.document import Document from prompt_toolkit.enums import DEFAULT_BUFFER, EditingMode @@ -204,6 +205,7 @@ def __init__( self.extra_key_bindings = extra_key_bindings or KeyBindings() # Settings. + self.title: AnyFormattedText = '' self.show_signature: bool = False self.show_docstring: bool = False self.show_meta_enter_message: bool = True From 75dceaa086b6659eab446143b0d995b50e82d9b2 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 14 Apr 2020 16:49:26 +0200 Subject: [PATCH 262/470] Added a few more type annotations. --- ptpython/completer.py | 2 +- ptpython/layout.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ptpython/completer.py b/ptpython/completer.py index 46995ba7..65d88a46 100644 --- a/ptpython/completer.py +++ b/ptpython/completer.py @@ -301,7 +301,7 @@ def _lookup(self, expression: str, temp_locals: Dict[str, Any]) -> object: try: return eval(expression.strip(), self.get_globals(), temp_locals) except BaseException: - return # Many exception, like NameError can be thrown here. + return None # Many exception, like NameError can be thrown here. def get_completions( self, document: Document, complete_event: CompleteEvent diff --git a/ptpython/layout.py b/ptpython/layout.py index 09f2c8ed..5e114879 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -303,13 +303,13 @@ class PythonPromptMargin(PromptMargin): It shows something like "In [1]:". """ - def __init__(self, python_input): + def __init__(self, python_input) -> None: self.python_input = python_input def get_prompt_style(): return python_input.all_prompt_styles[python_input.prompt_style] - def get_prompt(): + def get_prompt() -> StyleAndTextTuples: return to_formatted_text(get_prompt_style().in_prompt()) def get_continuation(width, line_number, is_soft_wrap): @@ -508,7 +508,7 @@ def exit_confirmation( Create `Layout` for the exit message. """ - def get_text_fragments(): + def get_text_fragments() -> StyleAndTextTuples: # Show "Do you really want to exit?" return [ (style, "\n %s ([y]/n)" % python_input.exit_message), @@ -564,7 +564,7 @@ def __init__( extra_toolbars=None, extra_buffer_processors=None, input_buffer_height: Optional[AnyDimension] = None, - ): + ) -> None: D = Dimension extra_body = [extra_body] if extra_body else [] extra_toolbars = extra_toolbars or [] From 7346416270bc9d954958618dad61cd6116a760ea Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 14 Apr 2020 16:49:49 +0200 Subject: [PATCH 263/470] Fixed python-embed-with-custom-prompt example. --- examples/python-embed-with-custom-prompt.py | 46 +++++++-------------- ptpython/completer.py | 2 +- ptpython/prompt_style.py | 20 ++++----- ptpython/python_input.py | 2 +- ptpython/repl.py | 4 +- 5 files changed, 30 insertions(+), 44 deletions(-) diff --git a/examples/python-embed-with-custom-prompt.py b/examples/python-embed-with-custom-prompt.py index bf27e936..85fd97c9 100755 --- a/examples/python-embed-with-custom-prompt.py +++ b/examples/python-embed-with-custom-prompt.py @@ -5,46 +5,32 @@ from __future__ import unicode_literals from pygments.token import Token +from prompt_toolkit.formatted_text import HTML from ptpython.prompt_style import PromptStyle from ptpython.repl import embed def configure(repl): - # There are several ways to override the prompt. - - # 1. Probably, the best is to add a new PromptStyle to `all_prompt_styles` - # and activate it. This way, the other styles are still selectable from - # the menu. + # Probably, the best is to add a new PromptStyle to `all_prompt_styles` and + # activate it. This way, the other styles are still selectable from the + # menu. class CustomPrompt(PromptStyle): - def in_tokens(self, cli): - return [ - (Token.In, "Input["), - (Token.In.Number, "%s" % repl.current_statement_index), - (Token.In, "] >>: "), - ] - - def in2_tokens(self, cli, width): - return [(Token.In, "...: ".rjust(width))] - - def out_tokens(self, cli): - return [ - (Token.Out, "Result["), - (Token.Out.Number, "%s" % repl.current_statement_index), - (Token.Out, "]: "), - ] - - repl.all_prompt_styles["custom"] = CustomPrompt() - repl.prompt_style = "custom" + def in_prompt(self): + return HTML("Input[%s]: ") % ( + repl.current_statement_index, + ) - # 2. Assign a new callable to `get_input_prompt_tokens`. This will always take effect. - ## repl.get_input_prompt_tokens = lambda cli: [(Token.In, '[hello] >>> ')] + def in2_prompt(self, width): + return "...: ".rjust(width) - # 3. Also replace `get_input_prompt_tokens`, but still call the original. This inserts - # a prefix. + def out_prompt(self): + return HTML("Result[%s]: ") % ( + repl.current_statement_index, + ) - ## original = repl.get_input_prompt_tokens - ## repl.get_input_prompt_tokens = lambda cli: [(Token.In, '[prefix]')] + original(cli) + repl.all_prompt_styles["custom"] = CustomPrompt() + repl.prompt_style = "custom" def main(): diff --git a/ptpython/completer.py b/ptpython/completer.py index 65d88a46..d8ec87b9 100644 --- a/ptpython/completer.py +++ b/ptpython/completer.py @@ -129,7 +129,7 @@ def get_completions( for c in self.dictionary_completer.get_completions( document, complete_event ): - if c.text not in '[.': + if c.text not in "[.": # If we get the [ or . completion, still include the other # completions. has_dict_completions = True diff --git a/ptpython/prompt_style.py b/ptpython/prompt_style.py index d5e6ca8c..24e5f883 100644 --- a/ptpython/prompt_style.py +++ b/ptpython/prompt_style.py @@ -1,7 +1,7 @@ from abc import ABCMeta, abstractmethod from typing import TYPE_CHECKING -from prompt_toolkit.formatted_text import StyleAndTextTuples +from prompt_toolkit.formatted_text import AnyFormattedText if TYPE_CHECKING: from .python_input import PythonInput @@ -15,12 +15,12 @@ class PromptStyle(metaclass=ABCMeta): """ @abstractmethod - def in_prompt(self) -> StyleAndTextTuples: + def in_prompt(self) -> AnyFormattedText: " Return the input tokens. " return [] @abstractmethod - def in2_prompt(self, width: int) -> StyleAndTextTuples: + def in2_prompt(self, width: int) -> AnyFormattedText: """ Tokens for every following input line. @@ -30,7 +30,7 @@ def in2_prompt(self, width: int) -> StyleAndTextTuples: return [] @abstractmethod - def out_prompt(self) -> StyleAndTextTuples: + def out_prompt(self) -> AnyFormattedText: " Return the output tokens. " return [] @@ -43,17 +43,17 @@ class IPythonPrompt(PromptStyle): def __init__(self, python_input: "PythonInput") -> None: self.python_input = python_input - def in_prompt(self) -> StyleAndTextTuples: + def in_prompt(self) -> AnyFormattedText: return [ ("class:in", "In ["), ("class:in.number", "%s" % self.python_input.current_statement_index), ("class:in", "]: "), ] - def in2_prompt(self, width: int) -> StyleAndTextTuples: + def in2_prompt(self, width: int) -> AnyFormattedText: return [("class:in", "...: ".rjust(width))] - def out_prompt(self) -> StyleAndTextTuples: + def out_prompt(self) -> AnyFormattedText: return [ ("class:out", "Out["), ("class:out.number", "%s" % self.python_input.current_statement_index), @@ -67,11 +67,11 @@ class ClassicPrompt(PromptStyle): The classic Python prompt. """ - def in_prompt(self) -> StyleAndTextTuples: + def in_prompt(self) -> AnyFormattedText: return [("class:prompt", ">>> ")] - def in2_prompt(self, width: int) -> StyleAndTextTuples: + def in2_prompt(self, width: int) -> AnyFormattedText: return [("class:prompt.dots", "...")] - def out_prompt(self) -> StyleAndTextTuples: + def out_prompt(self) -> AnyFormattedText: return [] diff --git a/ptpython/python_input.py b/ptpython/python_input.py index eaf818d4..0fc2f85a 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -205,7 +205,7 @@ def __init__( self.extra_key_bindings = extra_key_bindings or KeyBindings() # Settings. - self.title: AnyFormattedText = '' + self.title: AnyFormattedText = "" self.show_signature: bool = False self.show_docstring: bool = False self.show_meta_enter_message: bool = True diff --git a/ptpython/repl.py b/ptpython/repl.py index 06062dc3..69c53e32 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -21,7 +21,7 @@ PygmentsTokens, merge_formatted_text, ) -from prompt_toolkit.formatted_text.utils import fragment_list_width +from prompt_toolkit.formatted_text import fragment_list_width, to_formatted_text from prompt_toolkit.patch_stdout import patch_stdout as patch_stdout_context from prompt_toolkit.shortcuts import clear_title, print_formatted_text, set_title from prompt_toolkit.utils import DummyContext @@ -152,7 +152,7 @@ def compile_with_flags(code: str, mode: str): locals["_"] = locals["_%i" % self.current_statement_index] = result if result is not None: - out_prompt = self.get_output_prompt() + out_prompt = to_formatted_text(self.get_output_prompt()) try: result_str = "%r\n" % (result,) From d8b4eae00f2c3fd3888ce78709331df2dd2e1ab3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Grignard?= Date: Fri, 20 Mar 2020 09:08:33 +0100 Subject: [PATCH 264/470] fix: custom REPL input/output --- ptpython/python_input.py | 10 ++++++++-- ptpython/repl.py | 2 ++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 0fc2f85a..c14f9393 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -324,7 +324,7 @@ def __init__( extra_toolbars=self._extra_toolbars, ) - self.app = self._create_application() + self.app = self._create_application(input, output) if vi_mode: self.app.editing_mode = EditingMode.VI @@ -728,7 +728,11 @@ def get_values(): ), ] - def _create_application(self) -> Application: + def _create_application( + self, + input: Optional[Input], + output: Optional[Output] + ) -> Application: """ Create an `Application` instance. """ @@ -758,6 +762,8 @@ def _create_application(self) -> Application: style_transformation=self.style_transformation, include_default_pygments_style=False, reverse_vi_search_direction=True, + input=input, + output=output, ) def _create_buffer(self) -> Buffer: diff --git a/ptpython/repl.py b/ptpython/repl.py index 69c53e32..c7e71663 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -187,6 +187,7 @@ def compile_with_flags(code: str, mode: str): style=self._current_style, style_transformation=self.style_transformation, include_default_pygments_style=False, + output=output, ) # If not a valid `eval` expression, run using `exec` instead. @@ -233,6 +234,7 @@ def _handle_exception(self, e: Exception) -> None: style=self._current_style, style_transformation=self.style_transformation, include_default_pygments_style=False, + output=output, ) output.write("%s\n" % e) From fdb9e018412cde643cf86c183396adbec3078aed Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 14 Apr 2020 16:59:54 +0200 Subject: [PATCH 265/470] Run Mypy in CI. --- .travis.yml | 3 +++ mypy.ini | 6 ++++++ ptpython/contrib/asyncssh_repl.py | 4 ++-- 3 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 mypy.ini diff --git a/.travis.yml b/.travis.yml index 21611f91..6b1b8d65 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,3 +18,6 @@ script: - isort -c -rc ptpython tests setup.py examples - black --check ptpython setup.py examples + + # Type checking + - mypy ptpython diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 00000000..5a7ef2eb --- /dev/null +++ b/mypy.ini @@ -0,0 +1,6 @@ +[mypy] +ignore_missing_imports = True +no_implicit_optional = True +platform = win32 +strict_equality = True +strict_optional = True diff --git a/ptpython/contrib/asyncssh_repl.py b/ptpython/contrib/asyncssh_repl.py index 29c63afb..4c36217d 100644 --- a/ptpython/contrib/asyncssh_repl.py +++ b/ptpython/contrib/asyncssh_repl.py @@ -7,7 +7,7 @@ package should be installable in Python 2 as well! """ import asyncio -from typing import Optional, TextIO, cast +from typing import Any, Optional, TextIO, cast import asyncssh from prompt_toolkit.data_structures import Size @@ -31,7 +31,7 @@ class ReplSSHServerSession(asyncssh.SSHServerSession): def __init__( self, get_globals: _GetNamespace, get_locals: Optional[_GetNamespace] = None ) -> None: - self._chan = None + self._chan: Any = None def _globals() -> dict: data = get_globals() From c1aaf400d2747653012df36e1acdef6a47f3cbb8 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 14 Apr 2020 17:50:01 +0200 Subject: [PATCH 266/470] Fixed sorting of imports. --- examples/python-embed-with-custom-prompt.py | 2 +- ptpython/python_input.py | 2 +- ptpython/repl.py | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/examples/python-embed-with-custom-prompt.py b/examples/python-embed-with-custom-prompt.py index 85fd97c9..05417282 100755 --- a/examples/python-embed-with-custom-prompt.py +++ b/examples/python-embed-with-custom-prompt.py @@ -4,8 +4,8 @@ """ from __future__ import unicode_literals -from pygments.token import Token from prompt_toolkit.formatted_text import HTML +from pygments.token import Token from ptpython.prompt_style import PromptStyle from ptpython.repl import embed diff --git a/ptpython/python_input.py b/ptpython/python_input.py index c14f9393..6140826a 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -15,11 +15,11 @@ ThreadedAutoSuggest, ) from prompt_toolkit.buffer import Buffer -from prompt_toolkit.formatted_text import AnyFormattedText from prompt_toolkit.completion import Completer, FuzzyCompleter, ThreadedCompleter from prompt_toolkit.document import Document from prompt_toolkit.enums import DEFAULT_BUFFER, EditingMode from prompt_toolkit.filters import Condition +from prompt_toolkit.formatted_text import AnyFormattedText from prompt_toolkit.history import ( FileHistory, History, diff --git a/ptpython/repl.py b/ptpython/repl.py index c7e71663..897af693 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -19,9 +19,10 @@ from prompt_toolkit.formatted_text import ( FormattedText, PygmentsTokens, + fragment_list_width, merge_formatted_text, + to_formatted_text, ) -from prompt_toolkit.formatted_text import fragment_list_width, to_formatted_text from prompt_toolkit.patch_stdout import patch_stdout as patch_stdout_context from prompt_toolkit.shortcuts import clear_title, print_formatted_text, set_title from prompt_toolkit.utils import DummyContext From 469ef08b1c9b2bbc124c8d0cf03f98abdd9da349 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 14 Apr 2020 17:51:29 +0200 Subject: [PATCH 267/470] Added mypy to 'pip install' in .travis.yml. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 6b1b8d65..7061cb5d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,7 @@ matrix: - python: 3.7 install: - - travis_retry pip install . pytest isort black + - travis_retry pip install . pytest isort black mypy - pip list script: From 954be776bd1acc20d060ea297ea15e1e0b4a4d21 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 14 Apr 2020 22:29:02 +0200 Subject: [PATCH 268/470] Fixed code formatting. --- ptpython/python_input.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 6140826a..20eb5d90 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -729,9 +729,7 @@ def get_values(): ] def _create_application( - self, - input: Optional[Input], - output: Optional[Output] + self, input: Optional[Input], output: Optional[Output] ) -> Application: """ Create an `Application` instance. From ee18cb77675521474334fb4e0ac1e8bc5bb0adbf Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 14 Apr 2020 22:32:10 +0200 Subject: [PATCH 269/470] Added badges to README. --- README.rst | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index da60c1e6..38e34ce5 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,8 @@ ptpython ======== +|Build Status| |PyPI| |License| + *A better Python REPL* :: @@ -205,9 +207,12 @@ Special thanks to - `wcwidth `_: Determine columns needed for a wide characters. - `prompt_toolkit `_ for the interface. -.. |Build Status| image:: https://api.travis-ci.org/jonathanslenders/ptpython.svg?branch=master - :target: https://travis-ci.org/jonathanslenders/ptpython# +.. |Build Status| image:: https://api.travis-ci.org/prompt-toolkit/ptpython.svg?branch=master + :target: https://travis-ci.org/prompt-toolkit/ptpython# + +.. |License| image:: https://img.shields.io/github/license/prompt-toolkit/ptpython.svg + :target: https://github.com/prompt-toolkit/ptpython/blob/master/LICENSE -.. |PyPI| image:: https://pypip.in/version/prompt-toolkit/badge.svg - :target: https://pypi.python.org/pypi/prompt-toolkit/ +.. |PyPI| image:: https://pypip.in/version/ptpython/badge.svg + :target: https://pypi.python.org/pypi/ptpython/ :alt: Latest Version From 448b65555ec28deefb5c2700042c71e505faa685 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 14 Apr 2020 22:36:01 +0200 Subject: [PATCH 270/470] Fixed badges location in README. --- README.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.rst b/README.rst index 38e34ce5..481238a3 100644 --- a/README.rst +++ b/README.rst @@ -11,8 +11,6 @@ ptpython .. image :: https://github.com/jonathanslenders/ptpython/raw/master/docs/images/example1.png -|Build Status| - Ptpython is an advanced Python REPL. It should work on all Python versions from 2.6 up to 3.7 and work cross platform (Linux, BSD, OS X and Windows). From 89017ba158ed1d95319233fa5aedf3931c3b8b77 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 14 Apr 2020 22:47:44 +0200 Subject: [PATCH 271/470] Release 3.0.2 --- CHANGELOG | 12 ++++++++++++ setup.py | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index a90d86e1..2c385c6f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,18 @@ CHANGELOG ========= +3.0.2: 2020-04-14 +----------------- + +New features: +- Improved custom dictionary completion: + * Also complete list indexes. + * Also complete attributes after doing a dictionary lookup. + * Also complete iterators in a for-loop. +- Added a 'title' option, so that applications embedding ptpython can set a + title in the status bar. + + 3.0.1: 2020-02-24 ----------------- diff --git a/setup.py b/setup.py index 8fbc277c..4a59ad80 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ setup( name="ptpython", author="Jonathan Slenders", - version="3.0.1", + version="3.0.2", url="https://github.com/prompt-toolkit/ptpython", description="Python REPL build on top of prompt_toolkit", long_description=long_description, From 493c48e280ecf62099f089d411587b473781cfe9 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 9 Jul 2020 17:54:42 +0200 Subject: [PATCH 272/470] Sort attribute names for DictionaryCompleter and move underscored attributes to the end. --- ptpython/completer.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/ptpython/completer.py b/ptpython/completer.py index d8ec87b9..00ba8258 100644 --- a/ptpython/completer.py +++ b/ptpython/completer.py @@ -1,7 +1,7 @@ import ast import keyword import re -from typing import TYPE_CHECKING, Any, Dict, Iterable +from typing import TYPE_CHECKING, Any, Dict, Iterable, List from prompt_toolkit.completion import ( CompleteEvent, @@ -430,12 +430,29 @@ def _get_attribute_completions( # Do lookup of `object_var` in the context. result = self._lookup(object_var, temp_locals) - for name in dir(result): + names = self._sort_attribute_names(dir(result)) + + for name in names: if name.startswith(attr_name): yield Completion( name, -len(attr_name), ) + def _sort_attribute_names(self, names: List[str]) -> List[str]: + """ + Sort attribute names alphabetically, but move the double underscore and + underscore names to the end. + """ + + def sort_key(name: str): + if name.startswith("__"): + return (2, name) # Double underscore comes latest. + if name.startswith("_"): + return (1, name) # Single underscore before that. + return (0, name) # Other names first. + + return sorted(names, key=sort_key) + class ReprFailedError(Exception): " Raised when the repr() call in `DictionaryCompleter` fails. " From 82e61370682c46fe39fd81f194072429340bb240 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Fri, 10 Jul 2020 15:58:32 +0200 Subject: [PATCH 273/470] Isort fixes. --- ptpython/entry_points/run_ptipython.py | 2 +- ptpython/eventloop.py | 3 ++- ptpython/ipython.py | 8 ++++---- ptpython/layout.py | 2 +- ptpython/python_input.py | 3 ++- 5 files changed, 10 insertions(+), 8 deletions(-) diff --git a/ptpython/entry_points/run_ptipython.py b/ptpython/entry_points/run_ptipython.py index e7bcf39a..650633ec 100644 --- a/ptpython/entry_points/run_ptipython.py +++ b/ptpython/entry_points/run_ptipython.py @@ -19,7 +19,7 @@ def run(user_ns=None): sys.exit(1) else: from ptpython.ipython import embed - from ptpython.repl import run_config, enable_deprecation_warnings + from ptpython.repl import enable_deprecation_warnings, run_config # Add the current directory to `sys.path`. if sys.path[0] != "": diff --git a/ptpython/eventloop.py b/ptpython/eventloop.py index 1e8c46a3..c841972d 100644 --- a/ptpython/eventloop.py +++ b/ptpython/eventloop.py @@ -19,9 +19,10 @@ def _inputhook_tk(inputhook_context): Run the Tk eventloop until prompt-toolkit needs to process the next input. """ # Get the current TK application. - import _tkinter # Keep this imports inline! import tkinter + import _tkinter # Keep this imports inline! + root = tkinter._default_root def wait_using_filehandler(): diff --git a/ptpython/ipython.py b/ptpython/ipython.py index 20f29bdc..169aa2db 100644 --- a/ptpython/ipython.py +++ b/ptpython/ipython.py @@ -8,6 +8,10 @@ offer. """ +from IPython import utils as ipy_utils +from IPython.core.inputsplitter import IPythonInputSplitter +from IPython.terminal.embed import InteractiveShellEmbed as _InteractiveShellEmbed +from IPython.terminal.ipapp import load_default_config from prompt_toolkit.completion import ( Completer, Completion, @@ -24,10 +28,6 @@ from prompt_toolkit.styles import Style from pygments.lexers import BashLexer, PythonLexer -from IPython import utils as ipy_utils -from IPython.core.inputsplitter import IPythonInputSplitter -from IPython.terminal.embed import InteractiveShellEmbed as _InteractiveShellEmbed -from IPython.terminal.ipapp import load_default_config from ptpython.prompt_style import PromptStyle from .python_input import PythonCompleter, PythonInput, PythonValidator diff --git a/ptpython/layout.py b/ptpython/layout.py index 5e114879..3940e7a1 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -57,7 +57,7 @@ from .utils import if_mousedown if TYPE_CHECKING: - from .python_input import PythonInput, OptionCategory + from .python_input import OptionCategory, PythonInput __all__ = ["PtPythonLayout", "CompletionVisualisation"] diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 20eb5d90..7c57cf1e 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -891,9 +891,10 @@ def enter_history(self) -> None: history = PythonHistory(self, self.default_buffer.document) - from prompt_toolkit.application import in_terminal import asyncio + from prompt_toolkit.application import in_terminal + async def do_in_terminal() -> None: async with in_terminal(): result = await history.app.run_async() From 356dc481dadac56f636747daedeb46d0bb0f9321 Mon Sep 17 00:00:00 2001 From: Linus Pithan Date: Mon, 22 Jun 2020 11:31:28 +0200 Subject: [PATCH 274/470] get rid of 'Unhandled exception in event loop' caused by `get_compiler_flags` --- ptpython/python_input.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 7c57cf1e..bddbb2ef 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -361,8 +361,14 @@ def get_compiler_flags(self) -> int: flags = 0 for value in self.get_globals().values(): - if isinstance(value, __future__._Feature): - flags |= value.compiler_flag + try: + if isinstance(value, __future__._Feature): + f = value.compiler_flag + flags |= f + except BaseException: + # get_compiler_flags should never raise to not run into an + # `Unhandled exception in event loop` + pass return flags From 58e3cadf50914b630d99c0c48ccec65fa7488d04 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Fri, 10 Jul 2020 16:08:00 +0200 Subject: [PATCH 275/470] Added comment to 'get_compiler_flags' error handling. --- ptpython/python_input.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ptpython/python_input.py b/ptpython/python_input.py index bddbb2ef..e41b921f 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -368,6 +368,10 @@ def get_compiler_flags(self) -> int: except BaseException: # get_compiler_flags should never raise to not run into an # `Unhandled exception in event loop` + + # See: https://github.com/prompt-toolkit/ptpython/issues/351 + # An exception can be raised when some objects in the globals + # raise an exception in a custom `__getattribute__`. pass return flags From 458d26b377714d05570ea66742756535676bc6f8 Mon Sep 17 00:00:00 2001 From: Vincent Michel Date: Sat, 23 May 2020 11:45:29 +0200 Subject: [PATCH 276/470] Improve PythonRepl.run_async() method --- ptpython/repl.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/ptpython/repl.py b/ptpython/repl.py index 897af693..8633890e 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -94,9 +94,23 @@ def prompt() -> str: clear_title() async def run_async(self) -> None: + if self.terminal_title: + set_title(self.terminal_title) + while True: - text = await self.app.run_async() - self._process_text(text) + # Run the UI. + try: + text = await self.app.run_async() + except EOFError: + return + except KeyboardInterrupt: + # Abort - try again. + self.default_buffer.document = Document() + else: + self._process_text(text) + + if self.terminal_title: + clear_title() def _process_text(self, line: str) -> None: From f1ad66298972088ba4e4b72c847b7688167f14cc Mon Sep 17 00:00:00 2001 From: Vincent Michel Date: Sat, 23 May 2020 11:58:04 +0200 Subject: [PATCH 277/470] Remove duplicated code in PythonRepl.run() --- ptpython/repl.py | 55 ++++++++++++++++-------------------------------- 1 file changed, 18 insertions(+), 37 deletions(-) diff --git a/ptpython/repl.py b/ptpython/repl.py index 8633890e..237f6fb7 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -55,43 +55,24 @@ def _load_start_paths(self) -> None: output.write("WARNING | File not found: {}\n\n".format(path)) def run(self) -> None: - if self.terminal_title: - set_title(self.terminal_title) - - def prompt() -> str: - # In order to make sure that asyncio code written in the - # interactive shell doesn't interfere with the prompt, we run the - # prompt in a different event loop. - # If we don't do this, people could spawn coroutine with a - # while/true inside which will freeze the prompt. - - try: - old_loop: Optional[asyncio.AbstractEventLoop] = asyncio.get_event_loop() - except RuntimeError: - # This happens when the user used `asyncio.run()`. - old_loop = None - - asyncio.set_event_loop(self.pt_loop) - try: - return self.app.run() # inputhook=inputhook) - finally: - # Restore the original event loop. - asyncio.set_event_loop(old_loop) - - while True: - # Run the UI. - try: - text = prompt() - except EOFError: - return - except KeyboardInterrupt: - # Abort - try again. - self.default_buffer.document = Document() - else: - self._process_text(text) - - if self.terminal_title: - clear_title() + # In order to make sure that asyncio code written in the + # interactive shell doesn't interfere with the prompt, we run the + # prompt in a different event loop. + # If we don't do this, people could spawn coroutine with a + # while/true inside which will freeze the prompt. + + try: + old_loop: Optional[asyncio.AbstractEventLoop] = asyncio.get_event_loop() + except RuntimeError: + # This happens when the user used `asyncio.run()`. + old_loop = None + + asyncio.set_event_loop(self.pt_loop) + try: + return self.pt_loop.run_until_complete(self.run_async()) + finally: + # Restore the original event loop. + asyncio.set_event_loop(old_loop) async def run_async(self) -> None: if self.terminal_title: From 1fe521a7023181cdebeaec283fca498ace41a690 Mon Sep 17 00:00:00 2001 From: Julian Berman Date: Fri, 8 May 2020 08:00:21 -0400 Subject: [PATCH 278/470] Fix ptpython --version. Previously it'd just start the interpreter and not do anything. --- ptpython/entry_points/run_ptpython.py | 10 +++++++++- setup.py | 1 + 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/ptpython/entry_points/run_ptpython.py b/ptpython/entry_points/run_ptpython.py index 204a94aa..d2c382ff 100644 --- a/ptpython/entry_points/run_ptpython.py +++ b/ptpython/entry_points/run_ptpython.py @@ -21,6 +21,11 @@ import sys from typing import Tuple +try: + from importlib import metadata +except ImportError: + import importlib_metadata as metadata + import appdirs from prompt_toolkit.formatted_text import HTML from prompt_toolkit.shortcuts import print_formatted_text @@ -51,7 +56,10 @@ def create_parser() -> _Parser: ) parser.add_argument("--history-file", type=str, help="Location of history file.") parser.add_argument( - "-V", "--version", action="store_true", help="Print version and exit." + "-V", + "--version", + action="version", + version=metadata.version("ptpython"), ) parser.add_argument("args", nargs="*", help="Script and arguments") return parser diff --git a/setup.py b/setup.py index 4a59ad80..288870dd 100644 --- a/setup.py +++ b/setup.py @@ -18,6 +18,7 @@ packages=find_packages("."), install_requires=[ "appdirs", + "importlib_metadata;python_version<'3.8'", "jedi>=0.9.0", "prompt_toolkit>=3.0.0,<3.1.0", "pygments", From f5e7fb4e5515bbd8c5017dd649c14bc70c8baf64 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Fri, 10 Jul 2020 16:15:58 +0200 Subject: [PATCH 279/470] Fixed custom style in example config. --- examples/ptpython_config/config.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/ptpython_config/config.py b/examples/ptpython_config/config.py index 9c7241f8..aa0bb635 100644 --- a/examples/ptpython_config/config.py +++ b/examples/ptpython_config/config.py @@ -8,7 +8,7 @@ from prompt_toolkit.filters import ViInsertMode from prompt_toolkit.key_binding.key_processor import KeyPress from prompt_toolkit.keys import Keys -from pygments.token import Token +from prompt_toolkit.styles import Style from ptpython.layout import CompletionVisualisation @@ -121,7 +121,7 @@ def configure(repl): # Install custom colorscheme named 'my-colorscheme' and use it. """ - repl.install_ui_colorscheme('my-colorscheme', _custom_ui_colorscheme) + repl.install_ui_colorscheme('my-colorscheme', Style.from_dict(_custom_ui_colorscheme)) repl.use_ui_colorscheme('my-colorscheme') """ @@ -182,7 +182,7 @@ def _(event): # `ptpython/style.py` for all possible tokens. _custom_ui_colorscheme = { # Blue prompt. - Token.Layout.Prompt: "bg:#eeeeff #000000 bold", + "prompt": "bg:#eeeeff #000000 bold", # Make the status toolbar red. - Token.Toolbar.Status: "bg:#ff0000 #000000", + "status-toolbar": "bg:#ff0000 #000000", } From 5d46a58b5cdbf996b2243634793ddf1bfd0561c9 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Fri, 10 Jul 2020 16:44:59 +0200 Subject: [PATCH 280/470] Type:ignore for ImportError. --- ptpython/entry_points/run_ptpython.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ptpython/entry_points/run_ptpython.py b/ptpython/entry_points/run_ptpython.py index d2c382ff..aeb5c26d 100644 --- a/ptpython/entry_points/run_ptpython.py +++ b/ptpython/entry_points/run_ptpython.py @@ -24,7 +24,7 @@ try: from importlib import metadata except ImportError: - import importlib_metadata as metadata + import importlib_metadata as metadata # type: ignore import appdirs from prompt_toolkit.formatted_text import HTML @@ -59,7 +59,7 @@ def create_parser() -> _Parser: "-V", "--version", action="version", - version=metadata.version("ptpython"), + version=metadata.version("ptpython"), # type: ignore ) parser.add_argument("args", nargs="*", help="Script and arguments") return parser From a94449e5095ee93d7327bada4fb0478ad3ea6911 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Fri, 10 Jul 2020 16:47:42 +0200 Subject: [PATCH 281/470] Release 3.0.3 --- CHANGELOG | 11 +++++++++++ setup.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 2c385c6f..9a6d6447 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,17 @@ CHANGELOG ========= +3.0.3: 2020-07-10 +----------------- + +Fixes: +- Sort attribute names for `DictionaryCompleter` and move underscored + attributes to the end. +- Handle unhandled exceptions in `get_compiler_flags`. +- Improved `run_async` code. +- Fix --version parameter. + + 3.0.2: 2020-04-14 ----------------- diff --git a/setup.py b/setup.py index 288870dd..b6d42497 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ setup( name="ptpython", author="Jonathan Slenders", - version="3.0.2", + version="3.0.3", url="https://github.com/prompt-toolkit/ptpython", description="Python REPL build on top of prompt_toolkit", long_description=long_description, From d4ddea30779db1c2f3fa02c5302c5a3397f67ab5 Mon Sep 17 00:00:00 2001 From: Nasy Date: Mon, 27 Jul 2020 12:24:26 -0400 Subject: [PATCH 282/470] Replace IPython.utils.warn with warnings.warn (#370) * Replace IPython.utils.warn with warnings.warn IPython.utils.warn was removed. * Fixed isort --- ptpython/ipython.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ptpython/ipython.py b/ptpython/ipython.py index 169aa2db..2e8d1195 100644 --- a/ptpython/ipython.py +++ b/ptpython/ipython.py @@ -8,6 +8,8 @@ offer. """ +from warnings import warn + from IPython import utils as ipy_utils from IPython.core.inputsplitter import IPythonInputSplitter from IPython.terminal.embed import InteractiveShellEmbed as _InteractiveShellEmbed @@ -259,7 +261,7 @@ def initialize_extensions(shell, extensions): try: shell.extension_manager.load_extension(ext) except: - ipy_utils.warn.warn( + warn( "Error in loading extension: %s" % ext + "\nCheck your config files in %s" % ipy_utils.path.get_ipython_dir() From 6f7d953a165ad7bcedf6af1bd8cfe7658efe7818 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Fri, 31 Jul 2020 11:33:31 +0200 Subject: [PATCH 283/470] Show full syntax error in Validator. --- ptpython/validator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ptpython/validator.py b/ptpython/validator.py index b7880bf6..8e98e878 100644 --- a/ptpython/validator.py +++ b/ptpython/validator.py @@ -37,7 +37,7 @@ def validate(self, document): index = document.translate_row_col_to_index( e.lineno - 1, (e.offset or 1) - 1 ) - raise ValidationError(index, "Syntax Error") + raise ValidationError(index, f"Syntax Error: {e}") except TypeError as e: # e.g. "compile() expected string without null bytes" raise ValidationError(0, str(e)) From 85c4fc1c08795da7147da3ec3a007409a2fc70a2 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Fri, 31 Jul 2020 11:45:01 +0200 Subject: [PATCH 284/470] Allow leading whitespace before single line expressions. --- ptpython/repl.py | 5 +++++ ptpython/validator.py | 11 +++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/ptpython/repl.py b/ptpython/repl.py index 237f6fb7..5b8af92f 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -131,6 +131,11 @@ def compile_with_flags(code: str, mode: str): dont_inherit=True, ) + # If the input is single line, remove leading whitespace. + # (This doesn't have to be a syntax error.) + if len(line.splitlines()) == 1: + line = line.strip() + if line.lstrip().startswith("\x1a"): # When the input starts with Ctrl-Z, quit the REPL. self.app.exit() diff --git a/ptpython/validator.py b/ptpython/validator.py index 8e98e878..b63bedcb 100644 --- a/ptpython/validator.py +++ b/ptpython/validator.py @@ -18,9 +18,16 @@ def validate(self, document): """ Check input for Python syntax errors. """ + text = document.text + + # If the input is single line, remove leading whitespace. + # (This doesn't have to be a syntax error.) + if len(text.splitlines()) == 1: + text = text.strip() + # When the input starts with Ctrl-Z, always accept. This means EOF in a # Python REPL. - if document.text.startswith("\x1a"): + if text.startswith("\x1a"): return try: @@ -29,7 +36,7 @@ def validate(self, document): else: flags = 0 - compile(document.text, "", "exec", flags=flags, dont_inherit=True) + compile(text, "", "exec", flags=flags, dont_inherit=True) except SyntaxError as e: # Note, the 'or 1' for offset is required because Python 2.7 # gives `None` as offset in case of '4=4' as input. (Looks like From 49f0e0562499f4b884bbd62905dc0f4bea94d6c9 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Fri, 31 Jul 2020 11:45:26 +0200 Subject: [PATCH 285/470] Bugfix in dictionary completion: don't recognize numbers as variable names. --- ptpython/completer.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ptpython/completer.py b/ptpython/completer.py index 00ba8258..c62e2cb3 100644 --- a/ptpython/completer.py +++ b/ptpython/completer.py @@ -222,20 +222,22 @@ def __init__(self, get_globals, get_locals): # Pattern for expressions that are "safe" to eval for auto-completion. # These are expressions that contain only attribute and index lookups. - expression = r""" + varname = r"[a-zA-Z_][a-zA-Z0-9_]*" + + expression = rf""" # Any expression safe enough to eval while typing. # No operators, except dot, and only other dict lookups. # Technically, this can be unsafe of course, if bad code runs # in `__getattr__` or ``__getitem__``. ( # Variable name - [a-zA-Z0-9_]+ + {varname} \s* (?: # Attribute access. - \s* \. \s* [a-zA-Z0-9_]+ \s* + \s* \. \s* {varname} \s* | From c33d05889d35eb506ddbf7e3b1779bda7f3f08a1 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Fri, 31 Jul 2020 11:46:07 +0200 Subject: [PATCH 286/470] Completed type annotation for embed() call. --- ptpython/repl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ptpython/repl.py b/ptpython/repl.py index 5b8af92f..cbfb33b5 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -311,7 +311,7 @@ def enter_to_continue() -> None: def embed( globals=None, locals=None, - configure: Optional[Callable] = None, + configure: Optional[Callable[[PythonRepl], None]] = None, vi_mode: bool = False, history_filename: Optional[str] = None, title: Optional[str] = None, From a8519bbb2120ec06bcee941e623f91a534845501 Mon Sep 17 00:00:00 2001 From: NotAFile Date: Tue, 4 Aug 2020 16:27:16 +0200 Subject: [PATCH 287/470] Document Embedding with IPython Support --- README.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.rst b/README.rst index 481238a3..aa0c8eaa 100644 --- a/README.rst +++ b/README.rst @@ -147,6 +147,13 @@ ipython``) .. image :: https://github.com/jonathanslenders/ptpython/raw/master/docs/images/ipython.png +This is also available for embedding: + +.. code:: python + + from ptpython.ipython.repl import embed + embed(globals(), locals()) + Django support ************** From 77a5bea3c3f234357aca064170e8be98255af0e6 Mon Sep 17 00:00:00 2001 From: Anton Alekseev Date: Wed, 5 Feb 2020 01:31:30 +0300 Subject: [PATCH 288/470] Add config options related to Vi input mode Setting `vi_start_in_nav_mode` to `True` enables `NAVIGATION` mode on startup. The issue is that due to the current behaviour of `ViState.reset()` input mode gets resetted back to `INSERT` on the every iteration of the main loop. In order to at one hand to provide the user with desired behaviour and on the other hand doesn't introduce breaking changes the other option `vi_keep_last_used_mode` was introduced which sets `input_mode` to the state observed before reset. `vi_keep_last_used_mode` can be useful even with `vi_start_in_nav_mode` set to `False` in the case the user prefer to start in `INSERT` mode but still wants to maintain the last mode he was in. In the case of `vi_keep_last_used_mode` set to `False` and `vi_start_in_nav_mode` to `True` `NAVIGATION` mode is set on every iteration the same way `INSERT` was set before this commit. Fixes #258. Commit rebased and modified by Jonathan Slenders. --- examples/ptpython_config/config.py | 6 ++++++ ptpython/python_input.py | 6 ++++++ ptpython/repl.py | 20 +++++++++++++++++++- 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/examples/ptpython_config/config.py b/examples/ptpython_config/config.py index aa0bb635..2a4ffd94 100644 --- a/examples/ptpython_config/config.py +++ b/examples/ptpython_config/config.py @@ -119,6 +119,12 @@ def configure(repl): # Syntax. repl.enable_syntax_highlighting = True + # Get into Vi navigation mode at startup + repl.vi_start_in_nav_mode = False + + # Preserve last used Vi input mode between main loop iterations + repl.vi_keep_last_used_mode = False + # Install custom colorscheme named 'my-colorscheme' and use it. """ repl.install_ui_colorscheme('my-colorscheme', Style.from_dict(_custom_ui_colorscheme)) diff --git a/ptpython/python_input.py b/ptpython/python_input.py index e41b921f..3794020a 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -300,6 +300,12 @@ def __init__( # (Never run more than one at the same time.) self._get_signatures_thread_running: bool = False + # Get into Vi navigation mode at startup + self.vi_start_in_nav_mode: bool = False + + # Preserve last used Vi input mode between main loop iterations + self.vi_keep_last_used_mode: bool = False + self.style_transformation = merge_style_transformations( [ ConditionalStyleTransformation( diff --git a/ptpython/repl.py b/ptpython/repl.py index cbfb33b5..44c077cb 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -23,6 +23,8 @@ merge_formatted_text, to_formatted_text, ) +from prompt_toolkit.formatted_text.utils import fragment_list_width +from prompt_toolkit.key_binding.vi_state import InputMode from prompt_toolkit.patch_stdout import patch_stdout as patch_stdout_context from prompt_toolkit.shortcuts import clear_title, print_formatted_text, set_title from prompt_toolkit.utils import DummyContext @@ -79,9 +81,21 @@ async def run_async(self) -> None: set_title(self.terminal_title) while True: + # Capture the current input_mode in order to restore it after reset, + # for ViState.reset() sets it to InputMode.INSERT unconditionally and + # doesn't accept any arguments. + def pre_run( + last_input_mode: InputMode = self.app.vi_state.input_mode, + ) -> None: + if self.vi_keep_last_used_mode: + self.app.vi_state.input_mode = last_input_mode + + if not self.vi_keep_last_used_mode and self.vi_start_in_nav_mode: + self.app.vi_state.input_mode = InputMode.NAVIGATION + # Run the UI. try: - text = await self.app.run_async() + text = await self.app.run_async(pre_run=pre_run) except EOFError: return except KeyboardInterrupt: @@ -363,6 +377,10 @@ def get_locals(): if configure: configure(repl) + # Set Vi input mode + if repl.vi_start_in_nav_mode: + repl.app.vi_state.input_mode = InputMode.NAVIGATION + # Start repl. patch_context: ContextManager = patch_stdout_context() if patch_stdout else DummyContext() From e42c8d69718d0dda7e3b1563e5a8ee0aa49581b8 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 6 Aug 2020 12:03:15 +0200 Subject: [PATCH 289/470] Renamed repl.vi_start_in_nav_mode to repl.vi_start_in_navigation_mode. --- examples/ptpython_config/config.py | 2 +- ptpython/python_input.py | 2 +- ptpython/repl.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/ptpython_config/config.py b/examples/ptpython_config/config.py index 2a4ffd94..1a009018 100644 --- a/examples/ptpython_config/config.py +++ b/examples/ptpython_config/config.py @@ -120,7 +120,7 @@ def configure(repl): repl.enable_syntax_highlighting = True # Get into Vi navigation mode at startup - repl.vi_start_in_nav_mode = False + repl.vi_start_in_navigation_mode = False # Preserve last used Vi input mode between main loop iterations repl.vi_keep_last_used_mode = False diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 3794020a..18b9ef69 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -301,7 +301,7 @@ def __init__( self._get_signatures_thread_running: bool = False # Get into Vi navigation mode at startup - self.vi_start_in_nav_mode: bool = False + self.vi_start_in_navigation_mode: bool = False # Preserve last used Vi input mode between main loop iterations self.vi_keep_last_used_mode: bool = False diff --git a/ptpython/repl.py b/ptpython/repl.py index 44c077cb..d4f4ad83 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -90,7 +90,7 @@ def pre_run( if self.vi_keep_last_used_mode: self.app.vi_state.input_mode = last_input_mode - if not self.vi_keep_last_used_mode and self.vi_start_in_nav_mode: + if not self.vi_keep_last_used_mode and self.vi_start_in_navigation_mode: self.app.vi_state.input_mode = InputMode.NAVIGATION # Run the UI. @@ -378,7 +378,7 @@ def get_locals(): configure(repl) # Set Vi input mode - if repl.vi_start_in_nav_mode: + if repl.vi_start_in_navigation_mode: repl.app.vi_state.input_mode = InputMode.NAVIGATION # Start repl. From 594f0e69cde7a855c80589b41633559768ef9ebd Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 6 Aug 2020 12:09:30 +0200 Subject: [PATCH 290/470] Fix: no need to handle vi_start_in_navigation_mode in the embed() call. --- ptpython/repl.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/ptpython/repl.py b/ptpython/repl.py index d4f4ad83..ba95a3d5 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -377,10 +377,6 @@ def get_locals(): if configure: configure(repl) - # Set Vi input mode - if repl.vi_start_in_navigation_mode: - repl.app.vi_state.input_mode = InputMode.NAVIGATION - # Start repl. patch_context: ContextManager = patch_stdout_context() if patch_stdout else DummyContext() From 7b185fc7870de4bfb8366afa40687ef66589f404 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 6 Aug 2020 12:44:35 +0200 Subject: [PATCH 291/470] Fix exit confirmation. Before, the exit confirmation was not focused. Which meant that key bindings of the main buffer were still active. If we are in Vi mode, that meant that there was a key binding for the ("y", "y") already, which caused the handling of "y" to be delayed (it was not marked as eager). This fix will focus the exit confirmation and avoid further interference of buffer key bindings. --- ptpython/key_bindings.py | 6 ++++++ ptpython/layout.py | 11 ++++++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/ptpython/key_bindings.py b/ptpython/key_bindings.py index 1740caf7..d5171cc9 100644 --- a/ptpython/key_bindings.py +++ b/ptpython/key_bindings.py @@ -187,7 +187,12 @@ def _(event): Override Control-D exit, to ask for confirmation. """ if python_input.confirm_exit: + # Show exit confirmation and focus it (focusing is important for + # making sure the default buffer key bindings are not active). python_input.show_exit_confirmation = True + python_input.app.layout.focus( + python_input.ptpython_layout.exit_confirmation + ) else: event.app.exit(exception=EOFError) @@ -279,6 +284,7 @@ def _(event): Cancel exit. """ python_input.show_exit_confirmation = False + python_input.app.layout.focus_previous() return bindings diff --git a/ptpython/layout.py b/ptpython/layout.py index 3940e7a1..d50a3a53 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -501,7 +501,7 @@ def get_text_fragments() -> StyleAndTextTuples: ) -def exit_confirmation( +def create_exit_confirmation( python_input: "PythonInput", style="class:exit-confirmation" ) -> Container: """ @@ -511,7 +511,7 @@ def exit_confirmation( def get_text_fragments() -> StyleAndTextTuples: # Show "Do you really want to exit?" return [ - (style, "\n %s ([y]/n)" % python_input.exit_message), + (style, "\n %s ([y]/n) " % python_input.exit_message), ("[SetCursorPosition]", ""), (style, " \n"), ] @@ -520,8 +520,8 @@ def get_text_fragments() -> StyleAndTextTuples: return ConditionalContainer( content=Window( - FormattedTextControl(get_text_fragments), style=style - ), # , has_focus=visible)), + FormattedTextControl(get_text_fragments, focusable=True), style=style + ), filter=visible, ) @@ -635,6 +635,7 @@ def menu_position(): ) sidebar = python_sidebar(python_input) + self.exit_confirmation = create_exit_confirmation(python_input) root_container = HSplit( [ @@ -680,7 +681,7 @@ def menu_position(): Float( left=2, bottom=1, - content=exit_confirmation(python_input), + content=self.exit_confirmation, ), Float( bottom=0, From c1353d2e15d13b3b0f86faf14efea347a9f56c73 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 6 Aug 2020 11:29:13 +0200 Subject: [PATCH 292/470] Improved dictionary completion. Handle strings as dictionary keys that contain spaces. --- ptpython/completer.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ptpython/completer.py b/ptpython/completer.py index c62e2cb3..1ff7bcc8 100644 --- a/ptpython/completer.py +++ b/ptpython/completer.py @@ -242,10 +242,10 @@ def __init__(self, get_globals, get_locals): | # Item lookup. - # (We match the square brackets. We don't care about - # matching quotes here in the regex. Nested square brackets - # are not supported.) - \s* \[ [a-zA-Z0-9_'"\s]+ \] \s* + # (We match the square brackets. The key can be anything. + # We don't care about matching quotes here in the regex. + # Nested square brackets are not supported.) + \s* \[ [^\[\]]+ \] \s* )* ) """ @@ -278,7 +278,7 @@ def __init__(self, get_globals, get_locals): # Dict loopup to complete (square bracket open + start of # string). \[ - \s* ([a-zA-Z0-9_'"]*)$ + \s* ([^\[\]]*)$ """, re.VERBOSE, ) From c786ca40176515464cf567ee49534e7e4cdd4e41 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Mon, 10 Aug 2020 12:06:13 +0200 Subject: [PATCH 293/470] Release 3.0.4 --- CHANGELOG | 14 ++++++++++++++ setup.py | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 9a6d6447..9615da16 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,20 @@ CHANGELOG ========= +3.0.4: 2020-08-10 +----------------- + +New features: +- Allow leading whitespace before single line expressions. +- Show full syntax error in validator. +- Added `vi_start_in_navigation_mode` and `vi_keep_last_used_mode` options. + +Fixes: +- Improved dictionary completion: handle keys that contain spaces and don't + recognize numbers as variable names. +- Fix in exit confirmation. + + 3.0.3: 2020-07-10 ----------------- diff --git a/setup.py b/setup.py index b6d42497..c590ffa4 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ setup( name="ptpython", author="Jonathan Slenders", - version="3.0.3", + version="3.0.4", url="https://github.com/prompt-toolkit/ptpython", description="Python REPL build on top of prompt_toolkit", long_description=long_description, From c05c4a6cbb2c4f70e224cbfd854546ba0367e56e Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Mon, 10 Aug 2020 12:11:42 +0200 Subject: [PATCH 294/470] Added ssh-and-telnet-embed.py example. Thanks to Vincent Michel. --- examples/ssh-and-telnet-embed.py | 50 ++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100755 examples/ssh-and-telnet-embed.py diff --git a/examples/ssh-and-telnet-embed.py b/examples/ssh-and-telnet-embed.py new file mode 100755 index 00000000..541b885c --- /dev/null +++ b/examples/ssh-and-telnet-embed.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python +""" +Serve a ptpython console using both telnet and ssh. + +Thanks to Vincent Michel for this! +https://gist.github.com/vxgmichel/7685685b3e5ead04ada4a3ba75a48eef +""" + +import pathlib +import asyncio + +import asyncssh + +from ptpython.repl import embed + +from prompt_toolkit import print_formatted_text +from prompt_toolkit.contrib.telnet.server import TelnetServer +from prompt_toolkit.contrib.ssh.server import PromptToolkitSSHServer + + +def ensure_key(filename="ssh_host_key"): + path = pathlib.Path(filename) + if not path.exists(): + rsa_key = asyncssh.generate_private_key("ssh-rsa") + path.write_bytes(rsa_key.export_private_key()) + return str(path) + + +async def interact(connection=None): + global_dict = {**globals(), "print": print_formatted_text} + await embed(return_asyncio_coroutine=True, globals=global_dict) + + +async def main(ssh_port=8022, telnet_port=8023): + ssh_server = PromptToolkitSSHServer(interact=interact) + await asyncssh.create_server( + lambda: ssh_server, "", ssh_port, server_host_keys=[ensure_key()] + ) + print(f"Running ssh server on port {ssh_port}...") + + telnet_server = TelnetServer(interact=interact, port=telnet_port) + telnet_server.start() + print(f"Running telnet server on port {telnet_port}...") + + while True: + await asyncio.sleep(60) + + +if __name__ == "__main__": + asyncio.run(main()) From 85dab9a26ee84d02ecef20d295d4ace60364c31e Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Mon, 10 Aug 2020 12:12:45 +0200 Subject: [PATCH 295/470] Removed old 'from __future__ import unicode_literals' statements. --- examples/asyncio-python-embed.py | 2 -- examples/python-embed-with-custom-prompt.py | 2 -- examples/python-embed.py | 2 -- examples/python-input.py | 2 -- 4 files changed, 8 deletions(-) diff --git a/examples/asyncio-python-embed.py b/examples/asyncio-python-embed.py index 3b796b2a..4dbbbcdd 100755 --- a/examples/asyncio-python-embed.py +++ b/examples/asyncio-python-embed.py @@ -11,8 +11,6 @@ to stdout, it won't break the input line, but instead writes nicely above the prompt. """ -from __future__ import unicode_literals - import asyncio from ptpython.repl import embed diff --git a/examples/python-embed-with-custom-prompt.py b/examples/python-embed-with-custom-prompt.py index 05417282..f9f68cc2 100755 --- a/examples/python-embed-with-custom-prompt.py +++ b/examples/python-embed-with-custom-prompt.py @@ -2,8 +2,6 @@ """ Example of embedding a Python REPL, and setting a custom prompt. """ -from __future__ import unicode_literals - from prompt_toolkit.formatted_text import HTML from pygments.token import Token diff --git a/examples/python-embed.py b/examples/python-embed.py index af24456e..ac2cd06f 100755 --- a/examples/python-embed.py +++ b/examples/python-embed.py @@ -1,8 +1,6 @@ #!/usr/bin/env python """ """ -from __future__ import unicode_literals - from ptpython.repl import embed diff --git a/examples/python-input.py b/examples/python-input.py index 1956070d..567c2ee6 100755 --- a/examples/python-input.py +++ b/examples/python-input.py @@ -1,8 +1,6 @@ #!/usr/bin/env python """ """ -from __future__ import unicode_literals - from ptpython.python_input import PythonInput From 44397e82eeb07dda298fdbf5c25f693e3dc8176b Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Mon, 10 Aug 2020 12:17:35 +0200 Subject: [PATCH 296/470] Fix in dictionary completion. Handle bug when numeric keys are used. --- ptpython/completer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ptpython/completer.py b/ptpython/completer.py index 1ff7bcc8..9f36aab3 100644 --- a/ptpython/completer.py +++ b/ptpython/completer.py @@ -388,7 +388,7 @@ def _get_item_lookup_completions( break for k in result: - if str(k).startswith(key_obj): + if str(k).startswith(str(key_obj)): try: k_repr = self._do_repr(k) yield Completion( From 8f7b8e1ff1f8d7f92b7c9e7f9a492312fdc2df4d Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Mon, 10 Aug 2020 12:19:42 +0200 Subject: [PATCH 297/470] Release 3.0.5 --- CHANGELOG | 7 +++++++ setup.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 9615da16..d6220bda 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,13 @@ CHANGELOG ========= +3.0.5: 2020-08-10 +----------------- + +Fixes: +- Handle bug in dictionary completion when numeric keys are used. + + 3.0.4: 2020-08-10 ----------------- diff --git a/setup.py b/setup.py index c590ffa4..e2bf89ba 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ setup( name="ptpython", author="Jonathan Slenders", - version="3.0.4", + version="3.0.5", url="https://github.com/prompt-toolkit/ptpython", description="Python REPL build on top of prompt_toolkit", long_description=long_description, From 1c95fd72f3e92835962aaa6be8caaeafc5768f9d Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Wed, 23 Sep 2020 19:33:32 +0200 Subject: [PATCH 298/470] Added support for __pt_repr__ methods. For objects that expose this method, we'll use that for printing the result. Further, if __repr__ doesn't return a valid Python string, we won't apply syntax highlighting, because it's often wrong. --- README.rst | 18 ++++++++ ptpython/repl.py | 103 +++++++++++++++++++++++++++------------------- ptpython/utils.py | 23 ++++++++++- 3 files changed, 101 insertions(+), 43 deletions(-) diff --git a/README.rst b/README.rst index aa0c8eaa..ef8f569b 100644 --- a/README.rst +++ b/README.rst @@ -50,6 +50,24 @@ Features [2] If the terminal supports it (most terminals do), this allows pasting without going into paste mode. It will keep the indentation. +__pt_repr__: A nicer repr with colors +************************************* + +When classes implement a ``__pt_repr__`` method, this will be used instead of +``__repr__`` for printing. Any `prompt_toolkit "formatted text" +`_ +can be returned from here. In order to avoid writing a ``__repr__`` as well, +the ``ptpython.utils.ptrepr_to_repr`` decorator can be applied. For instance: + +.. code:: python + + from ptpython.utils import ptrepr_to_repr + from prompt_toolkit.formatted_text import HTML + + @ptrepr_to_repr + class MyClass: + def __pt_repr__(self): + return HTML('Hello world!') More screenshots **************** diff --git a/ptpython/repl.py b/ptpython/repl.py index ba95a3d5..9be7d05e 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -19,11 +19,16 @@ from prompt_toolkit.formatted_text import ( FormattedText, PygmentsTokens, + StyleAndTextTuples, fragment_list_width, merge_formatted_text, to_formatted_text, ) -from prompt_toolkit.formatted_text.utils import fragment_list_width +from prompt_toolkit.formatted_text.utils import ( + fragment_list_to_text, + fragment_list_width, + split_lines, +) from prompt_toolkit.key_binding.vi_state import InputMode from prompt_toolkit.patch_stdout import patch_stdout as patch_stdout_context from prompt_toolkit.shortcuts import clear_title, print_formatted_text, set_title @@ -128,8 +133,6 @@ def _execute(self, line: str) -> None: """ Evaluate the line and print the result. """ - output = self.app.output - # WORKAROUND: Due to a bug in Jedi, the current directory is removed # from sys.path. See: https://github.com/davidhalter/jedi/issues/1148 if "" not in sys.path: @@ -167,50 +170,66 @@ def compile_with_flags(code: str, mode: str): locals["_"] = locals["_%i" % self.current_statement_index] = result if result is not None: - out_prompt = to_formatted_text(self.get_output_prompt()) - - try: - result_str = "%r\n" % (result,) - except UnicodeDecodeError: - # In Python 2: `__repr__` should return a bytestring, - # so to put it in a unicode context could raise an - # exception that the 'ascii' codec can't decode certain - # characters. Decode as utf-8 in that case. - result_str = "%s\n" % repr(result).decode( # type: ignore - "utf-8" - ) - - # Align every line to the first one. - line_sep = "\n" + " " * fragment_list_width(out_prompt) - result_str = line_sep.join(result_str.splitlines()) + "\n" - - # Write output tokens. - if self.enable_syntax_highlighting: - formatted_output = merge_formatted_text( - [ - out_prompt, - PygmentsTokens(list(_lex_python_result(result_str))), - ] - ) - else: - formatted_output = FormattedText( - out_prompt + [("", result_str)] - ) - - print_formatted_text( - formatted_output, - style=self._current_style, - style_transformation=self.style_transformation, - include_default_pygments_style=False, - output=output, - ) - + self.show_result(result) # If not a valid `eval` expression, run using `exec` instead. except SyntaxError: code = compile_with_flags(line, "exec") exec(code, self.get_globals(), self.get_locals()) - output.flush() + def show_result(self, result: object) -> None: + """ + Show __repr__ for an `eval` result. + """ + out_prompt = to_formatted_text(self.get_output_prompt()) + result_repr = to_formatted_text("%r\n" % (result,)) + + # If __pt_repr__ is present, take this. This can return + # prompt_toolkit formatted text. + if hasattr(result, "__pt_repr__"): + try: + result_repr = to_formatted_text(getattr(result, "__pt_repr__")()) + if isinstance(result_repr, list): + result_repr = FormattedText(result_repr) + except: + pass + + # If we have a string so far, and it's valid Python code, + # use the Pygments lexer. + if isinstance(result, str): + try: + compile(result, "", "eval") + except SyntaxError: + pass + else: + result = PygmentsTokens(list(_lex_python_result(result))) + + # Align every line to the prompt. + line_sep = "\n" + " " * fragment_list_width(out_prompt) + indented_repr: StyleAndTextTuples = [] + + for fragment in split_lines(result_repr): + indented_repr.extend(fragment) + indented_repr.append(("", line_sep)) + if indented_repr: + indented_repr.pop() + indented_repr.append(("", "\n")) + + # Write output tokens. + if self.enable_syntax_highlighting: + formatted_output = merge_formatted_text([out_prompt, indented_repr]) + else: + formatted_output = FormattedText( + out_prompt + [("", fragment_list_to_text(result_repr))] + ) + + print_formatted_text( + formatted_output, + style=self._current_style, + style_transformation=self.style_transformation, + include_default_pygments_style=False, + output=self.app.output, + ) + self.app.output.flush() def _handle_exception(self, e: Exception) -> None: output = self.app.output diff --git a/ptpython/utils.py b/ptpython/utils.py index 130da34f..1642914e 100644 --- a/ptpython/utils.py +++ b/ptpython/utils.py @@ -2,8 +2,10 @@ For internal use only. """ import re -from typing import Callable, TypeVar, cast +from typing import Callable, Type, TypeVar, cast +from prompt_toolkit.formatted_text import to_formatted_text +from prompt_toolkit.formatted_text.utils import fragment_list_to_text from prompt_toolkit.mouse_events import MouseEvent, MouseEventType __all__ = [ @@ -139,3 +141,22 @@ def handle_if_mouse_down(mouse_event: MouseEvent): return NotImplemented return cast(_T, handle_if_mouse_down) + + +_T_type = TypeVar("_T_type", bound=Type) + + +def ptrepr_to_repr(cls: _T_type) -> _T_type: + """ + Generate a normal `__repr__` method for classes that have a `__pt_repr__`. + """ + if not hasattr(cls, "__pt_repr__"): + raise TypeError( + "@ptrepr_to_repr can only be applied to classes that have a `__pt_repr__` method." + ) + + def __repr__(self) -> str: + return fragment_list_to_text(to_formatted_text(cls.__pt_repr__(self))) + + cls.__repr__ = __repr__ # type:ignore + return cls From 4a81398d20fceccf04d439d679ea3fd625aed598 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Wed, 23 Sep 2020 19:35:00 +0200 Subject: [PATCH 299/470] Some changes because of a new Black release. --- examples/ssh-and-telnet-embed.py | 9 ++++----- ptpython/completer.py | 4 +--- ptpython/history_browser.py | 20 ++++++++++++-------- ptpython/key_bindings.py | 4 ++-- ptpython/python_input.py | 12 +++++++++--- ptpython/repl.py | 4 +++- 6 files changed, 31 insertions(+), 22 deletions(-) diff --git a/examples/ssh-and-telnet-embed.py b/examples/ssh-and-telnet-embed.py index 541b885c..378784ce 100755 --- a/examples/ssh-and-telnet-embed.py +++ b/examples/ssh-and-telnet-embed.py @@ -6,16 +6,15 @@ https://gist.github.com/vxgmichel/7685685b3e5ead04ada4a3ba75a48eef """ -import pathlib import asyncio +import pathlib import asyncssh - -from ptpython.repl import embed - from prompt_toolkit import print_formatted_text -from prompt_toolkit.contrib.telnet.server import TelnetServer from prompt_toolkit.contrib.ssh.server import PromptToolkitSSHServer +from prompt_toolkit.contrib.telnet.server import TelnetServer + +from ptpython.repl import embed def ensure_key(filename="ssh_host_key"): diff --git a/ptpython/completer.py b/ptpython/completer.py index 9f36aab3..9912d743 100644 --- a/ptpython/completer.py +++ b/ptpython/completer.py @@ -436,9 +436,7 @@ def _get_attribute_completions( for name in names: if name.startswith(attr_name): - yield Completion( - name, -len(attr_name), - ) + yield Completion(name, -len(attr_name)) def _sort_attribute_names(self, names: List[str]) -> List[str]: """ diff --git a/ptpython/history_browser.py b/ptpython/history_browser.py index 6d8ede43..798a280f 100644 --- a/ptpython/history_browser.py +++ b/ptpython/history_browser.py @@ -475,8 +475,8 @@ def _(event): sorted(history_mapping.selected_lines).index(line_no) + history_mapping.result_line_offset ) - default_buffer.cursor_position = default_buffer.document.translate_row_col_to_index( - default_lineno, 0 + default_buffer.cursor_position = ( + default_buffer.document.translate_row_col_to_index(default_lineno, 0) ) # Also move the cursor to the next line. (This way they can hold @@ -606,8 +606,8 @@ def __init__(self, python_input, original_document): ) def _default_buffer_pos_changed(self, _): - """ When the cursor changes in the default buffer. Synchronize with - history buffer. """ + """When the cursor changes in the default buffer. Synchronize with + history buffer.""" # Only when this buffer has the focus. if self.app.current_buffer == self.default_buffer: try: @@ -623,8 +623,10 @@ def _default_buffer_pos_changed(self, _): except IndexError: pass else: - self.history_buffer.cursor_position = self.history_buffer.document.translate_row_col_to_index( - history_lineno, 0 + self.history_buffer.cursor_position = ( + self.history_buffer.document.translate_row_col_to_index( + history_lineno, 0 + ) ) def _history_buffer_pos_changed(self, _): @@ -639,6 +641,8 @@ def _history_buffer_pos_changed(self, _): + self.history_mapping.result_line_offset ) - self.default_buffer.cursor_position = self.default_buffer.document.translate_row_col_to_index( - default_lineno, 0 + self.default_buffer.cursor_position = ( + self.default_buffer.document.translate_row_col_to_index( + default_lineno, 0 + ) ) diff --git a/ptpython/key_bindings.py b/ptpython/key_bindings.py index d5171cc9..b01762e6 100644 --- a/ptpython/key_bindings.py +++ b/ptpython/key_bindings.py @@ -149,8 +149,8 @@ def _(event): empty_lines_required = python_input.accept_input_on_enter or 10000 def at_the_end(b): - """ we consider the cursor at the end when there is no text after - the cursor, or only whitespace. """ + """we consider the cursor at the end when there is no text after + the cursor, or only whitespace.""" text = b.document.text_after_cursor return text == "" or (text.isspace() and not "\n" in text) diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 18b9ef69..5447d198 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -209,15 +209,21 @@ def __init__( self.show_signature: bool = False self.show_docstring: bool = False self.show_meta_enter_message: bool = True - self.completion_visualisation: CompletionVisualisation = CompletionVisualisation.MULTI_COLUMN + self.completion_visualisation: CompletionVisualisation = ( + CompletionVisualisation.MULTI_COLUMN + ) self.completion_menu_scroll_offset: int = 1 self.show_line_numbers: bool = False self.show_status_bar: bool = True self.wrap_lines: bool = True self.complete_while_typing: bool = True - self.paste_mode: bool = False # When True, don't insert whitespace after newline. - self.confirm_exit: bool = True # Ask for confirmation when Control-D is pressed. + self.paste_mode: bool = ( + False # When True, don't insert whitespace after newline. + ) + self.confirm_exit: bool = ( + True # Ask for confirmation when Control-D is pressed. + ) self.accept_input_on_enter: int = 2 # Accept when pressing Enter 'n' times. # 'None' means that meta-enter is always required. self.enable_open_in_editor: bool = True diff --git a/ptpython/repl.py b/ptpython/repl.py index 9be7d05e..27d2c60b 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -397,7 +397,9 @@ def get_locals(): configure(repl) # Start repl. - patch_context: ContextManager = patch_stdout_context() if patch_stdout else DummyContext() + patch_context: ContextManager = ( + patch_stdout_context() if patch_stdout else DummyContext() + ) if return_asyncio_coroutine: From 7425ce32197a08b4c897f5316644396e9dbc9996 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Wed, 23 Sep 2020 19:38:24 +0200 Subject: [PATCH 300/470] Added py.typed file. --- ptpython/py.typed | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 ptpython/py.typed diff --git a/ptpython/py.typed b/ptpython/py.typed new file mode 100644 index 00000000..e69de29b From f9d72f08a754042d8943f381b529f4fb0764adec Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Wed, 23 Sep 2020 19:54:21 +0200 Subject: [PATCH 301/470] Release 3.0.6 --- CHANGELOG | 11 +++++++++++ setup.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index d6220bda..a1c5c1e5 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,17 @@ CHANGELOG ========= +3.0.6: 2020-09-23 +----------------- + +New features: +- (Experimental) support for `__pt_repr__` methods. If objects implement this + method, this will be used to print the result in the REPL instead of the + normal `__repr__`. +- Added py.typed file, to enable type checking for applications that are + embedding ptpython. + + 3.0.5: 2020-08-10 ----------------- diff --git a/setup.py b/setup.py index e2bf89ba..10a70f12 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ setup( name="ptpython", author="Jonathan Slenders", - version="3.0.5", + version="3.0.6", url="https://github.com/prompt-toolkit/ptpython", description="Python REPL build on top of prompt_toolkit", long_description=long_description, From 0f57868a8a7b3b12a26ca586e7a104c63e0e03b1 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Wed, 23 Sep 2020 20:25:59 +0200 Subject: [PATCH 302/470] Run readme_renderer in Travis. --- .travis.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 7061cb5d..e622b352 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,7 @@ matrix: - python: 3.7 install: - - travis_retry pip install . pytest isort black mypy + - travis_retry pip install . pytest isort black mypy readme_renderer - pip list script: @@ -21,3 +21,6 @@ script: # Type checking - mypy ptpython + + # Ensure that the README renders correctly (required for uploading to PyPI). + - python -m readme_renderer README.rst > /dev/null From 54849cb9c30c66e4f15ca8d69867545e8c1a048c Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Fri, 25 Sep 2020 16:27:13 +0200 Subject: [PATCH 303/470] Added 'insert_blank_line_after_input' configuration option and fixed a few __pt_repr__ formatting issues. --- ptpython/python_input.py | 6 +++++ ptpython/repl.py | 47 +++++++++++++++++++++++----------------- 2 files changed, 33 insertions(+), 20 deletions(-) diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 5447d198..5c08c1b4 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -254,6 +254,7 @@ def __init__( self.exit_message: str = "Do you really want to exit?" self.insert_blank_line_after_output: bool = True # (For the REPL.) + self.insert_blank_line_after_input: bool = False # (For the REPL.) # The buffers. self.default_buffer = self._create_buffer() @@ -640,6 +641,11 @@ def get_values(): for s in self.all_prompt_styles ), ), + simple_option( + title="Blank line after input", + description="Insert a blank line after the input.", + field_name="insert_blank_line_after_input", + ), simple_option( title="Blank line after output", description="Insert a blank line after the output.", diff --git a/ptpython/repl.py b/ptpython/repl.py index 27d2c60b..95b1004e 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -115,6 +115,9 @@ def pre_run( def _process_text(self, line: str) -> None: if line and not line.isspace(): + if self.insert_blank_line_after_input: + self.app.output.write("\n") + try: # Eval and print. self._execute(line) @@ -181,45 +184,49 @@ def show_result(self, result: object) -> None: Show __repr__ for an `eval` result. """ out_prompt = to_formatted_text(self.get_output_prompt()) - result_repr = to_formatted_text("%r\n" % (result,)) + + # If the repr is valid Python code, use the Pygments lexer. + result_repr = repr(result) + try: + compile(result_repr, "", "eval") + except SyntaxError: + formatted_result_repr = to_formatted_text(result_repr) + else: + formatted_result_repr = to_formatted_text( + PygmentsTokens(list(_lex_python_result(result_repr))) + ) # If __pt_repr__ is present, take this. This can return # prompt_toolkit formatted text. if hasattr(result, "__pt_repr__"): try: - result_repr = to_formatted_text(getattr(result, "__pt_repr__")()) - if isinstance(result_repr, list): - result_repr = FormattedText(result_repr) + formatted_result_repr = to_formatted_text( + getattr(result, "__pt_repr__")() + ) + if isinstance(formatted_result_repr, list): + formatted_result_repr = FormattedText(formatted_result_repr) except: pass - # If we have a string so far, and it's valid Python code, - # use the Pygments lexer. - if isinstance(result, str): - try: - compile(result, "", "eval") - except SyntaxError: - pass - else: - result = PygmentsTokens(list(_lex_python_result(result))) - # Align every line to the prompt. line_sep = "\n" + " " * fragment_list_width(out_prompt) indented_repr: StyleAndTextTuples = [] - for fragment in split_lines(result_repr): + lines = list(split_lines(formatted_result_repr)) + + for i, fragment in enumerate(lines): indented_repr.extend(fragment) - indented_repr.append(("", line_sep)) - if indented_repr: - indented_repr.pop() - indented_repr.append(("", "\n")) + + # Add indentation separator between lines, not after the last line. + if i != len(lines) - 1: + indented_repr.append(("", line_sep)) # Write output tokens. if self.enable_syntax_highlighting: formatted_output = merge_formatted_text([out_prompt, indented_repr]) else: formatted_output = FormattedText( - out_prompt + [("", fragment_list_to_text(result_repr))] + out_prompt + [("", fragment_list_to_text(formatted_result_repr))] ) print_formatted_text( From 9f7819ea4a0df5d7da633a63f1e387c218d216a0 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Fri, 25 Sep 2020 16:25:39 +0200 Subject: [PATCH 304/470] Abbreviate completian meta information for dictionary completer if multiline or too long. --- ptpython/completer.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/ptpython/completer.py b/ptpython/completer.py index 9912d743..e4b43fc0 100644 --- a/ptpython/completer.py +++ b/ptpython/completer.py @@ -368,6 +368,16 @@ def _get_item_lookup_completions( """ Complete dictionary keys. """ + + def abbr_meta(text: str) -> str: + " Abbreviate meta text, make sure it fits on one line. " + # Take first line, if multiple lines. + if len(text) > 20: + text = text[:20] + "..." + if "\n" in text: + text = text.split("\n", 1)[0] + "..." + return text + match = self.item_lookup_pattern.search(document.text_before_cursor) if match is not None: object_var, key = match.groups() @@ -395,7 +405,7 @@ def _get_item_lookup_completions( k_repr + "]", -len(key), display=f"[{k_repr}]", - display_meta=self._do_repr(result[k]), + display_meta=abbr_meta(self._do_repr(result[k])), ) except ReprFailedError: pass @@ -411,7 +421,7 @@ def _get_item_lookup_completions( k_repr + "]", -len(key), display=f"[{k_repr}]", - display_meta=self._do_repr(result[k]), + display_meta=abbr_meta(self._do_repr(result[k])), ) except ReprFailedError: pass From a395a25f3307c7ece9c1fffe7c833f04556648b2 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Fri, 25 Sep 2020 17:16:42 +0200 Subject: [PATCH 305/470] Added option for hiding/showing private completions. --- ptpython/completer.py | 79 ++++++++++++++++++++++++++++++++++------ ptpython/layout.py | 5 ++- ptpython/python_input.py | 46 +++++++++++++++++++---- 3 files changed, 111 insertions(+), 19 deletions(-) diff --git a/ptpython/completer.py b/ptpython/completer.py index e4b43fc0..535d2e2e 100644 --- a/ptpython/completer.py +++ b/ptpython/completer.py @@ -1,7 +1,8 @@ import ast import keyword import re -from typing import TYPE_CHECKING, Any, Dict, Iterable, List +from enum import Enum +from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, List, Optional from prompt_toolkit.completion import ( CompleteEvent, @@ -12,13 +13,24 @@ from prompt_toolkit.contrib.regular_languages.compiler import compile as compile_grammar from prompt_toolkit.contrib.regular_languages.completion import GrammarCompleter from prompt_toolkit.document import Document +from prompt_toolkit.formatted_text import fragment_list_to_text, to_formatted_text from ptpython.utils import get_jedi_script_from_document if TYPE_CHECKING: from prompt_toolkit.contrib.regular_languages.compiler import _CompiledGrammar -__all__ = ["PythonCompleter"] +__all__ = ["PythonCompleter", "CompletePrivateAttributes", "HidePrivateCompleter"] + + +class CompletePrivateAttributes(Enum): + """ + Should we display private attributes in the completion pop-up? + """ + + NEVER = "NEVER" + IF_NO_PUBLIC = "IF_NO_PUBLIC" + ALWAYS = "ALWAYS" class PythonCompleter(Completer): @@ -26,7 +38,9 @@ class PythonCompleter(Completer): Completer for Python code. """ - def __init__(self, get_globals, get_locals, get_enable_dictionary_completion): + def __init__( + self, get_globals, get_locals, get_enable_dictionary_completion + ) -> None: super().__init__() self.get_globals = get_globals @@ -35,8 +49,8 @@ def __init__(self, get_globals, get_locals, get_enable_dictionary_completion): self.dictionary_completer = DictionaryCompleter(get_globals, get_locals) - self._path_completer_cache = None - self._path_completer_grammar_cache = None + self._path_completer_cache: Optional[GrammarCompleter] = None + self._path_completer_grammar_cache: Optional["_CompiledGrammar"] = None @property def _path_completer(self) -> GrammarCompleter: @@ -158,7 +172,7 @@ def get_completions( if script: try: - completions = script.completions() + jedi_completions = script.completions() except TypeError: # Issue #9: bad syntax causes completions() to fail in jedi. # https://github.com/jonathanslenders/python-prompt-toolkit/issues/9 @@ -196,12 +210,12 @@ def get_completions( # Supress all other Jedi exceptions. pass else: - for c in completions: + for jc in jedi_completions: yield Completion( - c.name_with_symbols, - len(c.complete) - len(c.name_with_symbols), - display=c.name_with_symbols, - style=_get_style_for_name(c.name_with_symbols), + jc.name_with_symbols, + len(jc.complete) - len(jc.name_with_symbols), + display=jc.name_with_symbols, + style=_get_style_for_name(jc.name_with_symbols), ) @@ -464,6 +478,49 @@ def sort_key(name: str): return sorted(names, key=sort_key) +class HidePrivateCompleter(Completer): + """ + Wrapper around completer that hides private fields, deponding on whether or + not public fields are shown. + + (The reason this is implemented as a `Completer` wrapper is because this + way it works also with `FuzzyCompleter`.) + """ + + def __init__( + self, + completer: Completer, + complete_private_attributes: Callable[[], CompletePrivateAttributes], + ) -> None: + self.completer = completer + self.complete_private_attributes = complete_private_attributes + + def get_completions( + self, document: Document, complete_event: CompleteEvent + ) -> Iterable[Completion]: + + completions = list(self.completer.get_completions(document, complete_event)) + complete_private_attributes = self.complete_private_attributes() + hide_private = False + + def is_private(completion: Completion) -> bool: + text = fragment_list_to_text(to_formatted_text(completion.display)) + return text.startswith("_") + + if complete_private_attributes == CompletePrivateAttributes.NEVER: + hide_private = True + + elif complete_private_attributes == CompletePrivateAttributes.IF_NO_PUBLIC: + hide_private = any(not is_private(completion) for completion in completions) + + if hide_private: + completions = [ + completion for completion in completions if not is_private(completion) + ] + + return completions + + class ReprFailedError(Exception): " Raised when the repr() call in `DictionaryCompleter` fails. " diff --git a/ptpython/layout.py b/ptpython/layout.py index d50a3a53..b06b95d3 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -213,7 +213,10 @@ def get_help_text(): return ConditionalContainer( content=Window( - FormattedTextControl(get_help_text), style=token, height=Dimension(min=3) + FormattedTextControl(get_help_text), + style=token, + height=Dimension(min=3), + wrap_lines=True, ), filter=ShowSidebar(python_input) & Condition(lambda: python_input.show_sidebar_help) diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 5c08c1b4..c119e391 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -51,7 +51,7 @@ from prompt_toolkit.validation import ConditionalValidator, Validator from pygments.lexers import Python3Lexer as PythonLexer -from .completer import PythonCompleter +from .completer import CompletePrivateAttributes, HidePrivateCompleter, PythonCompleter from .history_browser import PythonHistory from .key_bindings import ( load_confirm_exit_bindings, @@ -180,13 +180,17 @@ def __init__( self.get_globals: _GetNamespace = get_globals or (lambda: {}) self.get_locals: _GetNamespace = get_locals or self.get_globals - self._completer = _completer or FuzzyCompleter( - PythonCompleter( - self.get_globals, - self.get_locals, - lambda: self.enable_dictionary_completion, + self._completer = HidePrivateCompleter( + _completer + or FuzzyCompleter( + PythonCompleter( + self.get_globals, + self.get_locals, + lambda: self.enable_dictionary_completion, + ), + enable_fuzzy=Condition(lambda: self.enable_fuzzy_completion), ), - enable_fuzzy=Condition(lambda: self.enable_fuzzy_completion), + lambda: self.complete_private_attributes, ) self._validator = _validator or PythonValidator(self.get_compiler_flags) self._lexer = _lexer or PygmentsLexer(PythonLexer) @@ -239,6 +243,9 @@ def __init__( self.enable_syntax_highlighting: bool = True self.enable_fuzzy_completion: bool = False self.enable_dictionary_completion: bool = False + self.complete_private_attributes: CompletePrivateAttributes = ( + CompletePrivateAttributes.ALWAYS + ) self.swap_light_and_dark: bool = False self.highlight_matching_parenthesis: bool = False self.show_sidebar: bool = False # Currently show the sidebar. @@ -530,6 +537,31 @@ def get_values(): "off": lambda: disable("complete_while_typing"), }, ), + Option( + title="Complete private attrs", + description="Show or hide private attributes in the completions. " + "'If no public' means: show private attributes only if no public " + "matches are found or if an underscore was typed.", + get_current_value=lambda: { + CompletePrivateAttributes.NEVER: "Never", + CompletePrivateAttributes.ALWAYS: "Always", + CompletePrivateAttributes.IF_NO_PUBLIC: "If no public", + }[self.complete_private_attributes], + get_values=lambda: { + "Never": lambda: enable( + "complete_private_attributes", + CompletePrivateAttributes.NEVER, + ), + "Always": lambda: enable( + "complete_private_attributes", + CompletePrivateAttributes.ALWAYS, + ), + "If no public": lambda: enable( + "complete_private_attributes", + CompletePrivateAttributes.IF_NO_PUBLIC, + ), + }, + ), Option( title="Enable fuzzy completion", description="Enable fuzzy completion.", From 86e1571f8fda623cd49aa42841d7303c94fb95f7 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Fri, 25 Sep 2020 17:58:28 +0200 Subject: [PATCH 306/470] Release 3.0.7 --- CHANGELOG | 13 +++++++++++++ setup.py | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index a1c5c1e5..b37222d5 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,19 @@ CHANGELOG ========= +3.0.7: 2020-09-25 +----------------- + +New features: +- Option to show/hide private attributes during a completion +- Added `insert_blank_line_after_input` option similar to + `insert_blank_line_after_output`. + +Fixes: +- Fixed some formatting issues of `__pt_repr__`. +- Abbreviate completion meta information for dictionary completer if needed. + + 3.0.6: 2020-09-23 ----------------- diff --git a/setup.py b/setup.py index 10a70f12..9b71711e 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ setup( name="ptpython", author="Jonathan Slenders", - version="3.0.6", + version="3.0.7", url="https://github.com/prompt-toolkit/ptpython", description="Python REPL build on top of prompt_toolkit", long_description=long_description, From 544a8f800d5fe4569330e67d39fb6cc74dde3f45 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Wed, 28 Oct 2020 18:21:44 +0100 Subject: [PATCH 307/470] Stop using deprecated Jedi functions. Use Script.get_signatures() instead of Script.call_signatures() to get Jedi signatures, and Script.complete() instead of Script.completions(). --- ptpython/completer.py | 5 ++++- ptpython/python_input.py | 2 +- ptpython/utils.py | 2 -- setup.py | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/ptpython/completer.py b/ptpython/completer.py index 535d2e2e..73900da6 100644 --- a/ptpython/completer.py +++ b/ptpython/completer.py @@ -172,7 +172,10 @@ def get_completions( if script: try: - jedi_completions = script.completions() + jedi_completions = script.complete( + column=document.cursor_position_col, + line=document.cursor_position_row + 1, + ) except TypeError: # Issue #9: bad syntax causes completions() to fail in jedi. # https://github.com/jonathanslenders/python-prompt-toolkit/issues/9 diff --git a/ptpython/python_input.py b/ptpython/python_input.py index c119e391..efe0bdd5 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -891,7 +891,7 @@ def run(): # Show signatures in help text. if script: try: - signatures = script.call_signatures() + signatures = script.get_signatures() except ValueError: # e.g. in case of an invalid \\x escape. signatures = [] diff --git a/ptpython/utils.py b/ptpython/utils.py index 1642914e..3658085a 100644 --- a/ptpython/utils.py +++ b/ptpython/utils.py @@ -52,8 +52,6 @@ def get_jedi_script_from_document(document, locals, globals): try: return jedi.Interpreter( document.text, - column=document.cursor_position_col, - line=document.cursor_position_row + 1, path="input-text", namespaces=[locals, globals], ) diff --git a/setup.py b/setup.py index 9b71711e..6d3e93fc 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ install_requires=[ "appdirs", "importlib_metadata;python_version<'3.8'", - "jedi>=0.9.0", + "jedi>=0.16.0", "prompt_toolkit>=3.0.0,<3.1.0", "pygments", ], From f91f19b3c8c4ef6eb29722da0b2c63e64a86eb2c Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 29 Oct 2020 15:23:13 +0100 Subject: [PATCH 308/470] Fix typing error: 'sorted' needs a sortable type (was a TypeVar without bound). --- ptpython/python_input.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/ptpython/python_input.py b/ptpython/python_input.py index efe0bdd5..16837db8 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -6,7 +6,7 @@ from asyncio import get_event_loop from functools import partial -from typing import Any, Callable, Dict, Generic, List, Optional, TypeVar +from typing import TYPE_CHECKING, Any, Callable, Dict, Generic, List, Optional, TypeVar from prompt_toolkit.application import Application, get_app from prompt_toolkit.auto_suggest import ( @@ -66,7 +66,18 @@ __all__ = ["PythonInput"] -_T = TypeVar("_T") + +if TYPE_CHECKING: + from typing_extensions import Protocol + + class _SupportsLessThan(Protocol): + # Taken from typeshed. _T is used by "sorted", which needs anything + # sortable. + def __lt__(self, __other: Any) -> bool: + ... + + +_T = TypeVar("_T", bound="_SupportsLessThan") class OptionCategory: From 626a1b5f621d1c47a0e367ac0f5fd1b0d5a7841c Mon Sep 17 00:00:00 2001 From: Mikhail Terekhov Date: Mon, 5 Oct 2020 11:53:23 -0400 Subject: [PATCH 309/470] Update asyncio-python-embed.py Fix deprecation warning in Python 3.8. --- examples/asyncio-python-embed.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/examples/asyncio-python-embed.py b/examples/asyncio-python-embed.py index 4dbbbcdd..e1075a22 100755 --- a/examples/asyncio-python-embed.py +++ b/examples/asyncio-python-embed.py @@ -19,19 +19,17 @@ counter = [0] -@asyncio.coroutine -def print_counter(): +async def print_counter(): """ Coroutine that prints counters and saves it in a global variable. """ while True: print("Counter: %i" % counter[0]) counter[0] += 1 - yield from asyncio.sleep(3) + await asyncio.sleep(3) -@asyncio.coroutine -def interactive_shell(): +async def interactive_shell(): """ Coroutine that starts a Python REPL from which we can access the global counter variable. @@ -40,7 +38,7 @@ def interactive_shell(): 'You should be able to read and update the "counter[0]" variable from this shell.' ) try: - yield from embed( + await embed( globals=globals(), return_asyncio_coroutine=True, patch_stdout=True ) except EOFError: From be38c35d480c9f9ebdb9c0fd978bd6971dc290d9 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 19 Nov 2020 17:50:49 +0100 Subject: [PATCH 310/470] Show completion suffixes (like '(' for functions). --- ptpython/completer.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/ptpython/completer.py b/ptpython/completer.py index 73900da6..8261c224 100644 --- a/ptpython/completer.py +++ b/ptpython/completer.py @@ -1,4 +1,5 @@ import ast +import inspect import keyword import re from enum import Enum @@ -214,10 +215,16 @@ def get_completions( pass else: for jc in jedi_completions: + if jc.type == "function": + suffix = "()" + else: + suffix = "" + yield Completion( jc.name_with_symbols, len(jc.complete) - len(jc.name_with_symbols), - display=jc.name_with_symbols, + display=jc.name_with_symbols + suffix, + display_meta=jc.type, style=_get_style_for_name(jc.name_with_symbols), ) @@ -461,9 +468,24 @@ def _get_attribute_completions( names = self._sort_attribute_names(dir(result)) + def get_suffix(name: str) -> str: + try: + obj = getattr(result, name, None) + if inspect.isfunction(obj): + return "()" + + if isinstance(obj, dict): + return "{}" + if isinstance(obj, (list, tuple)): + return "[]" + except: + pass + return "" + for name in names: if name.startswith(attr_name): - yield Completion(name, -len(attr_name)) + suffix = get_suffix(name) + yield Completion(name, -len(attr_name), display=name + suffix) def _sort_attribute_names(self, names: List[str]) -> List[str]: """ From 2d26324fc56ef380e397b63550104a70735155a4 Mon Sep 17 00:00:00 2001 From: Julian Berman Date: Sat, 21 Nov 2020 11:55:24 -0500 Subject: [PATCH 311/470] Regenerate the docstring / helpstring, which looks outdated. --- ptpython/entry_points/run_ptpython.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/ptpython/entry_points/run_ptpython.py b/ptpython/entry_points/run_ptpython.py index aeb5c26d..53e0289e 100644 --- a/ptpython/entry_points/run_ptpython.py +++ b/ptpython/entry_points/run_ptpython.py @@ -1,17 +1,19 @@ #!/usr/bin/env python """ ptpython: Interactive Python shell. -Usage: - ptpython [ --vi ] - [ --config-dir= ] [ --interactive= ] - [--] [ ... ] - ptpython -h | --help - -Options: - --vi : Use Vi keybindings instead of Emacs bindings. - --config-dir= : Pass config directory. By default '$XDG_CONFIG_HOME/ptpython'. - -i, --interactive= : Start interactive shell after executing this file. +positional arguments: + args Script and arguments + +optional arguments: + -h, --help show this help message and exit + --vi Enable Vi key bindings + -i, --interactive Start interactive shell after executing this file. + --config-file CONFIG_FILE + Location of configuration file. + --history-file HISTORY_FILE + Location of history file. + -V, --version show program's version number and exit Other environment variables: PYTHONSTARTUP: file executed on interactive startup (no default) """ From 703133915af1f9fdbb478cf0667fa93442a669dc Mon Sep 17 00:00:00 2001 From: Julian Berman Date: Sat, 21 Nov 2020 12:03:35 -0500 Subject: [PATCH 312/470] Add PTPYTHON_CONFIG_HOME for explicitly setting a config dir. In particular allows macOS users to follow the Linux convention instead of the macOS one, of putting config back in ~/.config. Closes: #346 --- README.rst | 7 ++++++- ptpython/entry_points/run_ptpython.py | 23 ++++++++++++++++++----- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/README.rst b/README.rst index ef8f569b..3d6b1d9c 100644 --- a/README.rst +++ b/README.rst @@ -149,7 +149,12 @@ navigation mode. Configuration ************* -It is possible to create a ``$XDG_CONFIG_HOME/ptpython/config.py`` file to customize the configuration. +It is possible to create a ``config.py`` file to customize configuration. +ptpython will look in an appropriate platform-specific directory via `appdirs +`. See the ``appdirs`` documentation for the +precise location for your platform. A ``PTPYTHON_CONFIG_HOME`` environment +variable, if set, can also be used to explicitly override where configuration +is looked for. Have a look at this example to see what is possible: `config.py `_ diff --git a/ptpython/entry_points/run_ptpython.py b/ptpython/entry_points/run_ptpython.py index 53e0289e..47407c37 100644 --- a/ptpython/entry_points/run_ptpython.py +++ b/ptpython/entry_points/run_ptpython.py @@ -14,13 +14,16 @@ --history-file HISTORY_FILE Location of history file. -V, --version show program's version number and exit -Other environment variables: -PYTHONSTARTUP: file executed on interactive startup (no default) + +environment variables: + PTPYTHON_CONFIG_HOME: a configuration directory to use + PYTHONSTARTUP: file executed on interactive startup (no default) """ import argparse import os import pathlib import sys +from textwrap import dedent from typing import Tuple try: @@ -40,8 +43,15 @@ class _Parser(argparse.ArgumentParser): def print_help(self): super().print_help() - print("Other environment variables:") - print("PYTHONSTARTUP: file executed on interactive startup (no default)") + print( + dedent( + """ + environment variables: + PTPYTHON_CONFIG_HOME: a configuration directory to use + PYTHONSTARTUP: file executed on interactive startup (no default) + """, + ).rstrip(), + ) def create_parser() -> _Parser: @@ -72,7 +82,10 @@ def get_config_and_history_file(namespace: argparse.Namespace) -> Tuple[str, str Check which config/history files to use, ensure that the directories for these files exist, and return the config and history path. """ - config_dir = appdirs.user_config_dir("ptpython", "prompt_toolkit") + config_dir = os.environ.get( + "PTPYTHON_CONFIG_HOME", + appdirs.user_config_dir("ptpython", "prompt_toolkit"), + ) data_dir = appdirs.user_data_dir("ptpython", "prompt_toolkit") # Create directories. From 0409350b77f898223182851a27e8d89bbc54f3b5 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Mon, 7 Dec 2020 17:08:27 +0100 Subject: [PATCH 313/470] Some cleanup to the config file. --- README.rst | 1 - examples/ptpython_config/config.py | 44 ++++++++++++++++-------------- 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/README.rst b/README.rst index 3d6b1d9c..0cf7f3c0 100644 --- a/README.rst +++ b/README.rst @@ -231,7 +231,6 @@ Special thanks to - `Pygments `_: Syntax highlighter. - `Jedi `_: Autocompletion library. -- `Docopt `_: Command-line interface description language. - `wcwidth `_: Determine columns needed for a wide characters. - `prompt_toolkit `_ for the interface. diff --git a/examples/ptpython_config/config.py b/examples/ptpython_config/config.py index 1a009018..8532f938 100644 --- a/examples/ptpython_config/config.py +++ b/examples/ptpython_config/config.py @@ -2,9 +2,8 @@ Configuration example for ``ptpython``. Copy this file to $XDG_CONFIG_HOME/ptpython/config.py +On Linux, this is: ~/.config/ptpython/config.py """ -from __future__ import unicode_literals - from prompt_toolkit.filters import ViInsertMode from prompt_toolkit.key_binding.key_processor import KeyPress from prompt_toolkit.keys import Keys @@ -12,7 +11,7 @@ from ptpython.layout import CompletionVisualisation -__all__ = ("configure",) +__all__ = ["configure"] def configure(repl): @@ -107,14 +106,19 @@ def configure(repl): repl.enable_input_validation = True # Use this colorscheme for the code. - repl.use_code_colorscheme("pastie") + repl.use_code_colorscheme("default") + # repl.use_code_colorscheme("pastie") # Set color depth (keep in mind that not all terminals support true color). - # repl.color_depth = 'DEPTH_1_BIT' # Monochrome. - # repl.color_depth = 'DEPTH_4_BIT' # ANSI colors only. + # repl.color_depth = "DEPTH_1_BIT" # Monochrome. + # repl.color_depth = "DEPTH_4_BIT" # ANSI colors only. repl.color_depth = "DEPTH_8_BIT" # The default, 256 colors. - # repl.color_depth = 'DEPTH_24_BIT' # True color. + # repl.color_depth = "DEPTH_24_BIT" # True color. + + # Min/max brightness + repl.min_brightness = 0.0 # Increase for dark terminal backgrounds. + repl.max_brightness = 1.0 # Decrease for light terminal backgrounds. # Syntax. repl.enable_syntax_highlighting = True @@ -127,22 +131,22 @@ def configure(repl): # Install custom colorscheme named 'my-colorscheme' and use it. """ - repl.install_ui_colorscheme('my-colorscheme', Style.from_dict(_custom_ui_colorscheme)) - repl.use_ui_colorscheme('my-colorscheme') + repl.install_ui_colorscheme("my-colorscheme", Style.from_dict(_custom_ui_colorscheme)) + repl.use_ui_colorscheme("my-colorscheme") """ # Add custom key binding for PDB. """ - @repl.add_key_binding(Keys.ControlB) + @repl.add_key_binding("c-b") def _(event): - ' Pressing Control-B will insert "pdb.set_trace()" ' - event.cli.current_buffer.insert_text('\nimport pdb; pdb.set_trace()\n') + " Pressing Control-B will insert "pdb.set_trace()" " + event.cli.current_buffer.insert_text("\nimport pdb; pdb.set_trace()\n") """ # Typing ControlE twice should also execute the current command. # (Alternative for Meta-Enter.) """ - @repl.add_key_binding(Keys.ControlE, Keys.ControlE) + @repl.add_key_binding("c-e", "c-e") def _(event): event.current_buffer.validate_and_handle() """ @@ -150,22 +154,22 @@ def _(event): # Typing 'jj' in Vi Insert mode, should send escape. (Go back to navigation # mode.) """ - @repl.add_key_binding('j', 'j', filter=ViInsertMode()) + @repl.add_key_binding("j", "j", filter=ViInsertMode()) def _(event): " Map 'jj' to Escape. " - event.cli.key_processor.feed(KeyPress(Keys.Escape)) + event.cli.key_processor.feed(KeyPress("escape")) """ # Custom key binding for some simple autocorrection while typing. """ corrections = { - 'impotr': 'import', - 'pritn': 'print', + "impotr": "import", + "pritn": "print", } - @repl.add_key_binding(' ') + @repl.add_key_binding(" ") def _(event): - ' When a space is pressed. Check & correct word before cursor. ' + " When a space is pressed. Check & correct word before cursor. " b = event.cli.current_buffer w = b.document.get_word_before_cursor() @@ -174,7 +178,7 @@ def _(event): b.delete_before_cursor(count=len(w)) b.insert_text(corrections[w]) - b.insert_text(' ') + b.insert_text(" ") """ # Add a custom title to the status bar. This is useful when ptpython is From 5da4e370da2e7664e11c77ec1f19a9d20d21cafd Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Mon, 7 Dec 2020 17:18:50 +0100 Subject: [PATCH 314/470] Fix dictionary completion on Pandas objects. This should fix the following error: File ".../ptpython/completer.py", line 373, in _get_expression_completions elif result: File ".../pandas/core/generic.py", line 1330, in __nonzero__ f"The truth value of a {type(self).__name__} is ambiguous. " --- ptpython/completer.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ptpython/completer.py b/ptpython/completer.py index 8261c224..a5bf2d2d 100644 --- a/ptpython/completer.py +++ b/ptpython/completer.py @@ -380,7 +380,9 @@ def _get_expression_completions( if isinstance(result, (list, tuple, dict)): yield Completion("[", 0) - elif result: + elif result is not None: + # Note: Don't call `if result` here. That can fail for types + # that have custom truthness checks. yield Completion(".", 0) def _get_item_lookup_completions( From ca041ea71b66578bb117a70d2ca8bf2e8026a6a0 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Mon, 7 Dec 2020 17:41:03 +0100 Subject: [PATCH 315/470] Added --light-bg and --dark-bg CLI flags. --- ptpython/entry_points/run_ptpython.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/ptpython/entry_points/run_ptpython.py b/ptpython/entry_points/run_ptpython.py index 47407c37..f23e69e2 100644 --- a/ptpython/entry_points/run_ptpython.py +++ b/ptpython/entry_points/run_ptpython.py @@ -9,6 +9,8 @@ -h, --help show this help message and exit --vi Enable Vi key bindings -i, --interactive Start interactive shell after executing this file. + --light-bg Run on a light background (use dark colors for text). + --dark-bg Run on a dark background (use light colors for text). --config-file CONFIG_FILE Location of configuration file. --history-file HISTORY_FILE @@ -63,6 +65,16 @@ def create_parser() -> _Parser: action="store_true", help="Start interactive shell after executing this file.", ) + parser.add_argument( + "--light-bg", + action="store_true", + help="Run on a light background (use dark colors for text).", + ), + parser.add_argument( + "--dark-bg", + action="store_true", + help="Run on a dark background (use light colors for text).", + ), parser.add_argument( "--config-file", type=str, help="Location of configuration file." ) @@ -83,8 +95,7 @@ def get_config_and_history_file(namespace: argparse.Namespace) -> Tuple[str, str these files exist, and return the config and history path. """ config_dir = os.environ.get( - "PTPYTHON_CONFIG_HOME", - appdirs.user_config_dir("ptpython", "prompt_toolkit"), + "PTPYTHON_CONFIG_HOME", appdirs.user_config_dir("ptpython", "prompt_toolkit"), ) data_dir = appdirs.user_data_dir("ptpython", "prompt_toolkit") @@ -178,6 +189,14 @@ def configure(repl) -> None: if os.path.exists(config_file): run_config(repl, config_file) + # Adjust colors if dark/light background flag has been given. + if a.light_bg: + repl.min_brightness = 0.0 + repl.max_brightness = 0.60 + elif a.dark_bg: + repl.min_brightness = 0.60 + repl.max_brightness = 1.0 + import __main__ embed( From 86497891634275b7771c24cd7172c04e9bb94a0e Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 17 Dec 2020 17:17:36 +0100 Subject: [PATCH 316/470] Added option for output formatting and pager for displaying big outputs. --- examples/asyncio-python-embed.py | 4 +- ptpython/entry_points/run_ptpython.py | 14 +-- ptpython/python_input.py | 15 +++ ptpython/repl.py | 147 ++++++++++++++++++++++---- setup.py | 1 + 5 files changed, 154 insertions(+), 27 deletions(-) diff --git a/examples/asyncio-python-embed.py b/examples/asyncio-python-embed.py index e1075a22..05f52f1d 100755 --- a/examples/asyncio-python-embed.py +++ b/examples/asyncio-python-embed.py @@ -38,9 +38,7 @@ async def interactive_shell(): 'You should be able to read and update the "counter[0]" variable from this shell.' ) try: - await embed( - globals=globals(), return_asyncio_coroutine=True, patch_stdout=True - ) + await embed(globals=globals(), return_asyncio_coroutine=True, patch_stdout=True) except EOFError: # Stop the loop when quitting the repl. (Ctrl-D press.) loop.stop() diff --git a/ptpython/entry_points/run_ptpython.py b/ptpython/entry_points/run_ptpython.py index f23e69e2..e1255905 100644 --- a/ptpython/entry_points/run_ptpython.py +++ b/ptpython/entry_points/run_ptpython.py @@ -28,17 +28,18 @@ from textwrap import dedent from typing import Tuple -try: - from importlib import metadata -except ImportError: - import importlib_metadata as metadata # type: ignore - import appdirs from prompt_toolkit.formatted_text import HTML from prompt_toolkit.shortcuts import print_formatted_text from ptpython.repl import embed, enable_deprecation_warnings, run_config +try: + from importlib import metadata +except ImportError: + import importlib_metadata as metadata # type: ignore + + __all__ = ["create_parser", "get_config_and_history_file", "run"] @@ -95,7 +96,8 @@ def get_config_and_history_file(namespace: argparse.Namespace) -> Tuple[str, str these files exist, and return the config and history path. """ config_dir = os.environ.get( - "PTPYTHON_CONFIG_HOME", appdirs.user_config_dir("ptpython", "prompt_toolkit"), + "PTPYTHON_CONFIG_HOME", + appdirs.user_config_dir("ptpython", "prompt_toolkit"), ) data_dir = appdirs.user_data_dir("ptpython", "prompt_toolkit") diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 16837db8..508c42d4 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -261,6 +261,10 @@ def __init__( self.highlight_matching_parenthesis: bool = False self.show_sidebar: bool = False # Currently show the sidebar. + # Pager. + self.enable_output_formatting: bool = False + self.use_pager_for_big_outputs: bool = False + # When the sidebar is visible, also show the help text. self.show_sidebar_help: bool = True @@ -735,6 +739,17 @@ def get_values(): description="Highlight matching parenthesis, when the cursor is on or right after one.", field_name="highlight_matching_parenthesis", ), + simple_option( + title="Reformat output (black)", + description="Reformat outputs using Black, if possible (experimental).", + field_name="enable_output_formatting", + ), + simple_option( + title="Pager for big outputs", + description="Use a pager for displaying outputs that don't " + "fit on the screen.", + field_name="use_pager_for_big_outputs", + ), ], ), OptionCategory( diff --git a/ptpython/repl.py b/ptpython/repl.py index 95b1004e..fe869384 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -15,8 +15,10 @@ import warnings from typing import Any, Callable, ContextManager, Dict, Optional +import black from prompt_toolkit.document import Document from prompt_toolkit.formatted_text import ( + HTML, FormattedText, PygmentsTokens, StyleAndTextTuples, @@ -24,19 +26,20 @@ merge_formatted_text, to_formatted_text, ) -from prompt_toolkit.formatted_text.utils import ( - fragment_list_to_text, - fragment_list_width, - split_lines, -) +from prompt_toolkit.formatted_text.utils import fragment_list_to_text, split_lines +from prompt_toolkit.key_binding import KeyBindings, KeyPressEvent from prompt_toolkit.key_binding.vi_state import InputMode from prompt_toolkit.patch_stdout import patch_stdout as patch_stdout_context -from prompt_toolkit.shortcuts import clear_title, print_formatted_text, set_title -from prompt_toolkit.utils import DummyContext +from prompt_toolkit.shortcuts import ( + PromptSession, + clear_title, + print_formatted_text, + set_title, +) +from prompt_toolkit.utils import DummyContext, get_cwidth from pygments.lexers import PythonLexer, PythonTracebackLexer from pygments.token import Token -from .eventloop import inputhook from .python_input import PythonInput __all__ = ["PythonRepl", "enable_deprecation_warnings", "run_config", "embed"] @@ -107,12 +110,12 @@ def pre_run( # Abort - try again. self.default_buffer.document = Document() else: - self._process_text(text) + await self._process_text(text) if self.terminal_title: clear_title() - def _process_text(self, line: str) -> None: + async def _process_text(self, line: str) -> None: if line and not line.isspace(): if self.insert_blank_line_after_input: @@ -120,7 +123,7 @@ def _process_text(self, line: str) -> None: try: # Eval and print. - self._execute(line) + await self._execute(line) except KeyboardInterrupt as e: # KeyboardInterrupt doesn't inherit from Exception. self._handle_keyboard_interrupt(e) except Exception as e: @@ -132,7 +135,7 @@ def _process_text(self, line: str) -> None: self.current_statement_index += 1 self.signatures = [] - def _execute(self, line: str) -> None: + async def _execute(self, line: str) -> None: """ Evaluate the line and print the result. """ @@ -173,13 +176,13 @@ def compile_with_flags(code: str, mode: str): locals["_"] = locals["_%i" % self.current_statement_index] = result if result is not None: - self.show_result(result) + await self.show_result(result) # If not a valid `eval` expression, run using `exec` instead. except SyntaxError: code = compile_with_flags(line, "exec") exec(code, self.get_globals(), self.get_locals()) - def show_result(self, result: object) -> None: + async def show_result(self, result: object) -> None: """ Show __repr__ for an `eval` result. """ @@ -192,12 +195,19 @@ def show_result(self, result: object) -> None: except SyntaxError: formatted_result_repr = to_formatted_text(result_repr) else: + # Syntactically correct. Format with black and syntax highlight. + if self.enable_output_formatting: + result_repr = black.format_str( + result_repr, + mode=black.FileMode(line_length=self.app.output.get_size().columns), + ) + formatted_result_repr = to_formatted_text( PygmentsTokens(list(_lex_python_result(result_repr))) ) - # If __pt_repr__ is present, take this. This can return - # prompt_toolkit formatted text. + # If __pt_repr__ is present, take this. This can return prompt_toolkit + # formatted text. if hasattr(result, "__pt_repr__"): try: formatted_result_repr = to_formatted_text( @@ -229,14 +239,81 @@ def show_result(self, result: object) -> None: out_prompt + [("", fragment_list_to_text(formatted_result_repr))] ) + if self.use_pager_for_big_outputs: + await self._print_paginated_formatted_text( + to_formatted_text(formatted_output) + ) + else: + self.print_formatted_text(to_formatted_text(formatted_output)) + + self.app.output.flush() + + def print_formatted_text(self, formatted_text: StyleAndTextTuples) -> None: print_formatted_text( - formatted_output, + FormattedText(formatted_text), style=self._current_style, style_transformation=self.style_transformation, include_default_pygments_style=False, output=self.app.output, ) - self.app.output.flush() + + async def _print_paginated_formatted_text( + self, formatted_text: StyleAndTextTuples + ) -> None: + """ + Print formatted text, using --MORE-- style pagination. + (Avoid filling up the terminal's scrollback buffer.) + """ + continue_prompt = create_continue_prompt() + size = self.app.output.get_size() + + # Page buffer. + rows_in_buffer = 0 + columns_in_buffer = 0 + page: StyleAndTextTuples = [] + + def flush_page() -> None: + nonlocal page, columns_in_buffer, rows_in_buffer + self.print_formatted_text(page) + page = [] + columns_in_buffer = 0 + rows_in_buffer = 0 + + # Loop over lines. Show --MORE-- prompt when page is filled. + for line in split_lines(formatted_text): + for style, text, *_ in line: + for c in text: + width = get_cwidth(c) + + # (Soft) wrap line if it doesn't fit. + if columns_in_buffer + width > size.columns: + # Show pager first if we get too many lines after + # wrapping. + if rows_in_buffer + 1 >= size.rows - 1: + flush_page() + do_continue = await continue_prompt.prompt_async() + if not do_continue: + print("...") + return + + rows_in_buffer += 1 + columns_in_buffer = 0 + + columns_in_buffer += width + page.append((style, c)) + + if rows_in_buffer + 1 >= size.rows - 1: + flush_page() + do_continue = await continue_prompt.prompt_async() + if not do_continue: + print("...") + return + else: + page.append(("", "\n")) + rows_in_buffer += 1 + columns_in_buffer = 0 + + flush_page() def _handle_exception(self, e: Exception) -> None: output = self.app.output @@ -418,3 +495,37 @@ async def coroutine(): else: with patch_context: repl.run() + + +def create_continue_prompt() -> PromptSession[bool]: + """ + Create a "continue" prompt for paginated output. + """ + bindings = KeyBindings() + + @bindings.add("y") + @bindings.add("Y") + @bindings.add("enter") + @bindings.add("space") + def yes(event: KeyPressEvent) -> None: + event.app.exit(result=True) + + @bindings.add("n") + @bindings.add("N") + @bindings.add("q") + @bindings.add("c-c") + @bindings.add("escape", eager=True) + def no(event: KeyPressEvent) -> None: + event.app.exit(result=False) + + @bindings.add("") + def _(event: KeyPressEvent) -> None: + " Disallow inserting other text. " + pass + + session: PromptSession[bool] = PromptSession( + HTML(" -- MORE --"), + key_bindings=bindings, + erase_when_done=True, + ) + return session diff --git a/setup.py b/setup.py index 6d3e93fc..d75704f7 100644 --- a/setup.py +++ b/setup.py @@ -22,6 +22,7 @@ "jedi>=0.16.0", "prompt_toolkit>=3.0.0,<3.1.0", "pygments", + "black", ], python_requires=">=3.6", classifiers=[ From bc78c9e7861a69ee48bda61c0e6daf0bec07b3bc Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 5 Jan 2021 10:42:53 +0100 Subject: [PATCH 317/470] Improved the pager prompt. --- ptpython/layout.py | 4 +- ptpython/python_input.py | 6 +-- ptpython/repl.py | 82 ++++++++++++++++++++++++++++++---------- ptpython/style.py | 2 + 4 files changed, 69 insertions(+), 25 deletions(-) diff --git a/ptpython/layout.py b/ptpython/layout.py index b06b95d3..4ad70d36 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -371,9 +371,9 @@ def get_text_fragments() -> StyleAndTextTuples: else: result.extend( [ - (TB + " class:key", "[F3]", enter_history), + (TB + " class:status-toolbar.key", "[F3]", enter_history), (TB, " History ", enter_history), - (TB + " class:key", "[F6]", toggle_paste_mode), + (TB + " class:status-toolbar.key", "[F6]", toggle_paste_mode), (TB, " ", toggle_paste_mode), ] ) diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 508c42d4..1b6b8f36 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -263,7 +263,7 @@ def __init__( # Pager. self.enable_output_formatting: bool = False - self.use_pager_for_big_outputs: bool = False + self.enable_pager: bool = False # When the sidebar is visible, also show the help text. self.show_sidebar_help: bool = True @@ -745,10 +745,10 @@ def get_values(): field_name="enable_output_formatting", ), simple_option( - title="Pager for big outputs", + title="Enable pager for output", description="Use a pager for displaying outputs that don't " "fit on the screen.", - field_name="use_pager_for_big_outputs", + field_name="enable_pager", ), ], ), diff --git a/ptpython/repl.py b/ptpython/repl.py index fe869384..de1b92a4 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -13,6 +13,7 @@ import sys import traceback import warnings +from enum import Enum from typing import Any, Callable, ContextManager, Dict, Optional import black @@ -36,6 +37,7 @@ print_formatted_text, set_title, ) +from prompt_toolkit.styles import BaseStyle from prompt_toolkit.utils import DummyContext, get_cwidth from pygments.lexers import PythonLexer, PythonTracebackLexer from pygments.token import Token @@ -239,7 +241,7 @@ async def show_result(self, result: object) -> None: out_prompt + [("", fragment_list_to_text(formatted_result_repr))] ) - if self.use_pager_for_big_outputs: + if self.enable_pager: await self._print_paginated_formatted_text( to_formatted_text(formatted_output) ) @@ -264,9 +266,14 @@ async def _print_paginated_formatted_text( Print formatted text, using --MORE-- style pagination. (Avoid filling up the terminal's scrollback buffer.) """ - continue_prompt = create_continue_prompt() + pager_prompt = self.create_pager_prompt() size = self.app.output.get_size() + abort = False + + # Max number of lines allowed in the buffer before painting. + max_rows = size.rows - 1 + # Page buffer. rows_in_buffer = 0 columns_in_buffer = 0 @@ -279,6 +286,20 @@ def flush_page() -> None: columns_in_buffer = 0 rows_in_buffer = 0 + async def show_pager() -> None: + nonlocal abort, max_rows + + continue_result = await pager_prompt.prompt_async() + if continue_result == PagerResult.ABORT: + print("...") + abort = True + + elif continue_result == PagerResult.NEXT_LINE: + max_rows = 1 + + elif continue_result == PagerResult.NEXT_PAGE: + max_rows = size.rows - 1 + # Loop over lines. Show --MORE-- prompt when page is filled. for line in split_lines(formatted_text): for style, text, *_ in line: @@ -289,11 +310,10 @@ def flush_page() -> None: if columns_in_buffer + width > size.columns: # Show pager first if we get too many lines after # wrapping. - if rows_in_buffer + 1 >= size.rows - 1: + if rows_in_buffer + 1 >= max_rows: flush_page() - do_continue = await continue_prompt.prompt_async() - if not do_continue: - print("...") + await show_pager() + if abort: return rows_in_buffer += 1 @@ -302,11 +322,10 @@ def flush_page() -> None: columns_in_buffer += width page.append((style, c)) - if rows_in_buffer + 1 >= size.rows - 1: + if rows_in_buffer + 1 >= max_rows: flush_page() - do_continue = await continue_prompt.prompt_async() - if not do_continue: - print("...") + await show_pager() + if abort: return else: page.append(("", "\n")) @@ -315,6 +334,12 @@ def flush_page() -> None: flush_page() + def create_pager_prompt(self) -> PromptSession["PagerResult"]: + """ + Create pager --MORE-- prompt. + """ + return create_pager_prompt(self._current_style) + def _handle_exception(self, e: Exception) -> None: output = self.app.output @@ -497,35 +522,52 @@ async def coroutine(): repl.run() -def create_continue_prompt() -> PromptSession[bool]: +class PagerResult(Enum): + ABORT = "ABORT" + NEXT_LINE = "NEXT_LINE" + NEXT_PAGE = "NEXT_PAGE" + + +def create_pager_prompt(style: BaseStyle) -> PromptSession[PagerResult]: """ Create a "continue" prompt for paginated output. """ bindings = KeyBindings() - @bindings.add("y") - @bindings.add("Y") @bindings.add("enter") + @bindings.add("down") + def next_line(event: KeyPressEvent) -> None: + event.app.exit(result=PagerResult.NEXT_LINE) + @bindings.add("space") - def yes(event: KeyPressEvent) -> None: - event.app.exit(result=True) + def next_page(event: KeyPressEvent) -> None: + event.app.exit(result=PagerResult.NEXT_PAGE) - @bindings.add("n") - @bindings.add("N") @bindings.add("q") @bindings.add("c-c") + @bindings.add("c-d") @bindings.add("escape", eager=True) def no(event: KeyPressEvent) -> None: - event.app.exit(result=False) + event.app.exit(result=PagerResult.ABORT) @bindings.add("") def _(event: KeyPressEvent) -> None: " Disallow inserting other text. " pass - session: PromptSession[bool] = PromptSession( - HTML(" -- MORE --"), + style + + session: PromptSession[PagerResult] = PromptSession( + HTML( + "" + " -- MORE -- " + "[Enter] Scroll " + "[Space] Next page " + "[q] Quit " + ": " + ), key_bindings=bindings, erase_when_done=True, + style=style, ) return session diff --git a/ptpython/style.py b/ptpython/style.py index a084c076..b16be697 100644 --- a/ptpython/style.py +++ b/ptpython/style.py @@ -97,10 +97,12 @@ def generate_style(python_style: BaseStyle, ui_style: BaseStyle) -> BaseStyle: "status-toolbar.title": "underline", "status-toolbar.inputmode": "bg:#222222 #ffffaa", "status-toolbar.key": "bg:#000000 #888888", + "status-toolbar key": "bg:#000000 #888888", "status-toolbar.pastemodeon": "bg:#aa4444 #ffffff", "status-toolbar.pythonversion": "bg:#222222 #ffffff bold", "status-toolbar paste-mode-on": "bg:#aa4444 #ffffff", "record": "bg:#884444 white", + "status-toolbar more": "#ffff44", "status-toolbar.input-mode": "#ffff44", # The options sidebar. "sidebar": "bg:#bbbbbb #000000", From acb03f33f9746f5c8135af3ced264114be3de56d Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 5 Jan 2021 10:53:02 +0100 Subject: [PATCH 318/470] Release 3.0.8 --- CHANGELOG | 16 ++++++++++++++++ setup.py | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index b37222d5..7558f901 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,22 @@ CHANGELOG ========= +3.0.8: 2020-01-05 +----------------- + +New features: +- Optional output formatting using Black. +- Optional pager for displaying outputs that don't fit on the screen. +- Added --light-bg and --dark-bg flags to automatically optimize the brightness + of the colors according to the terminal background. +- Addd `PTPYTHON_CONFIG_HOME` for explicitely setting the config directory. +- Show completion suffixes (like '(' for functions). + +Fixes: +- Fix dictionary completion on Pandas objects. +- Stop using deprecated Jedi functions. + + 3.0.7: 2020-09-25 ----------------- diff --git a/setup.py b/setup.py index d75704f7..dd551eef 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ setup( name="ptpython", author="Jonathan Slenders", - version="3.0.7", + version="3.0.8", url="https://github.com/prompt-toolkit/ptpython", description="Python REPL build on top of prompt_toolkit", long_description=long_description, From cb03427b7dc0e980c27eee2d88cb4a854df03a7f Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 7 Jan 2021 16:19:41 +0100 Subject: [PATCH 319/470] Allow replacing the completer -> Use DynamicCompleter. --- ptpython/python_input.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 1b6b8f36..fd735d19 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -15,7 +15,12 @@ ThreadedAutoSuggest, ) from prompt_toolkit.buffer import Buffer -from prompt_toolkit.completion import Completer, FuzzyCompleter, ThreadedCompleter +from prompt_toolkit.completion import ( + Completer, + DynamicCompleter, + FuzzyCompleter, + ThreadedCompleter, +) from prompt_toolkit.document import Document from prompt_toolkit.enums import DEFAULT_BUFFER, EditingMode from prompt_toolkit.filters import Condition @@ -191,14 +196,15 @@ def __init__( self.get_globals: _GetNamespace = get_globals or (lambda: {}) self.get_locals: _GetNamespace = get_locals or self.get_globals + self.completer = _completer or PythonCompleter( + self.get_globals, + self.get_locals, + lambda: self.enable_dictionary_completion, + ) + self._completer = HidePrivateCompleter( - _completer - or FuzzyCompleter( - PythonCompleter( - self.get_globals, - self.get_locals, - lambda: self.enable_dictionary_completion, - ), + FuzzyCompleter( + DynamicCompleter(lambda: self.completer), enable_fuzzy=Condition(lambda: self.enable_fuzzy_completion), ), lambda: self.complete_private_attributes, From e9eabede316b6df293aa42df3d689016d2fc62ae Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 7 Jan 2021 16:20:01 +0100 Subject: [PATCH 320/470] Set REPL title in pager. --- ptpython/repl.py | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/ptpython/repl.py b/ptpython/repl.py index de1b92a4..332dd6ed 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -20,6 +20,7 @@ from prompt_toolkit.document import Document from prompt_toolkit.formatted_text import ( HTML, + AnyFormattedText, FormattedText, PygmentsTokens, StyleAndTextTuples, @@ -338,7 +339,7 @@ def create_pager_prompt(self) -> PromptSession["PagerResult"]: """ Create pager --MORE-- prompt. """ - return create_pager_prompt(self._current_style) + return create_pager_prompt(self._current_style, self.title) def _handle_exception(self, e: Exception) -> None: output = self.app.output @@ -528,7 +529,9 @@ class PagerResult(Enum): NEXT_PAGE = "NEXT_PAGE" -def create_pager_prompt(style: BaseStyle) -> PromptSession[PagerResult]: +def create_pager_prompt( + style: BaseStyle, title: AnyFormattedText = "" +) -> PromptSession[PagerResult]: """ Create a "continue" prompt for paginated output. """ @@ -558,13 +561,18 @@ def _(event: KeyPressEvent) -> None: style session: PromptSession[PagerResult] = PromptSession( - HTML( - "" - " -- MORE -- " - "[Enter] Scroll " - "[Space] Next page " - "[q] Quit " - ": " + merge_formatted_text( + [ + title, + HTML( + "" + " -- MORE -- " + "[Enter] Scroll " + "[Space] Next page " + "[q] Quit " + ": " + ), + ] ), key_bindings=bindings, erase_when_done=True, From 0bbb369940fa0d4ee2d09f0c19b9a43f7012e142 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Fri, 8 Jan 2021 12:39:24 +0100 Subject: [PATCH 321/470] Release 3.0.9 --- CHANGELOG | 9 +++++++++ setup.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 7558f901..80c918f2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,15 @@ CHANGELOG ========= +3.0.9: 2020-01-10 +----------------- + +New features: +- Allow replacing `PythonInput.completer` at runtime (useful for tools build on + top of ptpython). +- Show REPL title in pager. + + 3.0.8: 2020-01-05 ----------------- diff --git a/setup.py b/setup.py index dd551eef..109b0dea 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ setup( name="ptpython", author="Jonathan Slenders", - version="3.0.8", + version="3.0.9", url="https://github.com/prompt-toolkit/ptpython", description="Python REPL build on top of prompt_toolkit", long_description=long_description, From fe0ef13b852c1ea83f9c57d8c888a9a77377e4f0 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Fri, 8 Jan 2021 12:42:16 +0100 Subject: [PATCH 322/470] Removed unused import in example. --- examples/python-embed-with-custom-prompt.py | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/python-embed-with-custom-prompt.py b/examples/python-embed-with-custom-prompt.py index f9f68cc2..968aedc5 100755 --- a/examples/python-embed-with-custom-prompt.py +++ b/examples/python-embed-with-custom-prompt.py @@ -3,7 +3,6 @@ Example of embedding a Python REPL, and setting a custom prompt. """ from prompt_toolkit.formatted_text import HTML -from pygments.token import Token from ptpython.prompt_style import PromptStyle from ptpython.repl import embed From 6abf0050fe1e61d8c9f02c3d7e79d9559f9ee2e7 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 12 Jan 2021 16:48:45 +0100 Subject: [PATCH 323/470] Do dictionary completion on Sequence and Mapping objects (from collections.abc). --- ptpython/completer.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/ptpython/completer.py b/ptpython/completer.py index a5bf2d2d..da45023d 100644 --- a/ptpython/completer.py +++ b/ptpython/completer.py @@ -1,4 +1,5 @@ import ast +import collections.abc as collections_abc import inspect import keyword import re @@ -378,7 +379,10 @@ def _get_expression_completions( object_var = match.groups()[0] result = self._lookup(object_var, temp_locals) - if isinstance(result, (list, tuple, dict)): + if isinstance( + result, + (list, tuple, dict, collections_abc.Mapping, collections_abc.Sequence), + ): yield Completion("[", 0) elif result is not None: # Note: Don't call `if result` here. That can fail for types @@ -412,7 +416,7 @@ def abbr_meta(text: str) -> str: result = self._lookup(object_var, temp_locals) # If this object is a dictionary, complete the keys. - if isinstance(result, dict): + if isinstance(result, (dict, collections_abc.Mapping)): # Try to evaluate the key. key_obj = key for k in [key, key + '"', key + "'"]: @@ -437,7 +441,7 @@ def abbr_meta(text: str) -> str: pass # Complete list/tuple index keys. - elif isinstance(result, (list, tuple)): + elif isinstance(result, (list, tuple, collections_abc.Sequence)): if not key or key.isdigit(): for k in range(min(len(result), 1000)): if str(k).startswith(key): From 52a0da9e32f520908b905afee2a93175292b9a75 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Wed, 13 Jan 2021 10:33:09 +0100 Subject: [PATCH 324/470] Release 3.0.10 --- CHANGELOG | 9 +++++++++ setup.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 80c918f2..3ad6b2dd 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,15 @@ CHANGELOG ========= +3.0.10: 2020-01-13 +------------------ + +Fixes: +- Do dictionary completion on Sequence and Mapping objects (from + collections.abc). Note that dictionary completion is still turned off by + default. + + 3.0.9: 2020-01-10 ----------------- diff --git a/setup.py b/setup.py index 109b0dea..3388e91b 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ setup( name="ptpython", author="Jonathan Slenders", - version="3.0.9", + version="3.0.10", url="https://github.com/prompt-toolkit/ptpython", description="Python REPL build on top of prompt_toolkit", long_description=long_description, From 036346364e02614db66e0967038e264290c00c07 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Wed, 20 Jan 2021 10:50:45 +0100 Subject: [PATCH 325/470] Fix additional line ending after output. Use Pygments get_tokens_unprocessed. --- ptpython/repl.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ptpython/repl.py b/ptpython/repl.py index 332dd6ed..d34f6f93 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -400,7 +400,11 @@ def _lex_python_traceback(tb): def _lex_python_result(tb): " Return token list for Python string. " lexer = PythonLexer() - return lexer.get_tokens(tb) + # Use `get_tokens_unprocessed`, so that we get exactly the same string, + # without line endings appended. `print_formatted_text` already appends a + # line ending, and otherwise we'll have two line endings. + tokens = lexer.get_tokens_unprocessed(tb) + return [(tokentype, value) for index, tokentype, value in tokens] def enable_deprecation_warnings() -> None: From f0526c07a1f947f0ad6254f00eb7a9b894f1098d Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Wed, 20 Jan 2021 11:21:51 +0100 Subject: [PATCH 326/470] Improved system prompt. - Fix: accept !-style inputs in the validator again. - Added syntax highlighting for system prompt. - Added autocompletion for the system prompt. --- ptpython/completer.py | 19 +++++++++++++++---- ptpython/lexer.py | 28 ++++++++++++++++++++++++++++ ptpython/python_input.py | 6 +++--- ptpython/validator.py | 5 +++++ 4 files changed, 51 insertions(+), 7 deletions(-) create mode 100644 ptpython/lexer.py diff --git a/ptpython/completer.py b/ptpython/completer.py index da45023d..aee280f4 100644 --- a/ptpython/completer.py +++ b/ptpython/completer.py @@ -12,6 +12,7 @@ Completion, PathCompleter, ) +from prompt_toolkit.contrib.completers.system import SystemCompleter from prompt_toolkit.contrib.regular_languages.compiler import compile as compile_grammar from prompt_toolkit.contrib.regular_languages.completion import GrammarCompleter from prompt_toolkit.document import Document @@ -49,7 +50,8 @@ def __init__( self.get_locals = get_locals self.get_enable_dictionary_completion = get_enable_dictionary_completion - self.dictionary_completer = DictionaryCompleter(get_globals, get_locals) + self._system_completer = SystemCompleter() + self._dictionary_completer = DictionaryCompleter(get_globals, get_locals) self._path_completer_cache: Optional[GrammarCompleter] = None self._path_completer_grammar_cache: Optional["_CompiledGrammar"] = None @@ -139,10 +141,20 @@ def get_completions( """ Get Python completions. """ + # If the input starts with an exclamation mark. Use the system completer. + if document.text.lstrip().startswith("!"): + yield from self._system_completer.get_completions( + Document( + text=document.text[1:], cursor_position=document.cursor_position - 1 + ), + complete_event, + ) + return + # Do dictionary key completions. if self.get_enable_dictionary_completion(): has_dict_completions = False - for c in self.dictionary_completer.get_completions( + for c in self._dictionary_completer.get_completions( document, complete_event ): if c.text not in "[.": @@ -157,8 +169,7 @@ def get_completions( if complete_event.completion_requested or self._complete_path_while_typing( document ): - for c in self._path_completer.get_completions(document, complete_event): - yield c + yield from self._path_completer.get_completions(document, complete_event) # If we are inside a string, Don't do Jedi completion. if self._path_completer_grammar.match(document.text_before_cursor): diff --git a/ptpython/lexer.py b/ptpython/lexer.py new file mode 100644 index 00000000..62e470f8 --- /dev/null +++ b/ptpython/lexer.py @@ -0,0 +1,28 @@ +from typing import Callable, Optional + +from prompt_toolkit.document import Document +from prompt_toolkit.formatted_text import StyleAndTextTuples +from prompt_toolkit.lexers import Lexer, PygmentsLexer +from pygments.lexers import BashLexer +from pygments.lexers import Python3Lexer as PythonLexer + +__all__ = ["PtpythonLexer"] + + +class PtpythonLexer(Lexer): + """ + Lexer for ptpython input. + + If the input starts with an exclamation mark, use a Bash lexer, otherwise, + use a Python 3 lexer. + """ + + def __init__(self, python_lexer: Optional[Lexer] = None) -> None: + self.python_lexer = python_lexer or PygmentsLexer(PythonLexer) + self.system_lexer = PygmentsLexer(BashLexer) + + def lex_document(self, document: Document) -> Callable[[int], StyleAndTextTuples]: + if document.text.startswith("!"): + return self.system_lexer.lex_document(document) + + return self.python_lexer.lex_document(document) diff --git a/ptpython/python_input.py b/ptpython/python_input.py index fd735d19..125b2d03 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -42,7 +42,7 @@ load_open_in_editor_bindings, ) from prompt_toolkit.key_binding.vi_state import InputMode -from prompt_toolkit.lexers import DynamicLexer, Lexer, PygmentsLexer, SimpleLexer +from prompt_toolkit.lexers import DynamicLexer, Lexer, SimpleLexer from prompt_toolkit.output import ColorDepth, Output from prompt_toolkit.styles import ( AdjustBrightnessStyleTransformation, @@ -54,7 +54,6 @@ ) from prompt_toolkit.utils import is_windows from prompt_toolkit.validation import ConditionalValidator, Validator -from pygments.lexers import Python3Lexer as PythonLexer from .completer import CompletePrivateAttributes, HidePrivateCompleter, PythonCompleter from .history_browser import PythonHistory @@ -64,6 +63,7 @@ load_sidebar_bindings, ) from .layout import CompletionVisualisation, PtPythonLayout +from .lexer import PtpythonLexer from .prompt_style import ClassicPrompt, IPythonPrompt, PromptStyle from .style import generate_style, get_all_code_styles, get_all_ui_styles from .utils import get_jedi_script_from_document @@ -210,7 +210,7 @@ def __init__( lambda: self.complete_private_attributes, ) self._validator = _validator or PythonValidator(self.get_compiler_flags) - self._lexer = _lexer or PygmentsLexer(PythonLexer) + self._lexer = PtpythonLexer(_lexer) self.history: History if history_filename: diff --git a/ptpython/validator.py b/ptpython/validator.py index b63bedcb..a027ecb1 100644 --- a/ptpython/validator.py +++ b/ptpython/validator.py @@ -30,6 +30,11 @@ def validate(self, document): if text.startswith("\x1a"): return + # When the input starts with an exclamation mark. Accept as shell + # command. + if text.lstrip().startswith("!"): + return + try: if self.get_compiler_flags: flags = self.get_compiler_flags() From 40be8c54fd102a5db3295ac0fa4997d6a9ec2905 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 19 Jan 2021 18:31:47 +0100 Subject: [PATCH 327/470] Refactoring of event loop usage. - The ptpython input UI will now run in a separate thread. This makes it possible to properly embed ptpython in an asyncio application, without having to deal with nested event loops (which asyncio does not support). - The "eval" part doesn't anymore take place within a ptpython coroutine, so it can spawn its own loop if needed. This also fixes `asyncio.run()` usage in the REPL, which was broken before. - Add support for top-level await. Special thanks to both Stephen.Y and baldulin for the original prototype implementations of top-level await support. --- docs/concurrency-challenges.rst | 36 +++++ ptpython/python_input.py | 65 ++++++++ ptpython/repl.py | 261 +++++++++++++++++++++----------- 3 files changed, 270 insertions(+), 92 deletions(-) create mode 100644 docs/concurrency-challenges.rst diff --git a/docs/concurrency-challenges.rst b/docs/concurrency-challenges.rst new file mode 100644 index 00000000..1a94d491 --- /dev/null +++ b/docs/concurrency-challenges.rst @@ -0,0 +1,36 @@ + +Concurrency-related challenges regarding embedding of ptpython in asyncio code +============================================================================== + +Things we want to be possible +----------------------------- + +- embed blocking ptpython in non-asyncio code. +- embed blocking ptpython in asyncio code (the event loop will block). +- embed awaitable ptpython in asyncio code (the loop will continue). +- react to resize events (SIGWINCH). +- support top-level await. +- Be able to patch_stdout, so that logging messages from another thread will be + printed above the prompt. +- It should be possible to handle `KeyboardInterrupt` during evaluation of an + expression. (This only works if the "eval" happens in the main thread.) +- The "eval" should happen in the same thread in which embed() was used. + +- create asyncio background tasks and have them run in the ptpython event loop. +- create asyncio background tasks and have ptpython run in a separate, isolated loop. + +Limitations of asyncio/python +----------------------------- + +- Spawning a new event loop in an existing event loop (from in a coroutine) is + not allowed. We can however spawn the event loop in a separate thread, and + wait for that thread to finish. + +- We can't listen to SIGWINCH signals, but prompt_toolkit's terminal size + polling solves that. + +- For patch_stdout to work correctly, we have to know what prompt_toolkit + application is running on the terminal, and tell that application to print + the output and redraw itself. + +- Handling of `KeyboardInterrupt`. diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 125b2d03..fb0cc6a3 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -4,6 +4,7 @@ """ import __future__ +import threading from asyncio import get_event_loop from functools import partial from typing import TYPE_CHECKING, Any, Callable, Dict, Generic, List, Optional, TypeVar @@ -996,3 +997,67 @@ async def do_in_terminal() -> None: app.vi_state.input_mode = InputMode.INSERT asyncio.ensure_future(do_in_terminal()) + + def read(self) -> str: + """ + Read the input. + + This will run the Python input user interface in another thread, wait + for input to be accepted and return that. By running the UI in another + thread, we avoid issues regarding possibly nested event loops. + + This can raise EOFError, when Control-D is pressed. + """ + # Capture the current input_mode in order to restore it after reset, + # for ViState.reset() sets it to InputMode.INSERT unconditionally and + # doesn't accept any arguments. + def pre_run( + last_input_mode: InputMode = self.app.vi_state.input_mode, + ) -> None: + if self.vi_keep_last_used_mode: + self.app.vi_state.input_mode = last_input_mode + + if not self.vi_keep_last_used_mode and self.vi_start_in_navigation_mode: + self.app.vi_state.input_mode = InputMode.NAVIGATION + + # Run the UI. + result: str = "" + exception: Optional[BaseException] = None + + def in_thread() -> None: + nonlocal result, exception + try: + while True: + try: + result = self.app.run(pre_run=pre_run) + + if result.lstrip().startswith("\x1a"): + # When the input starts with Ctrl-Z, quit the REPL. + # (Important for Windows users.) + raise EOFError + + # If the input is single line, remove leading whitespace. + # (This doesn't have to be a syntax error.) + if len(result.splitlines()) == 1: + result = result.strip() + + if result and not result.isspace(): + return + except KeyboardInterrupt: + # Abort - try again. + self.default_buffer.document = Document() + except BaseException as e: + exception = e + return + + finally: + if self.insert_blank_line_after_input: + self.app.output.write("\n") + + thread = threading.Thread(target=in_thread) + thread.start() + thread.join() + + if exception is not None: + raise exception + return result diff --git a/ptpython/repl.py b/ptpython/repl.py index d34f6f93..3f88fe18 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -11,13 +11,15 @@ import builtins import os import sys +import threading import traceback +import types import warnings +from dis import COMPILER_FLAG_NAMES from enum import Enum from typing import Any, Callable, ContextManager, Dict, Optional import black -from prompt_toolkit.document import Document from prompt_toolkit.formatted_text import ( HTML, AnyFormattedText, @@ -30,7 +32,6 @@ ) from prompt_toolkit.formatted_text.utils import fragment_list_to_text, split_lines from prompt_toolkit.key_binding import KeyBindings, KeyPressEvent -from prompt_toolkit.key_binding.vi_state import InputMode from prompt_toolkit.patch_stdout import patch_stdout as patch_stdout_context from prompt_toolkit.shortcuts import ( PromptSession, @@ -45,15 +46,39 @@ from .python_input import PythonInput +try: + from ast import PyCF_ALLOW_TOP_LEVEL_AWAIT +except ImportError: + PyCF_ALLOW_TOP_LEVEL_AWAIT = 0 + __all__ = ["PythonRepl", "enable_deprecation_warnings", "run_config", "embed"] +def _get_coroutine_flag() -> Optional[int]: + for k, v in COMPILER_FLAG_NAMES.items(): + if v == "COROUTINE": + return k + + # Flag not found. + return None + + +COROUTINE_FLAG: Optional[int] = _get_coroutine_flag() + + +def _has_coroutine_flag(code: types.CodeType) -> bool: + if COROUTINE_FLAG is None: + # Not supported on this Python version. + return False + + return bool(code.co_flags & COROUTINE_FLAG) + + class PythonRepl(PythonInput): def __init__(self, *a, **kw) -> None: self._startup_paths = kw.pop("startup_paths", None) super().__init__(*a, **kw) self._load_start_paths() - self.pt_loop = asyncio.new_event_loop() def _load_start_paths(self) -> None: " Start the Read-Eval-Print Loop. " @@ -68,77 +93,82 @@ def _load_start_paths(self) -> None: output.write("WARNING | File not found: {}\n\n".format(path)) def run(self) -> None: - # In order to make sure that asyncio code written in the - # interactive shell doesn't interfere with the prompt, we run the - # prompt in a different event loop. - # If we don't do this, people could spawn coroutine with a - # while/true inside which will freeze the prompt. - - try: - old_loop: Optional[asyncio.AbstractEventLoop] = asyncio.get_event_loop() - except RuntimeError: - # This happens when the user used `asyncio.run()`. - old_loop = None - - asyncio.set_event_loop(self.pt_loop) - try: - return self.pt_loop.run_until_complete(self.run_async()) - finally: - # Restore the original event loop. - asyncio.set_event_loop(old_loop) - - async def run_async(self) -> None: + """ + Run the REPL loop. + """ if self.terminal_title: set_title(self.terminal_title) while True: - # Capture the current input_mode in order to restore it after reset, - # for ViState.reset() sets it to InputMode.INSERT unconditionally and - # doesn't accept any arguments. - def pre_run( - last_input_mode: InputMode = self.app.vi_state.input_mode, - ) -> None: - if self.vi_keep_last_used_mode: - self.app.vi_state.input_mode = last_input_mode - - if not self.vi_keep_last_used_mode and self.vi_start_in_navigation_mode: - self.app.vi_state.input_mode = InputMode.NAVIGATION - - # Run the UI. + # Read. try: - text = await self.app.run_async(pre_run=pre_run) + text = self.read() except EOFError: return - except KeyboardInterrupt: - # Abort - try again. - self.default_buffer.document = Document() + + # Eval. + try: + result = self.eval(text) + except KeyboardInterrupt as e: # KeyboardInterrupt doesn't inherit from Exception. + self._handle_keyboard_interrupt(e) + except BaseException as e: + self._handle_exception(e) else: - await self._process_text(text) + # Print. + if result is not None: + self.show_result(result) + + # Loop. + self.current_statement_index += 1 + self.signatures = [] if self.terminal_title: clear_title() - async def _process_text(self, line: str) -> None: + async def run_async(self) -> None: + """ + Run the REPL loop, but run the blocking parts in an executor, so that + we don't block the event loop. Both the input and output (which can + display a pager) will run in a separate thread with their own event + loop, this way ptpython's own event loop won't interfere with the + asyncio event loop from where this is called. + + The "eval" however happens in the current thread, which is important. + (Both for control-C to work, as well as for the code to see the right + thread in which it was embedded). + """ + loop = asyncio.get_event_loop() + + if self.terminal_title: + set_title(self.terminal_title) - if line and not line.isspace(): - if self.insert_blank_line_after_input: - self.app.output.write("\n") + while True: + # Read. + try: + text = await loop.run_in_executor(None, self.read) + except EOFError: + return + # Eval. try: - # Eval and print. - await self._execute(line) + result = await self.eval_async(text) except KeyboardInterrupt as e: # KeyboardInterrupt doesn't inherit from Exception. self._handle_keyboard_interrupt(e) - except Exception as e: + except BaseException as e: self._handle_exception(e) + else: + # Print. + if result is not None: + await loop.run_in_executor(None, lambda: self.show_result(result)) - if self.insert_blank_line_after_output: - self.app.output.write("\n") + # Loop. + self.current_statement_index += 1 + self.signatures = [] - self.current_statement_index += 1 - self.signatures = [] + if self.terminal_title: + clear_title() - async def _execute(self, line: str) -> None: + def eval(self, line: str) -> object: """ Evaluate the line and print the result. """ @@ -147,45 +177,79 @@ async def _execute(self, line: str) -> None: if "" not in sys.path: sys.path.insert(0, "") - def compile_with_flags(code: str, mode: str): - " Compile code with the right compiler flags. " - return compile( - code, - "", - mode, - flags=self.get_compiler_flags(), - dont_inherit=True, - ) + if line.lstrip().startswith("!"): + # Run as shell command + os.system(line[1:]) + else: + # Try eval first + try: + code = self._compile_with_flags(line, "eval") + except SyntaxError: + # If not a valid `eval` expression, run using `exec` instead. + code = self._compile_with_flags(line, "exec") + exec(code, self.get_globals(), self.get_locals()) + else: + # No syntax errors for eval. Do eval. + result = eval(code, self.get_globals(), self.get_locals()) + + if _has_coroutine_flag(code): + result = asyncio.get_event_loop().run_until_complete(result) - # If the input is single line, remove leading whitespace. - # (This doesn't have to be a syntax error.) - if len(line.splitlines()) == 1: - line = line.strip() + self._store_eval_result(result) + return result - if line.lstrip().startswith("\x1a"): - # When the input starts with Ctrl-Z, quit the REPL. - self.app.exit() + return None + + async def eval_async(self, line: str) -> object: + """ + Evaluate the line and print the result. + """ + # WORKAROUND: Due to a bug in Jedi, the current directory is removed + # from sys.path. See: https://github.com/davidhalter/jedi/issues/1148 + if "" not in sys.path: + sys.path.insert(0, "") - elif line.lstrip().startswith("!"): + if line.lstrip().startswith("!"): # Run as shell command os.system(line[1:]) else: # Try eval first try: - code = compile_with_flags(line, "eval") + code = self._compile_with_flags(line, "eval") + except SyntaxError: + # If not a valid `eval` expression, run using `exec` instead. + code = self._compile_with_flags(line, "exec") + exec(code, self.get_globals(), self.get_locals()) + else: + # No syntax errors for eval. Do eval. result = eval(code, self.get_globals(), self.get_locals()) - locals: Dict[str, Any] = self.get_locals() - locals["_"] = locals["_%i" % self.current_statement_index] = result + if _has_coroutine_flag(code): + result = await result - if result is not None: - await self.show_result(result) - # If not a valid `eval` expression, run using `exec` instead. - except SyntaxError: - code = compile_with_flags(line, "exec") - exec(code, self.get_globals(), self.get_locals()) + self._store_eval_result(result) + return result + + return None - async def show_result(self, result: object) -> None: + def _store_eval_result(self, result: object) -> None: + locals: Dict[str, Any] = self.get_locals() + locals["_"] = locals["_%i" % self.current_statement_index] = result + + def get_compiler_flags(self) -> int: + return super().get_compiler_flags() | PyCF_ALLOW_TOP_LEVEL_AWAIT + + def _compile_with_flags(self, code: str, mode: str): + " Compile code with the right compiler flags. " + return compile( + code, + "", + mode, + flags=self.get_compiler_flags(), + dont_inherit=True, + ) + + def show_result(self, result: object) -> None: """ Show __repr__ for an `eval` result. """ @@ -243,14 +307,15 @@ async def show_result(self, result: object) -> None: ) if self.enable_pager: - await self._print_paginated_formatted_text( - to_formatted_text(formatted_output) - ) + self.print_paginated_formatted_text(to_formatted_text(formatted_output)) else: self.print_formatted_text(to_formatted_text(formatted_output)) self.app.output.flush() + if self.insert_blank_line_after_output: + self.app.output.write("\n") + def print_formatted_text(self, formatted_text: StyleAndTextTuples) -> None: print_formatted_text( FormattedText(formatted_text), @@ -260,7 +325,7 @@ def print_formatted_text(self, formatted_text: StyleAndTextTuples) -> None: output=self.app.output, ) - async def _print_paginated_formatted_text( + def print_paginated_formatted_text( self, formatted_text: StyleAndTextTuples ) -> None: """ @@ -287,18 +352,30 @@ def flush_page() -> None: columns_in_buffer = 0 rows_in_buffer = 0 - async def show_pager() -> None: + def show_pager() -> None: nonlocal abort, max_rows - continue_result = await pager_prompt.prompt_async() - if continue_result == PagerResult.ABORT: + # Run pager prompt in another thread. + # Same as for the input. This prevents issues with nested event + # loops. + pager_result = None + + def in_thread() -> None: + nonlocal pager_result + pager_result = pager_prompt.prompt() + + th = threading.Thread(target=in_thread) + th.start() + th.join() + + if pager_result == PagerResult.ABORT: print("...") abort = True - elif continue_result == PagerResult.NEXT_LINE: + elif pager_result == PagerResult.NEXT_LINE: max_rows = 1 - elif continue_result == PagerResult.NEXT_PAGE: + elif pager_result == PagerResult.NEXT_PAGE: max_rows = size.rows - 1 # Loop over lines. Show --MORE-- prompt when page is filled. @@ -313,7 +390,7 @@ async def show_pager() -> None: # wrapping. if rows_in_buffer + 1 >= max_rows: flush_page() - await show_pager() + show_pager() if abort: return @@ -325,7 +402,7 @@ async def show_pager() -> None: if rows_in_buffer + 1 >= max_rows: flush_page() - await show_pager() + show_pager() if abort: return else: @@ -341,7 +418,7 @@ def create_pager_prompt(self) -> PromptSession["PagerResult"]: """ return create_pager_prompt(self._current_style, self.title) - def _handle_exception(self, e: Exception) -> None: + def _handle_exception(self, e: BaseException) -> None: output = self.app.output # Instead of just calling ``traceback.format_exc``, we take the From c794c120ae7034a75c1d6f14e09f3ec662eef954 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Wed, 20 Jan 2021 17:56:41 +0100 Subject: [PATCH 328/470] Don't run PYTHONSTARTUP when -i flag was given. --- ptpython/entry_points/run_ptpython.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ptpython/entry_points/run_ptpython.py b/ptpython/entry_points/run_ptpython.py index e1255905..0b3dbdb9 100644 --- a/ptpython/entry_points/run_ptpython.py +++ b/ptpython/entry_points/run_ptpython.py @@ -165,7 +165,8 @@ def run() -> None: # --interactive if a.interactive and a.args: - startup_paths.append(a.args[0]) + # Note that we shouldn't run PYTHONSTARTUP when -i is given. + startup_paths = [a.args[0]] sys.argv = a.args # Add the current directory to `sys.path`. From 4a74eb5621a018c340160a13d47b4e786c0ec19b Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 21 Jan 2021 15:44:35 +0100 Subject: [PATCH 329/470] Require prompt_toolkit 3.0.11 for the latest ptpython. --- setup.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 3388e91b..f4ccfed0 100644 --- a/setup.py +++ b/setup.py @@ -20,7 +20,10 @@ "appdirs", "importlib_metadata;python_version<'3.8'", "jedi>=0.16.0", - "prompt_toolkit>=3.0.0,<3.1.0", + # Use prompt_toolkit 3.0.11, because ptpython now runs the UI in the + # background thread, and we need the terminal size polling that was + # introduced here. + "prompt_toolkit>=3.0.11,<3.1.0", "pygments", "black", ], From cf422acc58eaaf9c8fdb435b7587adc1331c93ca Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 21 Jan 2021 15:45:20 +0100 Subject: [PATCH 330/470] Move 'black' import inline. --- ptpython/repl.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ptpython/repl.py b/ptpython/repl.py index 3f88fe18..84b015b6 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -19,7 +19,6 @@ from enum import Enum from typing import Any, Callable, ContextManager, Dict, Optional -import black from prompt_toolkit.formatted_text import ( HTML, AnyFormattedText, @@ -264,6 +263,9 @@ def show_result(self, result: object) -> None: else: # Syntactically correct. Format with black and syntax highlight. if self.enable_output_formatting: + # Inline import. Slightly speed up start-up time if black is + # not used. + import black result_repr = black.format_str( result_repr, mode=black.FileMode(line_length=self.app.output.get_size().columns), From 72f2ed7d4fc55bf7ae5568e7c2e634893cb74ac6 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 21 Jan 2021 17:27:14 +0100 Subject: [PATCH 331/470] Extended the concurrency-challenges documentation. --- docs/concurrency-challenges.rst | 87 +++++++++++++++++++++++++++------ 1 file changed, 71 insertions(+), 16 deletions(-) diff --git a/docs/concurrency-challenges.rst b/docs/concurrency-challenges.rst index 1a94d491..b56d9698 100644 --- a/docs/concurrency-challenges.rst +++ b/docs/concurrency-challenges.rst @@ -5,32 +5,87 @@ Concurrency-related challenges regarding embedding of ptpython in asyncio code Things we want to be possible ----------------------------- -- embed blocking ptpython in non-asyncio code. -- embed blocking ptpython in asyncio code (the event loop will block). -- embed awaitable ptpython in asyncio code (the loop will continue). -- react to resize events (SIGWINCH). -- support top-level await. +- Embed blocking ptpython in non-asyncio code (the normal use case). +- Embed blocking ptpython in asyncio code (the event loop will block). +- Embed awaitable ptpython in asyncio code (the loop will continue). +- React to resize events (SIGWINCH). +- Support top-level await. - Be able to patch_stdout, so that logging messages from another thread will be printed above the prompt. - It should be possible to handle `KeyboardInterrupt` during evaluation of an - expression. (This only works if the "eval" happens in the main thread.) -- The "eval" should happen in the same thread in which embed() was used. + expression. +- The "eval" should happen in the same thread from where embed() was called. -- create asyncio background tasks and have them run in the ptpython event loop. -- create asyncio background tasks and have ptpython run in a separate, isolated loop. Limitations of asyncio/python ----------------------------- -- Spawning a new event loop in an existing event loop (from in a coroutine) is - not allowed. We can however spawn the event loop in a separate thread, and - wait for that thread to finish. +- We can only listen to SIGWINCH signal (resize) events in the main thread. -- We can't listen to SIGWINCH signals, but prompt_toolkit's terminal size - polling solves that. +- Usage of Control-C for triggering a `KeyboardInterrupt` only works for code + running in the main thread. (And only if the terminal was not set in raw + input mode). + +- Spawning a new event loop from within a coroutine, that's being executed in + an existing event loop is not allowed in asyncio. We can however spawn any + event loop in a separate thread, and wait for that thread to finish. - For patch_stdout to work correctly, we have to know what prompt_toolkit - application is running on the terminal, and tell that application to print + application is running on the terminal, then tell that application to print the output and redraw itself. -- Handling of `KeyboardInterrupt`. + +Additional challenges for IPython +--------------------------------- + +IPython supports integration of 3rd party event loops (for various GUI +toolkits). These event loops are supposed to continue running while we are +prompting for input. In an asyncio environment, it means that there are +situations where we have to juggle three event loops: + +- The asyncio loop in which the code was embedded. +- The asyncio loop from the prompt. +- The 3rd party GUI loop. + +Approach taken in ptpython 3.0.11 +--------------------------------- + +For ptpython, the most reliable solution is to to run the prompt_toolkit input +prompt in a separate background thread. This way it can use its own asyncio +event loop without ever having to interfere with whatever runs in the main +thread. + +Then, depending on how we embed, we do the following: +When a normal blocking embed is used: + * We start the UI thread for the input, and do a blocking wait on + `thread.join()` here. + * The "eval" happens in the main thread. + * The "print" happens also in the main thread. Unless a pager is shown, + which is also a prompt_toolkit application, then the pager itself is runs + also in another thread, similar to the way we do the input. + +When an awaitable embed is used, for embedding in a coroutine, but having the +event loop continue: + * We run the input method from the blocking embed in an asyncio executor + and do an `await loop.run_in_excecutor(...)`. + * The "eval" happens again in the main thread. + * "print" is also similar, except that the pager code (if used) runs in an + executor too. + +This means that the prompt_toolkit application code will always run in a +different thread. It means it won't be able to respond to SIGWINCH (window +resize events), but prompt_toolkit's 3.0.11 has now terminal size polling which +solves this. + +Control-C key presses won't interrupt the main thread while we wait for input, +because the prompt_toolkit application turns the terminal in raw mode, while +it's reading, which means that it will receive control-c key presses as raw +data in its own thread. + +Top-level await works in most situations as expected. +- If a blocking embed is used. We execute ``loop.run_until_complete(code)``. + This assumes that the blocking embed is not used in a coroutine of a running + event loop, otherwise, this will attempt to start a nested event loop, which + asyncio does not support. In that case we will get an exception. +- If an awaitable embed is used. We literally execute ``await code``. This will + integrate nicely in the current event loop. From 8ab2e167df76dad64293a1b73b1b5c1f974d4b60 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 21 Jan 2021 15:45:56 +0100 Subject: [PATCH 332/470] Release 3.0.11 --- CHANGELOG | 23 +++++++++++++++++++++++ setup.py | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 3ad6b2dd..daba760f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,29 @@ CHANGELOG ========= +3.0.11: 2020-01-20 +------------------ + +New features: +- Add support for top-level await. +- Refactoring of event loop usage: + + * The ptpython input UI will now run in a separate thread. This makes it + possible to properly embed ptpython in an asyncio application, without + having to deal with nested event loops (which asyncio does not support). + + * The "eval" part doesn't anymore take place within a ptpython coroutine, so + it can spawn its own loop if needed. This also fixes `asyncio.run()` usage + in the REPL, which was broken before. + +- Added syntax highlighting and autocompletion for !-style system commands. + +Fixes: +- Remove unexpected additional line after output. +- Fix system prompt. Accept !-style inputs again. +- Don't execute PYTHONSTARTUP when -i flag was given. + + 3.0.10: 2020-01-13 ------------------ diff --git a/setup.py b/setup.py index f4ccfed0..af20ec6a 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ setup( name="ptpython", author="Jonathan Slenders", - version="3.0.10", + version="3.0.11", url="https://github.com/prompt-toolkit/ptpython", description="Python REPL build on top of prompt_toolkit", long_description=long_description, From dcc43f1ea74e9107d0aa99020f02f8ee751821d2 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Fri, 22 Jan 2021 16:44:47 +0100 Subject: [PATCH 333/470] Update README. We support up to Python 3.9 --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 0cf7f3c0..ae12f4d7 100644 --- a/README.rst +++ b/README.rst @@ -12,7 +12,7 @@ ptpython .. image :: https://github.com/jonathanslenders/ptpython/raw/master/docs/images/example1.png Ptpython is an advanced Python REPL. It should work on all -Python versions from 2.6 up to 3.7 and work cross platform (Linux, +Python versions from 2.6 up to 3.9 and work cross platform (Linux, BSD, OS X and Windows). Note: this version of ptpython requires at least Python 3.6. Install ptpython From 742c6d7c77fe03a42554304131083213b6103d63 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Fri, 22 Jan 2021 16:38:25 +0100 Subject: [PATCH 334/470] Properly handle SystemExit. --- ptpython/repl.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ptpython/repl.py b/ptpython/repl.py index 84b015b6..963b041d 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -110,6 +110,8 @@ def run(self) -> None: result = self.eval(text) except KeyboardInterrupt as e: # KeyboardInterrupt doesn't inherit from Exception. self._handle_keyboard_interrupt(e) + except SystemExit: + return except BaseException as e: self._handle_exception(e) else: @@ -153,6 +155,8 @@ async def run_async(self) -> None: result = await self.eval_async(text) except KeyboardInterrupt as e: # KeyboardInterrupt doesn't inherit from Exception. self._handle_keyboard_interrupt(e) + except SystemExit: + return except BaseException as e: self._handle_exception(e) else: From 7e49c40371e443bbf9b9c82f1cae2a663ab736ea Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Fri, 22 Jan 2021 16:43:35 +0100 Subject: [PATCH 335/470] Properly handle exceptions when trying to access __pt_repr__. --- ptpython/repl.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/ptpython/repl.py b/ptpython/repl.py index 963b041d..98978119 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -281,15 +281,17 @@ def show_result(self, result: object) -> None: # If __pt_repr__ is present, take this. This can return prompt_toolkit # formatted text. - if hasattr(result, "__pt_repr__"): - try: + try: + if hasattr(result, "__pt_repr__"): formatted_result_repr = to_formatted_text( getattr(result, "__pt_repr__")() ) if isinstance(formatted_result_repr, list): formatted_result_repr = FormattedText(formatted_result_repr) - except: - pass + except: + # For bad code, `__getattr__` can raise something that's not an + # `AttributeError`. This happens already when calling `hasattr()`. + pass # Align every line to the prompt. line_sep = "\n" + " " * fragment_list_width(out_prompt) From 5ccf10a03307a4e534f6530eaa72ed4966147df4 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Fri, 22 Jan 2021 16:54:47 +0100 Subject: [PATCH 336/470] Expose 'embed' function at the top-level of ptpython. --- ptpython/__init__.py | 3 +++ ptpython/repl.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/ptpython/__init__.py b/ptpython/__init__.py index e69de29b..4908eba8 100644 --- a/ptpython/__init__.py +++ b/ptpython/__init__.py @@ -0,0 +1,3 @@ +from .repl import embed + +__all__ = ["embed"] diff --git a/ptpython/repl.py b/ptpython/repl.py index 98978119..70e347eb 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -270,6 +270,7 @@ def show_result(self, result: object) -> None: # Inline import. Slightly speed up start-up time if black is # not used. import black + result_repr = black.format_str( result_repr, mode=black.FileMode(line_length=self.app.output.get_size().columns), @@ -562,6 +563,8 @@ def embed( :param configure: Callable that will be called with the `PythonRepl` as a first argument, to trigger configuration. :param title: Title to be displayed in the terminal titlebar. (None or string.) + :param patch_stdout: When true, patch `sys.stdout` so that background + threads that are printing will print nicely above the prompt. """ # Default globals/locals if globals is None: From 2cc7802610d158c9e8514460f13e5015030a02e5 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sat, 23 Jan 2021 19:59:35 +0100 Subject: [PATCH 337/470] Ignore typing error regarding PyCF_ALLOW_TOP_LEVEL_AWAIT (not known for older Python versions). --- ptpython/repl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ptpython/repl.py b/ptpython/repl.py index 70e347eb..f90a9c36 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -46,7 +46,7 @@ from .python_input import PythonInput try: - from ast import PyCF_ALLOW_TOP_LEVEL_AWAIT + from ast import PyCF_ALLOW_TOP_LEVEL_AWAIT # type: ignore except ImportError: PyCF_ALLOW_TOP_LEVEL_AWAIT = 0 From 0d0509c840e93826b42a6ae6509cb9e893b369b2 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Fri, 22 Jan 2021 17:43:22 +0100 Subject: [PATCH 338/470] Expose a get_ptpython function in the global namespace. --- ptpython/repl.py | 128 +++++++++++++++++++++++++++++------------------ 1 file changed, 79 insertions(+), 49 deletions(-) diff --git a/ptpython/repl.py b/ptpython/repl.py index f90a9c36..2883e770 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -98,33 +98,38 @@ def run(self) -> None: if self.terminal_title: set_title(self.terminal_title) - while True: - # Read. - try: - text = self.read() - except EOFError: - return + self._add_to_namespace() - # Eval. - try: - result = self.eval(text) - except KeyboardInterrupt as e: # KeyboardInterrupt doesn't inherit from Exception. - self._handle_keyboard_interrupt(e) - except SystemExit: - return - except BaseException as e: - self._handle_exception(e) - else: - # Print. - if result is not None: - self.show_result(result) + try: + while True: + # Read. + try: + text = self.read() + except EOFError: + return - # Loop. - self.current_statement_index += 1 - self.signatures = [] + # Eval. + try: + result = self.eval(text) + except KeyboardInterrupt as e: # KeyboardInterrupt doesn't inherit from Exception. + self._handle_keyboard_interrupt(e) + except SystemExit: + return + except BaseException as e: + self._handle_exception(e) + else: + # Print. + if result is not None: + self.show_result(result) - if self.terminal_title: - clear_title() + # Loop. + self.current_statement_index += 1 + self.signatures = [] + + finally: + if self.terminal_title: + clear_title() + self._remove_from_namespace() async def run_async(self) -> None: """ @@ -143,33 +148,39 @@ async def run_async(self) -> None: if self.terminal_title: set_title(self.terminal_title) - while True: - # Read. - try: - text = await loop.run_in_executor(None, self.read) - except EOFError: - return - - # Eval. - try: - result = await self.eval_async(text) - except KeyboardInterrupt as e: # KeyboardInterrupt doesn't inherit from Exception. - self._handle_keyboard_interrupt(e) - except SystemExit: - return - except BaseException as e: - self._handle_exception(e) - else: - # Print. - if result is not None: - await loop.run_in_executor(None, lambda: self.show_result(result)) + self._add_to_namespace() - # Loop. - self.current_statement_index += 1 - self.signatures = [] + try: + while True: + # Read. + try: + text = await loop.run_in_executor(None, self.read) + except EOFError: + return - if self.terminal_title: - clear_title() + # Eval. + try: + result = await self.eval_async(text) + except KeyboardInterrupt as e: # KeyboardInterrupt doesn't inherit from Exception. + self._handle_keyboard_interrupt(e) + except SystemExit: + return + except BaseException as e: + self._handle_exception(e) + else: + # Print. + if result is not None: + await loop.run_in_executor( + None, lambda: self.show_result(result) + ) + + # Loop. + self.current_statement_index += 1 + self.signatures = [] + finally: + if self.terminal_title: + clear_title() + self._remove_from_namespace() def eval(self, line: str) -> object: """ @@ -476,6 +487,25 @@ def _handle_keyboard_interrupt(self, e: KeyboardInterrupt) -> None: output.write("\rKeyboardInterrupt\n\n") output.flush() + def _add_to_namespace(self) -> None: + """ + Add ptpython built-ins to global namespace. + """ + globals = self.get_globals() + + # Add a 'get_ptpython', similar to 'get_ipython' + def get_ptpython() -> PythonInput: + return self + + globals["get_ptpython"] = get_ptpython + + def _remove_from_namespace(self) -> None: + """ + Remove added symbols from the globals. + """ + globals = self.get_globals() + del globals["get_ptpython"] + def _lex_python_traceback(tb): " Return token list for traceback string. " From 06554f9863b52b0db392725be98c19a1ef82bb3c Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sun, 24 Jan 2021 10:53:38 +0100 Subject: [PATCH 339/470] Release 3.0.12 --- CHANGELOG | 14 ++++++++++++++ setup.py | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index daba760f..ee90fcb5 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,20 @@ CHANGELOG ========= +3.0.12: 2020-01-24 +------------------ + +New features: +- Expose a `get_ptpython` function in the global namespace, to get programmatic + access to the REPL. +- Expose `embed()` at the top level of the package. Make it possible to do + `from ptpython import embed`. + +Fixes: +- Properly handle exceptions when trying to access `__pt_repr__`. +- Properly handle `SystemExit`. + + 3.0.11: 2020-01-20 ------------------ diff --git a/setup.py b/setup.py index af20ec6a..0b0da6ce 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ setup( name="ptpython", author="Jonathan Slenders", - version="3.0.11", + version="3.0.12", url="https://github.com/prompt-toolkit/ptpython", description="Python REPL build on top of prompt_toolkit", long_description=long_description, From 3dac89a804473d041906cb1649d1b1429675b46a Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Mon, 25 Jan 2021 16:44:29 +0100 Subject: [PATCH 340/470] Added Github actions test.yaml file and removed .travis.yml. --- .github/workflows/test.yaml | 38 +++++++++++++++++++++++++++++++++++++ .travis.yml | 26 ------------------------- 2 files changed, 38 insertions(+), 26 deletions(-) create mode 100644 .github/workflows/test.yaml delete mode 100644 .travis.yml diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 00000000..00ed1b00 --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,38 @@ +name: test + +on: + push: # any branch + pull_request: + branches: [master] + +jobs: + test-ubuntu: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.6, 3.7, 3.8, 3.9] + + steps: + - uses: actions/checkout@v2 + - name: Setup Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + - name: Install Dependencies + run: | + sudo apt remove python3-pip + python -m pip install --upgrade pip + python -m pip install . black isort mypy pytest readme_renderer + pip list + - name: Type Checker + run: | + mypy ptpython + isort -c --profile black ptpython examples setup.py + black --check ptpython examples setup.py + - name: Run Tests + run: | + ./tests/run_tests.py + - name: Validate README.md + # Ensure that the README renders correctly (required for uploading to PyPI). + run: | + python -m readme_renderer README.rst > /dev/null diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index e622b352..00000000 --- a/.travis.yml +++ /dev/null @@ -1,26 +0,0 @@ -sudo: false -language: python - -matrix: - include: - - python: 3.6 - - python: 3.7 - -install: - - travis_retry pip install . pytest isort black mypy readme_renderer - - pip list - -script: - - echo "$TRAVIS_PYTHON_VERSION" - - ./tests/run_tests.py - - # Check wheather the imports were sorted correctly. - - isort -c -rc ptpython tests setup.py examples - - - black --check ptpython setup.py examples - - # Type checking - - mypy ptpython - - # Ensure that the README renders correctly (required for uploading to PyPI). - - python -m readme_renderer README.rst > /dev/null From 24756f48e0d6a32ab67a73b35d32e99f14f43b7c Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Mon, 25 Jan 2021 14:43:39 +0100 Subject: [PATCH 341/470] Remove extra line ending in paginated output. When the "Enable pager for output" option is used, an extra line ending was printed. --- ptpython/repl.py | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/ptpython/repl.py b/ptpython/repl.py index 2883e770..e8ca3a0e 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -336,17 +336,22 @@ def show_result(self, result: object) -> None: if self.insert_blank_line_after_output: self.app.output.write("\n") - def print_formatted_text(self, formatted_text: StyleAndTextTuples) -> None: + def print_formatted_text( + self, formatted_text: StyleAndTextTuples, end: str = "\n" + ) -> None: print_formatted_text( FormattedText(formatted_text), style=self._current_style, style_transformation=self.style_transformation, include_default_pygments_style=False, output=self.app.output, + end=end, ) def print_paginated_formatted_text( - self, formatted_text: StyleAndTextTuples + self, + formatted_text: StyleAndTextTuples, + end: str = "\n", ) -> None: """ Print formatted text, using --MORE-- style pagination. @@ -367,7 +372,7 @@ def print_paginated_formatted_text( def flush_page() -> None: nonlocal page, columns_in_buffer, rows_in_buffer - self.print_formatted_text(page) + self.print_formatted_text(page, end="") page = [] columns_in_buffer = 0 rows_in_buffer = 0 @@ -399,7 +404,11 @@ def in_thread() -> None: max_rows = size.rows - 1 # Loop over lines. Show --MORE-- prompt when page is filled. - for line in split_lines(formatted_text): + + formatted_text = formatted_text + [("", end)] + lines = list(split_lines(formatted_text)) + + for lineno, line in enumerate(lines): for style, text, *_ in line: for c in text: width = get_cwidth(c) @@ -426,9 +435,13 @@ def in_thread() -> None: if abort: return else: - page.append(("", "\n")) - rows_in_buffer += 1 - columns_in_buffer = 0 + # Add line ending between lines (if `end="\n"` was given, one + # more empty line is added in `split_lines` automatically to + # take care of the final line ending). + if lineno != len(lines) - 1: + page.append(("", "\n")) + rows_in_buffer += 1 + columns_in_buffer = 0 flush_page() From fcc90bb139d52f222b0cb9d237fe3849737103b2 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Mon, 25 Jan 2021 17:40:29 +0100 Subject: [PATCH 342/470] Improve handling of indented code. - Allow multiline input to be indented as a whole. (We will unindent before executing.) - Use `TabsProcessor` to properly visualize tabs that were pasted (in bracketed paste) instead of `^I`. --- ptpython/layout.py | 2 ++ ptpython/python_input.py | 10 +++++----- ptpython/utils.py | 40 +++++++++++++++++++++++++++++++++++++++- ptpython/validator.py | 10 ++++------ 4 files changed, 50 insertions(+), 12 deletions(-) diff --git a/ptpython/layout.py b/ptpython/layout.py index 4ad70d36..3cf3c77d 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -40,6 +40,7 @@ HighlightIncrementalSearchProcessor, HighlightMatchingBracketProcessor, HighlightSelectionProcessor, + TabsProcessor, ) from prompt_toolkit.lexers import SimpleLexer from prompt_toolkit.mouse_events import MouseEvent @@ -603,6 +604,7 @@ def menu_position(): ), HighlightSelectionProcessor(), DisplayMultipleCursors(), + TabsProcessor(), # Show matching parentheses, but only while editing. ConditionalProcessor( processor=HighlightMatchingBracketProcessor(chars="[](){}"), diff --git a/ptpython/python_input.py b/ptpython/python_input.py index fb0cc6a3..c84c80f9 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -67,7 +67,7 @@ from .lexer import PtpythonLexer from .prompt_style import ClassicPrompt, IPythonPrompt, PromptStyle from .style import generate_style, get_all_code_styles, get_all_ui_styles -from .utils import get_jedi_script_from_document +from .utils import get_jedi_script_from_document, unindent_code from .validator import PythonValidator __all__ = ["PythonInput"] @@ -1036,10 +1036,10 @@ def in_thread() -> None: # (Important for Windows users.) raise EOFError - # If the input is single line, remove leading whitespace. - # (This doesn't have to be a syntax error.) - if len(result.splitlines()) == 1: - result = result.strip() + # Remove leading whitespace. + # (Users can add extra indentation, which happens for + # instance because of copy/pasting code.) + result = unindent_code(result) if result and not result.isspace(): return diff --git a/ptpython/utils.py b/ptpython/utils.py index 3658085a..2fb24a41 100644 --- a/ptpython/utils.py +++ b/ptpython/utils.py @@ -2,7 +2,7 @@ For internal use only. """ import re -from typing import Callable, Type, TypeVar, cast +from typing import Callable, Iterable, Type, TypeVar, cast from prompt_toolkit.formatted_text import to_formatted_text from prompt_toolkit.formatted_text.utils import fragment_list_to_text @@ -12,6 +12,7 @@ "has_unclosed_brackets", "get_jedi_script_from_document", "document_is_multiline_python", + "unindent_code", ] @@ -158,3 +159,40 @@ def __repr__(self) -> str: cls.__repr__ = __repr__ # type:ignore return cls + + +def unindent_code(text: str) -> str: + """ + Remove common leading whitespace when all lines are indented. + """ + lines = text.splitlines(keepends=True) + + # Look for common prefix. + common_prefix = _common_whitespace_prefix(lines) + + # Remove indentation. + lines = [line[len(common_prefix) :] for line in lines] + + return "".join(lines) + + +def _common_whitespace_prefix(strings: Iterable[str]) -> str: + """ + Return common prefix for a list of lines. + This will ignore lines that contain whitespace only. + """ + # Ignore empty lines and lines that have whitespace only. + strings = [s for s in strings if not s.isspace() and not len(s) == 0] + + if not strings: + return "" + + else: + s1 = min(strings) + s2 = max(strings) + + for i, c in enumerate(s1): + if c != s2[i] or c not in " \t": + return s1[:i] + + return s1 diff --git a/ptpython/validator.py b/ptpython/validator.py index a027ecb1..0f6a4eaf 100644 --- a/ptpython/validator.py +++ b/ptpython/validator.py @@ -1,5 +1,7 @@ from prompt_toolkit.validation import ValidationError, Validator +from .utils import unindent_code + __all__ = ["PythonValidator"] @@ -18,12 +20,7 @@ def validate(self, document): """ Check input for Python syntax errors. """ - text = document.text - - # If the input is single line, remove leading whitespace. - # (This doesn't have to be a syntax error.) - if len(text.splitlines()) == 1: - text = text.strip() + text = unindent_code(document.text) # When the input starts with Ctrl-Z, always accept. This means EOF in a # Python REPL. @@ -46,6 +43,7 @@ def validate(self, document): # Note, the 'or 1' for offset is required because Python 2.7 # gives `None` as offset in case of '4=4' as input. (Looks like # fixed in Python 3.) + # TODO: This is not correct if indentation was removed. index = document.translate_row_col_to_index( e.lineno - 1, (e.offset or 1) - 1 ) From 325072295bec594b8de273efe2daecdcb08f7e6a Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Mon, 25 Jan 2021 18:27:03 +0100 Subject: [PATCH 343/470] Added 'print_all' option in pager. --- ptpython/repl.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/ptpython/repl.py b/ptpython/repl.py index e8ca3a0e..9e22e2f6 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -361,6 +361,7 @@ def print_paginated_formatted_text( size = self.app.output.get_size() abort = False + print_all = False # Max number of lines allowed in the buffer before painting. max_rows = size.rows - 1 @@ -378,7 +379,7 @@ def flush_page() -> None: rows_in_buffer = 0 def show_pager() -> None: - nonlocal abort, max_rows + nonlocal abort, max_rows, print_all # Run pager prompt in another thread. # Same as for the input. This prevents issues with nested event @@ -403,6 +404,9 @@ def in_thread() -> None: elif pager_result == PagerResult.NEXT_PAGE: max_rows = size.rows - 1 + elif pager_result == PagerResult.PRINT_ALL: + print_all = True + # Loop over lines. Show --MORE-- prompt when page is filled. formatted_text = formatted_text + [("", end)] @@ -417,7 +421,7 @@ def in_thread() -> None: if columns_in_buffer + width > size.columns: # Show pager first if we get too many lines after # wrapping. - if rows_in_buffer + 1 >= max_rows: + if rows_in_buffer + 1 >= max_rows and not print_all: flush_page() show_pager() if abort: @@ -429,7 +433,7 @@ def in_thread() -> None: columns_in_buffer += width page.append((style, c)) - if rows_in_buffer + 1 >= max_rows: + if rows_in_buffer + 1 >= max_rows and not print_all: flush_page() show_pager() if abort: @@ -662,6 +666,7 @@ class PagerResult(Enum): ABORT = "ABORT" NEXT_LINE = "NEXT_LINE" NEXT_PAGE = "NEXT_PAGE" + PRINT_ALL = "PRINT_ALL" def create_pager_prompt( @@ -681,6 +686,10 @@ def next_line(event: KeyPressEvent) -> None: def next_page(event: KeyPressEvent) -> None: event.app.exit(result=PagerResult.NEXT_PAGE) + @bindings.add("a") + def print_all(event: KeyPressEvent) -> None: + event.app.exit(result=PagerResult.PRINT_ALL) + @bindings.add("q") @bindings.add("c-c") @bindings.add("c-d") @@ -704,6 +713,7 @@ def _(event: KeyPressEvent) -> None: " -- MORE -- " "[Enter] Scroll " "[Space] Next page " + "[a] Print all " "[q] Quit " ": " ), From 7f127c5be94d682c6ffda2a17e7923c3973930d9 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Mon, 25 Jan 2021 18:26:26 +0100 Subject: [PATCH 344/470] Fix line ending bug in pager. --- ptpython/repl.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ptpython/repl.py b/ptpython/repl.py index 9e22e2f6..0006a111 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -422,6 +422,7 @@ def in_thread() -> None: # Show pager first if we get too many lines after # wrapping. if rows_in_buffer + 1 >= max_rows and not print_all: + page.append(("", "\n")) flush_page() show_pager() if abort: @@ -434,6 +435,7 @@ def in_thread() -> None: page.append((style, c)) if rows_in_buffer + 1 >= max_rows and not print_all: + page.append(("", "\n")) flush_page() show_pager() if abort: From 93d45e060e775699102311d55dc1b58fae8ce1cd Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 26 Jan 2021 09:28:27 +0100 Subject: [PATCH 345/470] Added ptpython-in-other-thread.py example. --- .../test-cases/ptpython-in-other-thread.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 examples/test-cases/ptpython-in-other-thread.py diff --git a/examples/test-cases/ptpython-in-other-thread.py b/examples/test-cases/ptpython-in-other-thread.py new file mode 100644 index 00000000..7c788464 --- /dev/null +++ b/examples/test-cases/ptpython-in-other-thread.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python +""" +Example of running ptpython in another thread. + +(For testing whether it's working fine if it's not embedded in the main +thread.) +""" +import threading + +from ptpython.repl import embed + + +def in_thread(): + embed(globals(), locals(), vi_mode=False) + + +def main(): + th = threading.Thread(target=in_thread) + th.start() + th.join() + + +if __name__ == "__main__": + main() From 9a3d6c1b2ab8247797b5cf5dea46c87767817c23 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 26 Jan 2021 10:13:10 +0100 Subject: [PATCH 346/470] Release 3.0.13 --- CHANGELOG | 15 +++++++++++++++ setup.py | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index ee90fcb5..5b704461 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,21 @@ CHANGELOG ========= +3.0.13: 2020-01-26 +------------------ + +New features: +- Added 'print all' option to pager. +- Improve handling of indented code: + * Allow multiline input to be indented as a whole (we will unindent before + executing). + * Correctly visualize tabs (instead of ^I, which happens when pasted in + bracketed paste). + +Fixes: +- Fix line ending bug in pager. + + 3.0.12: 2020-01-24 ------------------ diff --git a/setup.py b/setup.py index 0b0da6ce..09643f49 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ setup( name="ptpython", author="Jonathan Slenders", - version="3.0.12", + version="3.0.13", url="https://github.com/prompt-toolkit/ptpython", description="Python REPL build on top of prompt_toolkit", long_description=long_description, From 7ea2e5bc23301ea1790b2ec19b0554dee78c9a39 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Wed, 27 Jan 2021 10:49:53 +0100 Subject: [PATCH 347/470] Handle exceptions raised when repr() is called. --- ptpython/repl.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/ptpython/repl.py b/ptpython/repl.py index 0006a111..301b3207 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -270,7 +270,13 @@ def show_result(self, result: object) -> None: out_prompt = to_formatted_text(self.get_output_prompt()) # If the repr is valid Python code, use the Pygments lexer. - result_repr = repr(result) + try: + result_repr = repr(result) + except BaseException as e: + # Calling repr failed. + self._handle_exception(e) + return + try: compile(result_repr, "", "eval") except SyntaxError: From 5af2dac65fdf5d73b7730437952158c4b0dfb996 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 9 Feb 2021 15:09:26 +0100 Subject: [PATCH 348/470] Fix leakage of exc_info from eval to exec call. `exec()` was always executed in the `except SyntaxError` block of the try around `eval()`, and because of this ``sys.exc_info()`` would not see the right exception if called as a statement. See: https://github.com/prompt-toolkit/ptpython/issues/435 Thanks to Peter Holloway for the proposed fix. --- ptpython/repl.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/ptpython/repl.py b/ptpython/repl.py index 301b3207..1253504d 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -199,9 +199,7 @@ def eval(self, line: str) -> object: try: code = self._compile_with_flags(line, "eval") except SyntaxError: - # If not a valid `eval` expression, run using `exec` instead. - code = self._compile_with_flags(line, "exec") - exec(code, self.get_globals(), self.get_locals()) + pass else: # No syntax errors for eval. Do eval. result = eval(code, self.get_globals(), self.get_locals()) @@ -212,6 +210,13 @@ def eval(self, line: str) -> object: self._store_eval_result(result) return result + # If not a valid `eval` expression, run using `exec` instead. + # Note that we shouldn't run this in the `except SyntaxError` block + # above, then `sys.exc_info()` would not report the right error. + # See issue: https://github.com/prompt-toolkit/ptpython/issues/435 + code = self._compile_with_flags(line, "exec") + exec(code, self.get_globals(), self.get_locals()) + return None async def eval_async(self, line: str) -> object: @@ -231,9 +236,7 @@ async def eval_async(self, line: str) -> object: try: code = self._compile_with_flags(line, "eval") except SyntaxError: - # If not a valid `eval` expression, run using `exec` instead. - code = self._compile_with_flags(line, "exec") - exec(code, self.get_globals(), self.get_locals()) + pass else: # No syntax errors for eval. Do eval. result = eval(code, self.get_globals(), self.get_locals()) @@ -244,6 +247,10 @@ async def eval_async(self, line: str) -> object: self._store_eval_result(result) return result + # If not a valid `eval` expression, run using `exec` instead. + code = self._compile_with_flags(line, "exec") + exec(code, self.get_globals(), self.get_locals()) + return None def _store_eval_result(self, result: object) -> None: From 08b7417d3fdec0ddca4aa45eb899942d639bd8a3 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 9 Feb 2021 14:37:12 +0100 Subject: [PATCH 349/470] Fix handling of `KeyboardInterrupt` in REPL during evaluation of __repr__. This fixes the issue that if calling `__repr__`, `__pt_repr__` or formatting the output using "Black" takes too long and the uses presses control-C, that we don't terminate the REPL by mistake. --- ptpython/repl.py | 114 ++++++++++++++++++++++++++++------------------- 1 file changed, 69 insertions(+), 45 deletions(-) diff --git a/ptpython/repl.py b/ptpython/repl.py index 1253504d..ae7b1d0d 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -102,30 +102,39 @@ def run(self) -> None: try: while True: - # Read. try: - text = self.read() - except EOFError: - return - - # Eval. - try: - result = self.eval(text) - except KeyboardInterrupt as e: # KeyboardInterrupt doesn't inherit from Exception. + # Read. + try: + text = self.read() + except EOFError: + return + + # Eval. + try: + result = self.eval(text) + except KeyboardInterrupt as e: # KeyboardInterrupt doesn't inherit from Exception. + raise + except SystemExit: + return + except BaseException as e: + self._handle_exception(e) + else: + # Print. + if result is not None: + self.show_result(result) + + # Loop. + self.current_statement_index += 1 + self.signatures = [] + + except KeyboardInterrupt as e: + # Handle all possible `KeyboardInterrupt` errors. This can + # happen during the `eval`, but also during the + # `show_result` if something takes too long. + # (Try/catch is around the whole block, because we want to + # prevent that a Control-C keypress terminates the REPL in + # any case.) self._handle_keyboard_interrupt(e) - except SystemExit: - return - except BaseException as e: - self._handle_exception(e) - else: - # Print. - if result is not None: - self.show_result(result) - - # Loop. - self.current_statement_index += 1 - self.signatures = [] - finally: if self.terminal_title: clear_title() @@ -152,31 +161,38 @@ async def run_async(self) -> None: try: while True: - # Read. try: - text = await loop.run_in_executor(None, self.read) - except EOFError: - return - - # Eval. - try: - result = await self.eval_async(text) - except KeyboardInterrupt as e: # KeyboardInterrupt doesn't inherit from Exception. + # Read. + try: + text = await loop.run_in_executor(None, self.read) + except EOFError: + return + + # Eval. + try: + result = await self.eval_async(text) + except KeyboardInterrupt as e: # KeyboardInterrupt doesn't inherit from Exception. + raise + except SystemExit: + return + except BaseException as e: + self._handle_exception(e) + else: + # Print. + if result is not None: + await loop.run_in_executor( + None, lambda: self.show_result(result) + ) + + # Loop. + self.current_statement_index += 1 + self.signatures = [] + + except KeyboardInterrupt as e: + # XXX: This does not yet work properly. In some situations, + # `KeyboardInterrupt` exceptions can end up in the event + # loop selector. self._handle_keyboard_interrupt(e) - except SystemExit: - return - except BaseException as e: - self._handle_exception(e) - else: - # Print. - if result is not None: - await loop.run_in_executor( - None, lambda: self.show_result(result) - ) - - # Loop. - self.current_statement_index += 1 - self.signatures = [] finally: if self.terminal_title: clear_title() @@ -273,12 +289,18 @@ def _compile_with_flags(self, code: str, mode: str): def show_result(self, result: object) -> None: """ Show __repr__ for an `eval` result. + + Note: this can raise `KeyboardInterrupt` if either calling `__repr__`, + `__pt_repr__` or formatting the output with "Black" takes to long + and the user presses Control-C. """ out_prompt = to_formatted_text(self.get_output_prompt()) # If the repr is valid Python code, use the Pygments lexer. try: result_repr = repr(result) + except KeyboardInterrupt: + raise # Don't catch here. except BaseException as e: # Calling repr failed. self._handle_exception(e) @@ -313,6 +335,8 @@ def show_result(self, result: object) -> None: ) if isinstance(formatted_result_repr, list): formatted_result_repr = FormattedText(formatted_result_repr) + except KeyboardInterrupt: + raise # Don't catch here. except: # For bad code, `__getattr__` can raise something that's not an # `AttributeError`. This happens already when calling `hasattr()`. From fd97a4254e936dcc191fe57bf08c298b62a5b085 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 28 Jan 2021 12:16:08 +0100 Subject: [PATCH 350/470] Fix style for signature toolbar. --- ptpython/style.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ptpython/style.py b/ptpython/style.py index b16be697..23e51c7e 100644 --- a/ptpython/style.py +++ b/ptpython/style.py @@ -87,8 +87,8 @@ def generate_style(python_style: BaseStyle, ui_style: BaseStyle) -> BaseStyle: "arg-toolbar.text": "noinherit", # Signature toolbar. "signature-toolbar": "bg:#44bbbb #000000", - "signature-toolbar.currentname": "bg:#008888 #ffffff bold", - "signature-toolbar.operator": "#000000 bold", + "signature-toolbar current-name": "bg:#008888 #ffffff bold", + "signature-toolbar operator": "#000000 bold", "docstring": "#888888", # Validation toolbar. "validation-toolbar": "bg:#440000 #aaaaaa", From 1b528cf5c1b355ba3bd040b0086957dd5072b567 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 28 Jan 2021 12:17:14 +0100 Subject: [PATCH 351/470] Allow display of signature and completion drop down together. --- ptpython/layout.py | 60 +++++++++++++++++++--------------------------- setup.py | 7 +++--- 2 files changed, 27 insertions(+), 40 deletions(-) diff --git a/ptpython/layout.py b/ptpython/layout.py index 3cf3c77d..4c76dbd4 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -230,8 +230,8 @@ def signature_toolbar(python_input): Return the `Layout` for the signature. """ - def get_text_fragments(): - result = [] + def get_text_fragments() -> StyleAndTextTuples: + result: StyleAndTextTuples = [] append = result.append Signature = "class:signature-toolbar" @@ -260,7 +260,7 @@ def get_text_fragments(): # and sig has no 'index' attribute. # See: https://github.com/jonathanslenders/ptpython/issues/47 # https://github.com/davidhalter/jedi/issues/598 - description = p.description if p else "*" # or '*' + description = p.description if p else "*" sig_index = getattr(sig, "index", 0) if i == sig_index: @@ -286,16 +286,8 @@ def get_text_fragments(): filter= # Show only when there is a signature HasSignature(python_input) & - # And there are no completions to be shown. (would cover signature pop-up.) - ~( - has_completions - & ( - show_completions_menu(python_input) - | show_multi_column_completions_menu(python_input) - ) - ) # Signature needs to be shown. - & ShowSignature(python_input) & + ShowSignature(python_input) & # Not done yet. ~is_done, ) @@ -656,33 +648,29 @@ def menu_position(): Float( xcursor=True, ycursor=True, - content=ConditionalContainer( - content=CompletionsMenu( - scroll_offset=( - lambda: python_input.completion_menu_scroll_offset + content=HSplit( + [ + signature_toolbar(python_input), + ConditionalContainer( + content=CompletionsMenu( + scroll_offset=( + lambda: python_input.completion_menu_scroll_offset + ), + max_height=12, + ), + filter=show_completions_menu( + python_input + ), ), - max_height=12, - ), - filter=show_completions_menu( - python_input - ), - ), - ), - Float( - xcursor=True, - ycursor=True, - content=ConditionalContainer( - content=MultiColumnCompletionsMenu(), - filter=show_multi_column_completions_menu( - python_input - ), + ConditionalContainer( + content=MultiColumnCompletionsMenu(), + filter=show_multi_column_completions_menu( + python_input + ), + ), + ] ), ), - Float( - xcursor=True, - ycursor=True, - content=signature_toolbar(python_input), - ), Float( left=2, bottom=1, diff --git a/setup.py b/setup.py index 09643f49..a803ef1a 100644 --- a/setup.py +++ b/setup.py @@ -20,10 +20,9 @@ "appdirs", "importlib_metadata;python_version<'3.8'", "jedi>=0.16.0", - # Use prompt_toolkit 3.0.11, because ptpython now runs the UI in the - # background thread, and we need the terminal size polling that was - # introduced here. - "prompt_toolkit>=3.0.11,<3.1.0", + # Use prompt_toolkit 3.0.12, because of dont_extend_width bugfix when + # signature and completion dropdown are displayed together. + "prompt_toolkit>=3.0.12,<3.1.0", "pygments", "black", ], From 107aba8dea556207389227c6789043e50c65ff95 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 28 Jan 2021 12:18:32 +0100 Subject: [PATCH 352/470] Cleanup of completion code. --- ptpython/completer.py | 205 +++++++++++++++++++++++++----------------- ptpython/style.py | 1 + 2 files changed, 125 insertions(+), 81 deletions(-) diff --git a/ptpython/completer.py b/ptpython/completer.py index aee280f4..af4d1c74 100644 --- a/ptpython/completer.py +++ b/ptpython/completer.py @@ -51,6 +51,7 @@ def __init__( self.get_enable_dictionary_completion = get_enable_dictionary_completion self._system_completer = SystemCompleter() + self._jedi_completer = JediCompleter(get_globals, get_locals) self._dictionary_completer = DictionaryCompleter(get_globals, get_locals) self._path_completer_cache: Optional[GrammarCompleter] = None @@ -129,10 +130,14 @@ def _complete_path_while_typing(self, document: Document) -> bool: ) def _complete_python_while_typing(self, document: Document) -> bool: - char_before_cursor = document.char_before_cursor + """ + When `complete_while_typing` is set, only return completions when this + returns `True`. + """ + text = document.text_before_cursor.rstrip() + char_before_cursor = text[-1:] return bool( - document.text - and (char_before_cursor.isalnum() or char_before_cursor in "_.") + text and (char_before_cursor.isalnum() or char_before_cursor in "_.(,") ) def get_completions( @@ -151,94 +156,127 @@ def get_completions( ) return - # Do dictionary key completions. - if self.get_enable_dictionary_completion(): - has_dict_completions = False - for c in self._dictionary_completer.get_completions( - document, complete_event - ): - if c.text not in "[.": - # If we get the [ or . completion, still include the other - # completions. - has_dict_completions = True - yield c - if has_dict_completions: - return - # Do Path completions (if there were no dictionary completions). if complete_event.completion_requested or self._complete_path_while_typing( document ): yield from self._path_completer.get_completions(document, complete_event) - # If we are inside a string, Don't do Jedi completion. - if self._path_completer_grammar.match(document.text_before_cursor): - return - - # Do Jedi Python completions. if complete_event.completion_requested or self._complete_python_while_typing( document ): - script = get_jedi_script_from_document( - document, self.get_locals(), self.get_globals() - ) + # If we are inside a string, Don't do Python completion. + if self._path_completer_grammar.match(document.text_before_cursor): + return - if script: - try: - jedi_completions = script.complete( - column=document.cursor_position_col, - line=document.cursor_position_row + 1, + # Do dictionary key completions. + if self.get_enable_dictionary_completion(): + has_dict_completions = False + for c in self._dictionary_completer.get_completions( + document, complete_event + ): + if c.text not in "[.": + # If we get the [ or . completion, still include the other + # completions. + has_dict_completions = True + yield c + if has_dict_completions: + return + + # Do Jedi Python completions. + yield from self._jedi_completer.get_completions(document, complete_event) + + +class JediCompleter(Completer): + """ + Autocompleter that uses the Jedi library. + """ + + def __init__(self, get_globals, get_locals) -> None: + super().__init__() + + self.get_globals = get_globals + self.get_locals = get_locals + + def get_completions( + self, document: Document, complete_event: CompleteEvent + ) -> Iterable[Completion]: + script = get_jedi_script_from_document( + document, self.get_locals(), self.get_globals() + ) + + if script: + try: + jedi_completions = script.complete( + column=document.cursor_position_col, + line=document.cursor_position_row + 1, + ) + except TypeError: + # Issue #9: bad syntax causes completions() to fail in jedi. + # https://github.com/jonathanslenders/python-prompt-toolkit/issues/9 + pass + except UnicodeDecodeError: + # Issue #43: UnicodeDecodeError on OpenBSD + # https://github.com/jonathanslenders/python-prompt-toolkit/issues/43 + pass + except AttributeError: + # Jedi issue #513: https://github.com/davidhalter/jedi/issues/513 + pass + except ValueError: + # Jedi issue: "ValueError: invalid \x escape" + pass + except KeyError: + # Jedi issue: "KeyError: u'a_lambda'." + # https://github.com/jonathanslenders/ptpython/issues/89 + pass + except IOError: + # Jedi issue: "IOError: No such file or directory." + # https://github.com/jonathanslenders/ptpython/issues/71 + pass + except AssertionError: + # In jedi.parser.__init__.py: 227, in remove_last_newline, + # the assertion "newline.value.endswith('\n')" can fail. + pass + except SystemError: + # In jedi.api.helpers.py: 144, in get_stack_at_position + # raise SystemError("This really shouldn't happen. There's a bug in Jedi.") + pass + except NotImplementedError: + # See: https://github.com/jonathanslenders/ptpython/issues/223 + pass + except Exception: + # Supress all other Jedi exceptions. + pass + else: + # Move function parameters to the top. + jedi_completions = sorted( + jedi_completions, + key=lambda jc: ( + # Params first. + jc.type != "param", + # Private at the end. + jc.name.startswith("_"), + # Then sort by name. + jc.name_with_symbols.lower(), + ), + ) + + for jc in jedi_completions: + if jc.type == "function": + suffix = "()" + else: + suffix = "" + + if jc.type == "param": + suffix = "..." + + yield Completion( + jc.name_with_symbols, + len(jc.complete) - len(jc.name_with_symbols), + display=jc.name_with_symbols + suffix, + display_meta=jc.type, + style=_get_style_for_jedi_completion(jc), ) - except TypeError: - # Issue #9: bad syntax causes completions() to fail in jedi. - # https://github.com/jonathanslenders/python-prompt-toolkit/issues/9 - pass - except UnicodeDecodeError: - # Issue #43: UnicodeDecodeError on OpenBSD - # https://github.com/jonathanslenders/python-prompt-toolkit/issues/43 - pass - except AttributeError: - # Jedi issue #513: https://github.com/davidhalter/jedi/issues/513 - pass - except ValueError: - # Jedi issue: "ValueError: invalid \x escape" - pass - except KeyError: - # Jedi issue: "KeyError: u'a_lambda'." - # https://github.com/jonathanslenders/ptpython/issues/89 - pass - except IOError: - # Jedi issue: "IOError: No such file or directory." - # https://github.com/jonathanslenders/ptpython/issues/71 - pass - except AssertionError: - # In jedi.parser.__init__.py: 227, in remove_last_newline, - # the assertion "newline.value.endswith('\n')" can fail. - pass - except SystemError: - # In jedi.api.helpers.py: 144, in get_stack_at_position - # raise SystemError("This really shouldn't happen. There's a bug in Jedi.") - pass - except NotImplementedError: - # See: https://github.com/jonathanslenders/ptpython/issues/223 - pass - except Exception: - # Supress all other Jedi exceptions. - pass - else: - for jc in jedi_completions: - if jc.type == "function": - suffix = "()" - else: - suffix = "" - - yield Completion( - jc.name_with_symbols, - len(jc.complete) - len(jc.name_with_symbols), - display=jc.name_with_symbols + suffix, - display_meta=jc.type, - style=_get_style_for_name(jc.name_with_symbols), - ) class DictionaryCompleter(Completer): @@ -575,10 +613,15 @@ class ReprFailedError(Exception): _builtin_names = [] -def _get_style_for_name(name: str) -> str: +def _get_style_for_jedi_completion(jedi_completion) -> str: """ Return completion style to use for this name. """ + name = jedi_completion.name_with_symbols + + if jedi_completion.type == "param": + return "class:completion.param" + if name in _builtin_names: return "class:completion.builtin" diff --git a/ptpython/style.py b/ptpython/style.py index 23e51c7e..4b54d0cd 100644 --- a/ptpython/style.py +++ b/ptpython/style.py @@ -75,6 +75,7 @@ def generate_style(python_style: BaseStyle, ui_style: BaseStyle) -> BaseStyle: "out.number": "#ff0000", # Completions. "completion.builtin": "", + "completion.param": "#006666 italic", "completion.keyword": "fg:#008800", "completion.keyword fuzzymatch.inside": "fg:#008800", "completion.keyword fuzzymatch.outside": "fg:#44aa44", From 2238d412952d43dd9a2dfbe30a42b02a1a115c28 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 28 Jan 2021 12:27:32 +0100 Subject: [PATCH 353/470] Improve signature pop-up. --- ptpython/layout.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ptpython/layout.py b/ptpython/layout.py index 4c76dbd4..3496b60f 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -240,7 +240,7 @@ def get_text_fragments() -> StyleAndTextTuples: append((Signature, " ")) try: - append((Signature, sig.full_name)) + append((Signature, sig.name)) except IndexError: # Workaround for #37: https://github.com/jonathanslenders/python-prompt-toolkit/issues/37 # See also: https://github.com/davidhalter/jedi/issues/490 @@ -260,7 +260,7 @@ def get_text_fragments() -> StyleAndTextTuples: # and sig has no 'index' attribute. # See: https://github.com/jonathanslenders/ptpython/issues/47 # https://github.com/davidhalter/jedi/issues/598 - description = p.description if p else "*" + description = p.to_string() if p else "*" sig_index = getattr(sig, "index", 0) if i == sig_index: From bf991c67a3b9a46c7fbd73085ca58d79f99ab995 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 28 Jan 2021 13:59:36 +0100 Subject: [PATCH 354/470] Better signature abstractions. Retrieve signatures without Jedi when Jedi fails. --- ptpython/completer.py | 24 +++- ptpython/layout.py | 43 ++++--- ptpython/python_input.py | 48 +++----- ptpython/signatures.py | 238 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 296 insertions(+), 57 deletions(-) create mode 100644 ptpython/signatures.py diff --git a/ptpython/completer.py b/ptpython/completer.py index af4d1c74..68774be4 100644 --- a/ptpython/completer.py +++ b/ptpython/completer.py @@ -157,6 +157,7 @@ def get_completions( return # Do Path completions (if there were no dictionary completions). + # TODO: not if we have dictionary completions... if complete_event.completion_requested or self._complete_path_while_typing( document ): @@ -383,7 +384,7 @@ def get_completions( self, document: Document, complete_event: CompleteEvent ) -> Iterable[Completion]: - # First, find all for-loops, and assing the first item of the + # First, find all for-loops, and assign the first item of the # collections they're iterating to the iterator variable, so that we # can provide code completion on the iterators. temp_locals = self.get_locals().copy() @@ -414,6 +415,17 @@ def _do_repr(self, obj: object) -> str: except BaseException: raise ReprFailedError + def eval_expression(self, document: Document, locals: Dict[str, Any]) -> object: + """ + Evaluate + """ + match = self.expression_pattern.search(document.text_before_cursor) + if match is not None: + object_var = match.groups()[0] + return self._lookup(object_var, locals) + + return None + def _get_expression_completions( self, document: Document, @@ -423,17 +435,17 @@ def _get_expression_completions( """ Complete the [ or . operator after an object. """ - match = self.expression_pattern.search(document.text_before_cursor) - if match is not None: - object_var = match.groups()[0] - result = self._lookup(object_var, temp_locals) + result = self.eval_expression(document, temp_locals) + + if result is not None: if isinstance( result, (list, tuple, dict, collections_abc.Mapping, collections_abc.Sequence), ): yield Completion("[", 0) - elif result is not None: + + else: # Note: Don't call `if result` here. That can fail for types # that have custom truthness checks. yield Completion(".", 0) diff --git a/ptpython/layout.py b/ptpython/layout.py index 3496b60f..b12010ce 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -4,13 +4,13 @@ import platform import sys from enum import Enum +from inspect import _ParameterKind as ParameterKind from typing import TYPE_CHECKING, Optional from prompt_toolkit.application import get_app from prompt_toolkit.enums import DEFAULT_BUFFER, SEARCH_BUFFER from prompt_toolkit.filters import ( Condition, - has_completions, has_focus, is_done, renderer_height_is_known, @@ -248,30 +248,41 @@ def get_text_fragments() -> StyleAndTextTuples: append((Signature + ",operator", "(")) - try: - enumerated_params = enumerate(sig.params) - except AttributeError: - # Workaround for #136: https://github.com/jonathanslenders/ptpython/issues/136 - # AttributeError: 'Lambda' object has no attribute 'get_subscope_by_name' - return [] + got_positional_only = False + got_keyword_only = False + + for i, p in enumerate(sig.parameters): + # Detect transition between positional-only and not positional-only. + if p.kind == ParameterKind.POSITIONAL_ONLY: + got_positional_only = True + if got_positional_only and p.kind != ParameterKind.POSITIONAL_ONLY: + got_positional_only = False + append((Signature, "/")) + append((Signature + ",operator", ", ")) - for i, p in enumerated_params: - # Workaround for #47: 'p' is None when we hit the '*' in the signature. - # and sig has no 'index' attribute. - # See: https://github.com/jonathanslenders/ptpython/issues/47 - # https://github.com/davidhalter/jedi/issues/598 - description = p.to_string() if p else "*" + if not got_keyword_only and p.kind == ParameterKind.KEYWORD_ONLY: + got_keyword_only = True + append((Signature, "*")) + append((Signature + ",operator", ", ")) + + description = p.name sig_index = getattr(sig, "index", 0) if i == sig_index: # Note: we use `_Param.description` instead of # `_Param.name`, that way we also get the '*' before args. - append((Signature + ",current-name", str(description))) + append((Signature + ",current-name", description)) else: append((Signature, str(description))) + + if p.default: + # NOTE: For the jedi-based completion, the default is + # currently still part of the name. + append((Signature, f"={p.default}")) + append((Signature + ",operator", ", ")) - if sig.params: + if sig.parameters: # Pop last comma result.pop() @@ -577,7 +588,7 @@ def menu_position(): """ b = python_input.default_buffer - if b.complete_state is None and python_input.signatures: + if python_input.signatures: row, col = python_input.signatures[0].bracket_start index = b.document.translate_row_col_to_index(row - 1, col) return index diff --git a/ptpython/python_input.py b/ptpython/python_input.py index c84c80f9..8d5da502 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -66,8 +66,9 @@ from .layout import CompletionVisualisation, PtPythonLayout from .lexer import PtpythonLexer from .prompt_style import ClassicPrompt, IPythonPrompt, PromptStyle +from .signatures import Signature, get_signatures_using_eval, get_signatures_using_jedi from .style import generate_style, get_all_code_styles, get_all_ui_styles -from .utils import get_jedi_script_from_document, unindent_code +from .utils import unindent_code from .validator import PythonValidator __all__ = ["PythonInput"] @@ -260,7 +261,7 @@ def __init__( self.enable_syntax_highlighting: bool = True self.enable_fuzzy_completion: bool = False - self.enable_dictionary_completion: bool = False + self.enable_dictionary_completion: bool = False # Also eval-based completion. self.complete_private_attributes: CompletePrivateAttributes = ( CompletePrivateAttributes.ALWAYS ) @@ -330,7 +331,7 @@ def __init__( self.current_statement_index: int = 1 # Code signatures. (This is set asynchronously after a timeout.) - self.signatures: List[Any] = [] + self.signatures: List[Signature] = [] # Boolean indicating whether we have a signatures thread running. # (Never run more than one at the same time.) @@ -917,36 +918,16 @@ def _on_input_timeout(self, buff: Buffer, loop=None) -> None: loop = loop or get_event_loop() def run(): - script = get_jedi_script_from_document( + # First, get signatures from Jedi. If we didn't found any and if + # "dictionary completion" (eval-based completion) is enabled, then + # get signatures using eval. + signatures = get_signatures_using_jedi( document, self.get_locals(), self.get_globals() ) - - # Show signatures in help text. - if script: - try: - signatures = script.get_signatures() - except ValueError: - # e.g. in case of an invalid \\x escape. - signatures = [] - except Exception: - # Sometimes we still get an exception (TypeError), because - # of probably bugs in jedi. We can silence them. - # See: https://github.com/davidhalter/jedi/issues/492 - signatures = [] - else: - # Try to access the params attribute just once. For Jedi - # signatures containing the keyword-only argument star, - # this will crash when retrieving it the first time with - # AttributeError. Every following time it works. - # See: https://github.com/jonathanslenders/ptpython/issues/47 - # https://github.com/davidhalter/jedi/issues/598 - try: - if signatures: - signatures[0].params - except AttributeError: - pass - else: - signatures = [] + if not signatures and self.enable_dictionary_completion: + signatures = get_signatures_using_eval( + document, self.get_locals(), self.get_globals() + ) self._get_signatures_thread_running = False @@ -957,11 +938,8 @@ def run(): # Set docstring in docstring buffer. if signatures: - string = signatures[0].docstring() - if not isinstance(string, str): - string = string.decode("utf-8") self.docstring_buffer.reset( - document=Document(string, cursor_position=0) + document=Document(signatures[0].docstring, cursor_position=0) ) else: self.docstring_buffer.reset() diff --git a/ptpython/signatures.py b/ptpython/signatures.py new file mode 100644 index 00000000..39cdba29 --- /dev/null +++ b/ptpython/signatures.py @@ -0,0 +1,238 @@ +""" +Helpers for retrieving the function signature of the function call that we are +editing. + +Either with the Jedi library, or using `inspect.signature` if Jedi fails and we +can use `eval()` to evaluate the function object. +""" +import inspect +from inspect import Signature as InspectSignature +from inspect import _ParameterKind as ParameterKind +from typing import Any, Dict, List, Optional, Sequence, Tuple + +from prompt_toolkit.document import Document + +from .completer import DictionaryCompleter +from .utils import get_jedi_script_from_document + +__all__ = ["Signature", "get_signatures_using_jedi", "get_signatures_using_eval"] + + +class Parameter: + def __init__( + self, + name: str, + annotation: Optional[str], + default: Optional[str], + kind: ParameterKind, + ) -> None: + self.name = name + self.kind = kind + + self.annotation = annotation + self.default = default + + def __repr__(self) -> str: + return f"Parameter(name={self.name!r})" + + +class Signature: + """ + Signature definition used wrap around both Jedi signatures and + python-inspect signatures. + + :param index: Parameter index of the current cursor position. + :param bracket_start: (line, column) tuple for the open bracket that starts + the function call. + """ + + def __init__( + self, + name: str, + docstring: str, + parameters: Sequence[Parameter], + index: Optional[int] = None, + returns: str = "", + bracket_start: Tuple[int, int] = (0, 0), + ) -> None: + self.name = name + self.docstring = docstring + self.parameters = parameters + self.index = index + self.returns = returns + self.bracket_start = bracket_start + + @classmethod + def from_inspect_signature( + cls, + name: str, + docstring: str, + signature: InspectSignature, + index: int, + ) -> "Signature": + parameters = [] + for p in signature.parameters.values(): + parameters.append( + Parameter( + name=p.name, + annotation=p.annotation.__name__, + default=repr(p.default) + if p.default is not inspect.Parameter.empty + else None, + kind=p.kind, + ) + ) + + return cls( + name=name, + docstring=docstring, + parameters=parameters, + index=index, + returns="", + ) + + @classmethod + def from_jedi_signature(cls, signature) -> "Signature": + parameters = [] + + for p in signature.params: + if p is None: + # We just hit the "*". + continue + + parameters.append( + Parameter( + name=p.to_string(), # p.name, + annotation=None, # p.infer_annotation() + default=None, # p.infer_default() + kind=p.kind, + ) + ) + + docstring = signature.docstring() + if not isinstance(docstring, str): + docstring = docstring.decode("utf-8") + + return cls( + name=signature.name, + docstring=docstring, + parameters=parameters, + index=signature.index, + returns="", + bracket_start=signature.bracket_start, + ) + + def __repr__(self) -> str: + return f"Signature({self.name!r}, parameters={self.parameters!r})" + + +def get_signatures_using_jedi( + document: Document, locals: Dict[str, Any], globals: Dict[str, Any] +) -> List[Signature]: + script = get_jedi_script_from_document(document, locals, globals) + + # Show signatures in help text. + if not script: + return [] + + try: + signatures = script.get_signatures() + except ValueError: + # e.g. in case of an invalid \\x escape. + signatures = [] + except Exception: + # Sometimes we still get an exception (TypeError), because + # of probably bugs in jedi. We can silence them. + # See: https://github.com/davidhalter/jedi/issues/492 + signatures = [] + else: + # Try to access the params attribute just once. For Jedi + # signatures containing the keyword-only argument star, + # this will crash when retrieving it the first time with + # AttributeError. Every following time it works. + # See: https://github.com/jonathanslenders/ptpython/issues/47 + # https://github.com/davidhalter/jedi/issues/598 + try: + if signatures: + signatures[0].params + except AttributeError: + pass + + return [Signature.from_jedi_signature(sig) for sig in signatures] + + +def get_signatures_using_eval( + document: Document, locals: Dict[str, Any], globals: Dict[str, Any] +) -> List[Signature]: + """ + Look for the signature of the function before the cursor position without + use of Jedi. This uses a similar approach as the `DictionaryCompleter` of + running `eval()` over the detected function name. + """ + # Look for open parenthesis, before cursor position. + text = document.text_before_cursor + pos = document.cursor_position - 1 + + paren_mapping = {")": "(", "}": "{", "]": "["} + paren_stack = [ + ")" + ] # Start stack with closing ')'. We are going to look for the matching open ')'. + comma_count = 0 # Number of comma's between start of function call and cursor pos. + found_start = False # Found something. + + while pos >= 0: + char = document.text[pos] + if char in ")]}": + paren_stack.append(char) + elif char in "([{": + if not paren_stack: + # Open paren, while no closing paren was found. Mouse cursor is + # positioned in nested parentheses. Not at the "top-level" of a + # function call. + break + if paren_mapping[paren_stack[-1]] != char: + # Unmatching parentheses: syntax error? + break + + paren_stack.pop() + + if len(paren_stack) == 0: + found_start = True + break + + elif char == "," and len(paren_stack) == 1: + comma_count += 1 + + pos -= 1 + + if not found_start: + return [] + + # We found the start of the function call. Now look for the object before + # this position on which we can do an 'eval' to retrieve the function + # object. + obj = DictionaryCompleter(lambda: globals, lambda: locals).eval_expression( + Document(document.text, cursor_position=pos), locals + ) + if obj is None: + return [] + + try: + name = obj.__name__ # type:ignore + except Exception: + name = obj.__class__.__name__ + + try: + signature = inspect.signature(obj) # type: ignore + except TypeError: + return [] # Not a callable object. + except ValueError: + return [] # No signature found, like for build-ins like "print". + + try: + doc = obj.__doc__ or "" + except: + doc = "" + + # TODO: `index` is not yet correct when dealing with keyword-only arguments. + return [Signature.from_inspect_signature(name, doc, signature, index=comma_count)] From 3a389b702e5b6d08016d1cd05987802c981aabe3 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Fri, 5 Feb 2021 17:24:11 +0100 Subject: [PATCH 355/470] Hide signature when sidebar is visible. --- ptpython/layout.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ptpython/layout.py b/ptpython/layout.py index b12010ce..7c3739ff 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -299,6 +299,8 @@ def get_text_fragments() -> StyleAndTextTuples: HasSignature(python_input) & # Signature needs to be shown. ShowSignature(python_input) & + # And no sidebar is visible. + ~ShowSidebar(python_input) & # Not done yet. ~is_done, ) From b0e761966237867ccfb392efaba3e0d41183bc85 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Wed, 10 Feb 2021 18:51:44 +0100 Subject: [PATCH 356/470] Release 3.0.14 --- CHANGELOG | 19 +++++++++++++++++++ setup.py | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 5b704461..abca054c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,25 @@ CHANGELOG ========= +3.0.14: 2020-02-10 +------------------ + +New features: +- Display of signature and completion drop down together. +- If `DictionaryCompleter` is enabled, also retrieve signatures when Jedi + fails, using the same logic. +- List function parameters first and private attributes at the end in the + completion menu. +- Cleanup of the completion code. + +Fixes: +- Handle exceptions raised when `repr()` is called. +- Fix leakage of `exc_info` from eval to exec call. +- Fix handling of `KeyboardInterrupt` in REPL during evaluation of `__repr__`. +- Fix style for signature toolbar. +- Hide signature when sidebar is visible. + + 3.0.13: 2020-01-26 ------------------ diff --git a/setup.py b/setup.py index a803ef1a..d17bdb4d 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ setup( name="ptpython", author="Jonathan Slenders", - version="3.0.13", + version="3.0.14", url="https://github.com/prompt-toolkit/ptpython", description="Python REPL build on top of prompt_toolkit", long_description=long_description, From 869e4fa08a6b90d1d104b0729bf188cef834fe1e Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 11 Feb 2021 10:45:03 +0100 Subject: [PATCH 357/470] Fix AttributeError in when retrieving signatures that contain type annotations. --- ptpython/layout.py | 5 ++--- ptpython/signatures.py | 32 ++++++++++++++++++++++++++++++-- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/ptpython/layout.py b/ptpython/layout.py index 7c3739ff..6482cbd0 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -265,15 +265,14 @@ def get_text_fragments() -> StyleAndTextTuples: append((Signature, "*")) append((Signature + ",operator", ", ")) - description = p.name sig_index = getattr(sig, "index", 0) if i == sig_index: # Note: we use `_Param.description` instead of # `_Param.name`, that way we also get the '*' before args. - append((Signature + ",current-name", description)) + append((Signature + ",current-name", p.description)) else: - append((Signature, str(description))) + append((Signature, p.description)) if p.default: # NOTE: For the jedi-based completion, the default is diff --git a/ptpython/signatures.py b/ptpython/signatures.py index 39cdba29..228b99b2 100644 --- a/ptpython/signatures.py +++ b/ptpython/signatures.py @@ -35,6 +35,18 @@ def __init__( def __repr__(self) -> str: return f"Parameter(name={self.name!r})" + @property + def description(self) -> str: + """ + Name + annotation. + """ + description = self.name + + if self.annotation is not None: + description += f": {self.annotation}" + + return description + class Signature: """ @@ -71,11 +83,27 @@ def from_inspect_signature( index: int, ) -> "Signature": parameters = [] + + def get_annotation_name(annotation: object) -> str: + """ + Get annotation as string from inspect signature. + """ + try: + # In case the annotation is a class like "int", "float", ... + return str(annotation.__name__) # type: ignore + except AttributeError: + pass # No attribute `__name__`, e.g., in case of `List[int]`. + + annotation = str(annotation) + if annotation.startswith("typing."): + annotation = annotation[len("typing:") :] + return annotation + for p in signature.parameters.values(): parameters.append( Parameter( name=p.name, - annotation=p.annotation.__name__, + annotation=get_annotation_name(p.annotation), default=repr(p.default) if p.default is not inspect.Parameter.empty else None, @@ -102,7 +130,7 @@ def from_jedi_signature(cls, signature) -> "Signature": parameters.append( Parameter( - name=p.to_string(), # p.name, + name=p.to_string(), # p.name, (`to_string()` already includes the annotation). annotation=None, # p.infer_annotation() default=None, # p.infer_default() kind=p.kind, From 1246deb1517a4e0d3d4fa599d87cd11b8f910a21 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 11 Feb 2021 12:06:19 +0100 Subject: [PATCH 358/470] Only delete 'word' instead of 'WORD' before cursor when control-w is pressed. (Stopping at any punctiation character is more logical when editing Python code.) --- ptpython/key_bindings.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ptpython/key_bindings.py b/ptpython/key_bindings.py index b01762e6..86317f90 100644 --- a/ptpython/key_bindings.py +++ b/ptpython/key_bindings.py @@ -10,6 +10,7 @@ vi_insert_mode, ) from prompt_toolkit.key_binding import KeyBindings +from prompt_toolkit.key_binding.bindings.named_commands import get_by_name from prompt_toolkit.keys import Keys from .utils import document_is_multiline_python @@ -61,6 +62,10 @@ def _(event): if python_input.enable_system_bindings: event.app.suspend_to_background() + # Delete word before cursor, but use all Python symbols as separators + # (WORD=False). + handle("c-w")(get_by_name("backward-kill-word")) + @handle("f2") def _(event): """ From 7f619ed7d5774c44193781b7f029fd64b5855475 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 11 Feb 2021 12:02:26 +0100 Subject: [PATCH 359/470] Several fixes to the completion code. - Give dictionary completions priority over path completions. - Always call non-fuzzy completer after fuzzy completer to prevent that some completions were missed out if the fuzzy completer doesn't find them. --- ptpython/completer.py | 45 +++++++++++++++++++++++----------------- ptpython/python_input.py | 19 ++++++++++++++--- setup.py | 5 ++--- 3 files changed, 44 insertions(+), 25 deletions(-) diff --git a/ptpython/completer.py b/ptpython/completer.py index 68774be4..9f7e10bc 100644 --- a/ptpython/completer.py +++ b/ptpython/completer.py @@ -42,13 +42,16 @@ class PythonCompleter(Completer): """ def __init__( - self, get_globals, get_locals, get_enable_dictionary_completion + self, + get_globals: Callable[[], dict], + get_locals: Callable[[], dict], + enable_dictionary_completion: Callable[[], bool], ) -> None: super().__init__() self.get_globals = get_globals self.get_locals = get_locals - self.get_enable_dictionary_completion = get_enable_dictionary_completion + self.enable_dictionary_completion = enable_dictionary_completion self._system_completer = SystemCompleter() self._jedi_completer = JediCompleter(get_globals, get_locals) @@ -134,10 +137,10 @@ def _complete_python_while_typing(self, document: Document) -> bool: When `complete_while_typing` is set, only return completions when this returns `True`. """ - text = document.text_before_cursor.rstrip() + text = document.text_before_cursor # .rstrip() char_before_cursor = text[-1:] return bool( - text and (char_before_cursor.isalnum() or char_before_cursor in "_.(,") + text and (char_before_cursor.isalnum() or char_before_cursor in "_.([,") ) def get_completions( @@ -156,22 +159,11 @@ def get_completions( ) return - # Do Path completions (if there were no dictionary completions). - # TODO: not if we have dictionary completions... - if complete_event.completion_requested or self._complete_path_while_typing( - document - ): - yield from self._path_completer.get_completions(document, complete_event) - + # Do dictionary key completions. if complete_event.completion_requested or self._complete_python_while_typing( document ): - # If we are inside a string, Don't do Python completion. - if self._path_completer_grammar.match(document.text_before_cursor): - return - - # Do dictionary key completions. - if self.get_enable_dictionary_completion(): + if self.enable_dictionary_completion(): has_dict_completions = False for c in self._dictionary_completer.get_completions( document, complete_event @@ -184,8 +176,23 @@ def get_completions( if has_dict_completions: return - # Do Jedi Python completions. - yield from self._jedi_completer.get_completions(document, complete_event) + # Do Path completions (if there were no dictionary completions). + if complete_event.completion_requested or self._complete_path_while_typing( + document + ): + yield from self._path_completer.get_completions(document, complete_event) + + # Do Jedi completions. + if complete_event.completion_requested or self._complete_python_while_typing( + document + ): + # If we are inside a string, Don't do Jedi completion. + if not self._path_completer_grammar.match(document.text_before_cursor): + + # Do Jedi Python completions. + yield from self._jedi_completer.get_completions( + document, complete_event + ) class JediCompleter(Completer): diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 8d5da502..e63cdf1d 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -18,9 +18,11 @@ from prompt_toolkit.buffer import Buffer from prompt_toolkit.completion import ( Completer, + ConditionalCompleter, DynamicCompleter, FuzzyCompleter, ThreadedCompleter, + merge_completers, ) from prompt_toolkit.document import Document from prompt_toolkit.enums import DEFAULT_BUFFER, EditingMode @@ -205,9 +207,20 @@ def __init__( ) self._completer = HidePrivateCompleter( - FuzzyCompleter( - DynamicCompleter(lambda: self.completer), - enable_fuzzy=Condition(lambda: self.enable_fuzzy_completion), + # If fuzzy is enabled, first do fuzzy completion, but always add + # the non-fuzzy completions, if somehow the fuzzy completer didn't + # find them. (Due to the way the cursor position is moved in the + # fuzzy completer, some completions will not always be found by the + # fuzzy completer, but will be found with the normal completer.) + merge_completers( + [ + ConditionalCompleter( + FuzzyCompleter(DynamicCompleter(lambda: self.completer)), + Condition(lambda: self.enable_fuzzy_completion), + ), + DynamicCompleter(lambda: self.completer), + ], + deduplicate=True, ), lambda: self.complete_private_attributes, ) diff --git a/setup.py b/setup.py index d17bdb4d..57fe2030 100644 --- a/setup.py +++ b/setup.py @@ -20,9 +20,8 @@ "appdirs", "importlib_metadata;python_version<'3.8'", "jedi>=0.16.0", - # Use prompt_toolkit 3.0.12, because of dont_extend_width bugfix when - # signature and completion dropdown are displayed together. - "prompt_toolkit>=3.0.12,<3.1.0", + # Use prompt_toolkit 3.0.16, because of the `DeduplicateCompleter`. + "prompt_toolkit>=3.0.16,<3.1.0", "pygments", "black", ], From 02a7b835623cb6f8c0aa65584fbe786fff2ed10a Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 11 Feb 2021 12:30:54 +0100 Subject: [PATCH 360/470] Release 3.0.15 --- CHANGELOG | 10 ++++++++++ setup.py | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index abca054c..6d48ade2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,16 @@ CHANGELOG ========= +3.0.15: 2020-02-11 +------------------ + +New features: +- When pressing control-w, only delete characters until a punctuation. + +Fixes: +- Fix `AttributeError` during retrieval of signatures with type annotations. + + 3.0.14: 2020-02-10 ------------------ diff --git a/setup.py b/setup.py index 57fe2030..b4a4b683 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ setup( name="ptpython", author="Jonathan Slenders", - version="3.0.14", + version="3.0.15", url="https://github.com/prompt-toolkit/ptpython", description="Python REPL build on top of prompt_toolkit", long_description=long_description, From caff15a461b64dee36c22608d36170830cafd5f3 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 11 Feb 2021 12:34:06 +0100 Subject: [PATCH 361/470] Release 3.0.16 --- CHANGELOG | 12 ++++++++++++ setup.py | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 6d48ade2..67ac0a85 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,18 @@ CHANGELOG ========= +3.0.16: 2020-02-11 +------------------ + +(Commit 7f619e was missing in previous release.) + +Fixes: +- Several fixes to the completion code: + * Give dictionary completions priority over path completions. + * Always call non-fuzzy completer after fuzzy completer to prevent that some + completions were missed out if the fuzzy completer doesn't find them. + + 3.0.15: 2020-02-11 ------------------ diff --git a/setup.py b/setup.py index b4a4b683..dbbe55b9 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ setup( name="ptpython", author="Jonathan Slenders", - version="3.0.15", + version="3.0.16", url="https://github.com/prompt-toolkit/ptpython", description="Python REPL build on top of prompt_toolkit", long_description=long_description, From b222d03f3c83f106003f23527dc55f0eaf514776 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Fri, 19 Mar 2021 12:48:24 +0100 Subject: [PATCH 362/470] Fix leaking file descriptors. --- ptpython/python_input.py | 60 ++++++++++++++-------------------------- ptpython/repl.py | 10 +------ setup.py | 4 +-- 3 files changed, 24 insertions(+), 50 deletions(-) diff --git a/ptpython/python_input.py b/ptpython/python_input.py index e63cdf1d..d5f03738 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -1012,43 +1012,25 @@ def pre_run( self.app.vi_state.input_mode = InputMode.NAVIGATION # Run the UI. - result: str = "" - exception: Optional[BaseException] = None - - def in_thread() -> None: - nonlocal result, exception + while True: try: - while True: - try: - result = self.app.run(pre_run=pre_run) - - if result.lstrip().startswith("\x1a"): - # When the input starts with Ctrl-Z, quit the REPL. - # (Important for Windows users.) - raise EOFError - - # Remove leading whitespace. - # (Users can add extra indentation, which happens for - # instance because of copy/pasting code.) - result = unindent_code(result) - - if result and not result.isspace(): - return - except KeyboardInterrupt: - # Abort - try again. - self.default_buffer.document = Document() - except BaseException as e: - exception = e - return - - finally: - if self.insert_blank_line_after_input: - self.app.output.write("\n") - - thread = threading.Thread(target=in_thread) - thread.start() - thread.join() - - if exception is not None: - raise exception - return result + result = self.app.run(pre_run=pre_run, in_thread=True) + + if result.lstrip().startswith("\x1a"): + # When the input starts with Ctrl-Z, quit the REPL. + # (Important for Windows users.) + raise EOFError + + # Remove leading whitespace. + # (Users can add extra indentation, which happens for + # instance because of copy/pasting code.) + result = unindent_code(result) + + if result and not result.isspace(): + if self.insert_blank_line_after_input: + self.app.output.write("\n") + + return result + except KeyboardInterrupt: + # Abort - try again. + self.default_buffer.document = Document() diff --git a/ptpython/repl.py b/ptpython/repl.py index ae7b1d0d..f960f6f6 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -421,15 +421,7 @@ def show_pager() -> None: # Run pager prompt in another thread. # Same as for the input. This prevents issues with nested event # loops. - pager_result = None - - def in_thread() -> None: - nonlocal pager_result - pager_result = pager_prompt.prompt() - - th = threading.Thread(target=in_thread) - th.start() - th.join() + pager_result = pager_prompt.prompt(in_thread=True) if pager_result == PagerResult.ABORT: print("...") diff --git a/setup.py b/setup.py index dbbe55b9..c5f40f35 100644 --- a/setup.py +++ b/setup.py @@ -20,8 +20,8 @@ "appdirs", "importlib_metadata;python_version<'3.8'", "jedi>=0.16.0", - # Use prompt_toolkit 3.0.16, because of the `DeduplicateCompleter`. - "prompt_toolkit>=3.0.16,<3.1.0", + # Use prompt_toolkit 3.0.18, because of the `in_thread` option. + "prompt_toolkit>=3.0.18,<3.1.0", "pygments", "black", ], From 7d116e84909d13832935ab4132eabfe316d26b74 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Mon, 22 Mar 2021 15:43:47 +0100 Subject: [PATCH 363/470] Fix race condition during retrieval of signatures. `_on_input_timeout` was called recursively from within another thread, while it was not thread safe. --- ptpython/python_input.py | 64 +++++++++++++++++++++++----------------- ptpython/repl.py | 5 ++-- 2 files changed, 39 insertions(+), 30 deletions(-) diff --git a/ptpython/python_input.py b/ptpython/python_input.py index d5f03738..2f6a5b9f 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -4,7 +4,6 @@ """ import __future__ -import threading from asyncio import get_event_loop from functools import partial from typing import TYPE_CHECKING, Any, Callable, Dict, Generic, List, Optional, TypeVar @@ -914,23 +913,13 @@ def vi_mode(self, value: bool) -> None: else: self.editing_mode = EditingMode.EMACS - def _on_input_timeout(self, buff: Buffer, loop=None) -> None: + def _on_input_timeout(self, buff: Buffer) -> None: """ When there is no input activity, in another thread, get the signature of the current code. """ - app = self.app - - # Never run multiple get-signature threads. - if self._get_signatures_thread_running: - return - self._get_signatures_thread_running = True - - document = buff.document - - loop = loop or get_event_loop() - def run(): + def get_signatures_in_executor(document: Document) -> List[Signature]: # First, get signatures from Jedi. If we didn't found any and if # "dictionary completion" (eval-based completion) is enabled, then # get signatures using eval. @@ -942,26 +931,47 @@ def run(): document, self.get_locals(), self.get_globals() ) - self._get_signatures_thread_running = False + return signatures + + app = self.app + + async def on_timeout_task() -> None: + loop = get_event_loop() - # Set signatures and redraw if the text didn't change in the - # meantime. Otherwise request new signatures. - if buff.text == document.text: - self.signatures = signatures + # Never run multiple get-signature threads. + if self._get_signatures_thread_running: + return + self._get_signatures_thread_running = True - # Set docstring in docstring buffer. - if signatures: - self.docstring_buffer.reset( - document=Document(signatures[0].docstring, cursor_position=0) + try: + while True: + document = buff.document + signatures = await loop.run_in_executor( + None, get_signatures_in_executor, document ) - else: - self.docstring_buffer.reset() - app.invalidate() + # If the text didn't change in the meantime, take these + # signatures. Otherwise, try again. + if buff.text == document.text: + break + finally: + self._get_signatures_thread_running = False + + # Set signatures and redraw. + self.signatures = signatures + + # Set docstring in docstring buffer. + if signatures: + self.docstring_buffer.reset( + document=Document(signatures[0].docstring, cursor_position=0) + ) else: - self._on_input_timeout(buff, loop=loop) + self.docstring_buffer.reset() + + app.invalidate() - loop.run_in_executor(None, run) + if app.is_running: + app.create_background_task(on_timeout_task()) def on_reset(self) -> None: self.signatures = [] diff --git a/ptpython/repl.py b/ptpython/repl.py index f960f6f6..af73cb69 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -11,7 +11,6 @@ import builtins import os import sys -import threading import traceback import types import warnings @@ -112,7 +111,7 @@ def run(self) -> None: # Eval. try: result = self.eval(text) - except KeyboardInterrupt as e: # KeyboardInterrupt doesn't inherit from Exception. + except KeyboardInterrupt: # KeyboardInterrupt doesn't inherit from Exception. raise except SystemExit: return @@ -171,7 +170,7 @@ async def run_async(self) -> None: # Eval. try: result = await self.eval_async(text) - except KeyboardInterrupt as e: # KeyboardInterrupt doesn't inherit from Exception. + except KeyboardInterrupt: # KeyboardInterrupt doesn't inherit from Exception. raise except SystemExit: return From 321d9e177d42de782790740927a4e7a45c3cb7b3 Mon Sep 17 00:00:00 2001 From: AnthonyDiGirolamo Date: Sun, 18 Apr 2021 19:14:49 -0700 Subject: [PATCH 364/470] Support using ptpython as a library. This change allows another fullscreen prompt_toolkit application to create it's own ptpython based repl embedded in a window. It separates the print and format output parts and run functionality into se - Create _format_result_output and _format_exception_output functions to separate the format result from the print to stdout. - Move the eval parts of run() and run_async() into their own functions: run_and_show_expression() and run_and_show_expression_async(). --- ptpython/layout.py | 4 +- ptpython/python_input.py | 27 ++++++-- ptpython/repl.py | 137 +++++++++++++++++++++++---------------- 3 files changed, 105 insertions(+), 63 deletions(-) diff --git a/ptpython/layout.py b/ptpython/layout.py index 6482cbd0..e7b3f554 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -646,7 +646,7 @@ def menu_position(): sidebar = python_sidebar(python_input) self.exit_confirmation = create_exit_confirmation(python_input) - root_container = HSplit( + self.root_container = HSplit( [ VSplit( [ @@ -759,5 +759,5 @@ def menu_position(): ] ) - self.layout = Layout(root_container) + self.layout = Layout(self.root_container) self.sidebar = sidebar diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 2f6a5b9f..fce0242b 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -173,6 +173,11 @@ class PythonInput: python_input = PythonInput(...) python_code = python_input.app.run() + + :param create_app: When `False`, don't create and manage a prompt_toolkit + application. The default is `True` and should only be set + to false if PythonInput is being embedded in a separate + prompt_toolkit application. """ def __init__( @@ -187,6 +192,7 @@ def __init__( output: Optional[Output] = None, # For internal use. extra_key_bindings: Optional[KeyBindings] = None, + create_app = True, _completer: Optional[Completer] = None, _validator: Optional[Validator] = None, _lexer: Optional[Lexer] = None, @@ -379,10 +385,17 @@ def __init__( extra_toolbars=self._extra_toolbars, ) - self.app = self._create_application(input, output) + # Create an app if requested. If not, the global get_app() is returned + # for self.app via property getter. + if create_app: + self._app = self._create_application(input, output) + # Setting vi_mode will not work unless the prompt_toolkit + # application has been created. + if vi_mode: + self.app.editing_mode = EditingMode.VI + else: + self._app = None - if vi_mode: - self.app.editing_mode = EditingMode.VI def _accept_handler(self, buff: Buffer) -> bool: app = get_app() @@ -913,6 +926,12 @@ def vi_mode(self, value: bool) -> None: else: self.editing_mode = EditingMode.EMACS + @property + def app(self) -> Application: + if self._app is None: + return get_app() + return self._app + def _on_input_timeout(self, buff: Buffer) -> None: """ When there is no input activity, @@ -980,7 +999,7 @@ def enter_history(self) -> None: """ Display the history. """ - app = get_app() + app = self.app app.vi_state.input_mode = InputMode.NAVIGATION history = PythonHistory(self, self.default_buffer.document) diff --git a/ptpython/repl.py b/ptpython/repl.py index af73cb69..7d05e710 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -90,6 +90,35 @@ def _load_start_paths(self) -> None: output = self.app.output output.write("WARNING | File not found: {}\n\n".format(path)) + def run_and_show_expression(self, expression): + try: + # Eval. + try: + result = self.eval(expression) + except KeyboardInterrupt: # KeyboardInterrupt doesn't inherit from Exception. + raise + except SystemExit: + return + except BaseException as e: + self._handle_exception(e) + else: + # Print. + if result is not None: + self.show_result(result) + + # Loop. + self.current_statement_index += 1 + self.signatures = [] + + except KeyboardInterrupt as e: + # Handle all possible `KeyboardInterrupt` errors. This can + # happen during the `eval`, but also during the + # `show_result` if something takes too long. + # (Try/catch is around the whole block, because we want to + # prevent that a Control-C keypress terminates the REPL in + # any case.) + self._handle_keyboard_interrupt(e) + def run(self) -> None: """ Run the REPL loop. @@ -101,44 +130,43 @@ def run(self) -> None: try: while True: + # Pull text from the user. try: - # Read. - try: - text = self.read() - except EOFError: - return - - # Eval. - try: - result = self.eval(text) - except KeyboardInterrupt: # KeyboardInterrupt doesn't inherit from Exception. - raise - except SystemExit: - return - except BaseException as e: - self._handle_exception(e) - else: - # Print. - if result is not None: - self.show_result(result) - - # Loop. - self.current_statement_index += 1 - self.signatures = [] + text = self.read() + except EOFError: + return - except KeyboardInterrupt as e: - # Handle all possible `KeyboardInterrupt` errors. This can - # happen during the `eval`, but also during the - # `show_result` if something takes too long. - # (Try/catch is around the whole block, because we want to - # prevent that a Control-C keypress terminates the REPL in - # any case.) - self._handle_keyboard_interrupt(e) + # Run it; display the result (or errors if applicable). + self.run_and_show_expression(text) finally: if self.terminal_title: clear_title() self._remove_from_namespace() + async def run_and_show_expression_async(self, text): + loop = asyncio.get_event_loop() + + try: + result = await self.eval_async(text) + except KeyboardInterrupt: # KeyboardInterrupt doesn't inherit from Exception. + raise + except SystemExit: + return + except BaseException as e: + self._handle_exception(e) + else: + # Print. + if result is not None: + await loop.run_in_executor( + None, lambda: self.show_result(result) + ) + + # Loop. + self.current_statement_index += 1 + self.signatures = [] + # Return the result for future consumers. + return result + async def run_async(self) -> None: """ Run the REPL loop, but run the blocking parts in an executor, so that @@ -168,24 +196,7 @@ async def run_async(self) -> None: return # Eval. - try: - result = await self.eval_async(text) - except KeyboardInterrupt: # KeyboardInterrupt doesn't inherit from Exception. - raise - except SystemExit: - return - except BaseException as e: - self._handle_exception(e) - else: - # Print. - if result is not None: - await loop.run_in_executor( - None, lambda: self.show_result(result) - ) - - # Loop. - self.current_statement_index += 1 - self.signatures = [] + await self.run_and_show_expression_async(text) except KeyboardInterrupt as e: # XXX: This does not yet work properly. In some situations, @@ -285,9 +296,9 @@ def _compile_with_flags(self, code: str, mode: str): dont_inherit=True, ) - def show_result(self, result: object) -> None: + def _format_result_output(self, result: object) -> AnyFormattedText: """ - Show __repr__ for an `eval` result. + Format __repr__ for an `eval` result. Note: this can raise `KeyboardInterrupt` if either calling `__repr__`, `__pt_repr__` or formatting the output with "Black" takes to long @@ -303,7 +314,7 @@ def show_result(self, result: object) -> None: except BaseException as e: # Calling repr failed. self._handle_exception(e) - return + return None try: compile(result_repr, "", "eval") @@ -362,10 +373,18 @@ def show_result(self, result: object) -> None: out_prompt + [("", fragment_list_to_text(formatted_result_repr))] ) + return to_formatted_text(formatted_output) + + def show_result(self, result: object) -> None: + """ + Show __repr__ for an `eval` result and print to ouptut. + """ + formatted_text_output = self._format_result_output(result) + if self.enable_pager: - self.print_paginated_formatted_text(to_formatted_text(formatted_output)) + self.print_paginated_formatted_text(formatted_text_output) else: - self.print_formatted_text(to_formatted_text(formatted_output)) + self.print_formatted_text(formatted_text_output) self.app.output.flush() @@ -485,9 +504,7 @@ def create_pager_prompt(self) -> PromptSession["PagerResult"]: """ return create_pager_prompt(self._current_style, self.title) - def _handle_exception(self, e: BaseException) -> None: - output = self.app.output - + def _format_exception_output(self, e: BaseException) -> AnyFormattedText: # Instead of just calling ``traceback.format_exc``, we take the # traceback and skip the bottom calls of this framework. t, v, tb = sys.exc_info() @@ -516,6 +533,12 @@ def _handle_exception(self, e: BaseException) -> None: tokens = list(_lex_python_traceback(tb_str)) else: tokens = [(Token, tb_str)] + return tokens + + def _handle_exception(self, e: BaseException) -> None: + output = self.app.output + + tokens = self._format_exception_output(e) print_formatted_text( PygmentsTokens(tokens), From b436e79c09467098712dbc4da4d260317a13620e Mon Sep 17 00:00:00 2001 From: Roee Nizan Date: Thu, 11 Mar 2021 20:25:18 +0200 Subject: [PATCH 365/470] Feature: read ipython config files --- ptpython/ipython.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/ptpython/ipython.py b/ptpython/ipython.py index 2e8d1195..91633340 100644 --- a/ptpython/ipython.py +++ b/ptpython/ipython.py @@ -282,4 +282,21 @@ def embed(**kwargs): kwargs["config"] = config shell = InteractiveShellEmbed.instance(**kwargs) initialize_extensions(shell, config["InteractiveShellApp"]["extensions"]) + run_startup_scripts(shell) shell(header=header, stack_depth=2, compile_flags=compile_flags) + + +def run_startup_scripts(shell): + """ + Contributed by linyuxu: + https://github.com/prompt-toolkit/ptpython/issues/126#issue-161242480 + """ + import glob + import os + + startup_dir = shell.profile_dir.startup_dir + startup_files = [] + startup_files += glob.glob(os.path.join(startup_dir, "*.py")) + startup_files += glob.glob(os.path.join(startup_dir, "*.ipy")) + for file in startup_files: + shell.run_cell(open(file).read()) From 588f9d368193f1ec8e448e541c3f091b62230e8e Mon Sep 17 00:00:00 2001 From: Curiosity <53520949+sisrfeng@users.noreply.github.com> Date: Thu, 28 Jan 2021 15:43:03 +0800 Subject: [PATCH 366/470] Update repl.py use ~/.config/ptpython/config.py instead of ~/.ptpython/config.py --- ptpython/repl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ptpython/repl.py b/ptpython/repl.py index 7d05e710..2c186515 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -604,7 +604,7 @@ def enable_deprecation_warnings() -> None: warnings.filterwarnings("default", category=DeprecationWarning, module="__main__") -def run_config(repl: PythonInput, config_file: str = "~/.ptpython/config.py") -> None: +def run_config(repl: PythonInput, config_file: str = "~/.config/ptpython/config.py") -> None: """ Execute REPL config file. From 8d1ee2163a6b5e32d065a2289c5eceb0e515326b Mon Sep 17 00:00:00 2001 From: jhylands Date: Wed, 21 Apr 2021 16:58:38 +0100 Subject: [PATCH 367/470] Fixed config example, vi jj remap --- examples/ptpython_config/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/ptpython_config/config.py b/examples/ptpython_config/config.py index 8532f938..24275728 100644 --- a/examples/ptpython_config/config.py +++ b/examples/ptpython_config/config.py @@ -157,7 +157,7 @@ def _(event): @repl.add_key_binding("j", "j", filter=ViInsertMode()) def _(event): " Map 'jj' to Escape. " - event.cli.key_processor.feed(KeyPress("escape")) + event.cli.key_processor.feed(KeyPress(Keys("escape"))) """ # Custom key binding for some simple autocorrection while typing. From 8f36d931e2ea29d58ad69981abb7f0c04d840bbf Mon Sep 17 00:00:00 2001 From: Andrew Zhou <0az@afzhou.com> Date: Wed, 3 Mar 2021 14:07:00 -0600 Subject: [PATCH 368/470] Fix incorrect __main__ on script execution (#444) --- ptpython/entry_points/run_ptipython.py | 2 +- ptpython/entry_points/run_ptpython.py | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/ptpython/entry_points/run_ptipython.py b/ptpython/entry_points/run_ptipython.py index 650633ec..1a489d3f 100644 --- a/ptpython/entry_points/run_ptipython.py +++ b/ptpython/entry_points/run_ptipython.py @@ -31,7 +31,7 @@ def run(user_ns=None): path = a.args[0] with open(path, "rb") as f: code = compile(f.read(), path, "exec") - exec(code, {}) + exec(code, {'__name__': '__main__', '__file__': path}) else: enable_deprecation_warnings() diff --git a/ptpython/entry_points/run_ptpython.py b/ptpython/entry_points/run_ptpython.py index 0b3dbdb9..84a9aee1 100644 --- a/ptpython/entry_points/run_ptpython.py +++ b/ptpython/entry_points/run_ptpython.py @@ -179,9 +179,11 @@ def run() -> None: path = a.args[0] with open(path, "rb") as f: code = compile(f.read(), path, "exec") - # NOTE: We have to pass an empty dictionary as namespace. Omitting - # this argument causes imports to not be found. See issue #326. - exec(code, {}) + # NOTE: We have to pass a dict as namespace. Omitting this argument + # causes imports to not be found. See issue #326. + # However, an empty dict sets __name__ to 'builtins', which + # breaks `if __name__ == '__main__'` checks. See issue #444. + exec(code, {'__name__': '__main__', '__file__': path}) # Run interactive shell. else: From b85716354e34ce32424e3aa17f6afd342d88e513 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Mon, 22 Mar 2021 16:08:40 +0100 Subject: [PATCH 369/470] Release 3.0.17 --- CHANGELOG | 9 +++++++++ setup.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 67ac0a85..8f946b4b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,15 @@ CHANGELOG ========= +3.0.17: 2020-03-22 +------------------ + +Fixes: +- Fix leaking file descriptors due to not closing the asyncio event loop after + reading input in a thread. +- Fix race condition during retrieval of signatures. + + 3.0.16: 2020-02-11 ------------------ diff --git a/setup.py b/setup.py index c5f40f35..3f735073 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ setup( name="ptpython", author="Jonathan Slenders", - version="3.0.16", + version="3.0.17", url="https://github.com/prompt-toolkit/ptpython", description="Python REPL build on top of prompt_toolkit", long_description=long_description, From 513b9f41120f49aad75dbac00ef01ce17746b07c Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Mon, 24 May 2021 14:18:32 +0200 Subject: [PATCH 370/470] Code formatting (latest black). --- ptpython/completer.py | 4 ++-- ptpython/entry_points/run_ptipython.py | 2 +- ptpython/entry_points/run_ptpython.py | 2 +- ptpython/history_browser.py | 22 +++++++++++----------- ptpython/key_bindings.py | 12 ++++++------ ptpython/layout.py | 8 ++++---- ptpython/prompt_style.py | 4 ++-- ptpython/python_input.py | 9 ++++----- ptpython/repl.py | 18 +++++++++--------- 9 files changed, 40 insertions(+), 41 deletions(-) diff --git a/ptpython/completer.py b/ptpython/completer.py index 9f7e10bc..285398c2 100644 --- a/ptpython/completer.py +++ b/ptpython/completer.py @@ -468,7 +468,7 @@ def _get_item_lookup_completions( """ def abbr_meta(text: str) -> str: - " Abbreviate meta text, make sure it fits on one line. " + "Abbreviate meta text, make sure it fits on one line." # Take first line, if multiple lines. if len(text) > 20: text = text[:20] + "..." @@ -621,7 +621,7 @@ def is_private(completion: Completion) -> bool: class ReprFailedError(Exception): - " Raised when the repr() call in `DictionaryCompleter` fails. " + "Raised when the repr() call in `DictionaryCompleter` fails." try: diff --git a/ptpython/entry_points/run_ptipython.py b/ptpython/entry_points/run_ptipython.py index 1a489d3f..21d70637 100644 --- a/ptpython/entry_points/run_ptipython.py +++ b/ptpython/entry_points/run_ptipython.py @@ -31,7 +31,7 @@ def run(user_ns=None): path = a.args[0] with open(path, "rb") as f: code = compile(f.read(), path, "exec") - exec(code, {'__name__': '__main__', '__file__': path}) + exec(code, {"__name__": "__main__", "__file__": path}) else: enable_deprecation_warnings() diff --git a/ptpython/entry_points/run_ptpython.py b/ptpython/entry_points/run_ptpython.py index 84a9aee1..5ebe2b95 100644 --- a/ptpython/entry_points/run_ptpython.py +++ b/ptpython/entry_points/run_ptpython.py @@ -183,7 +183,7 @@ def run() -> None: # causes imports to not be found. See issue #326. # However, an empty dict sets __name__ to 'builtins', which # breaks `if __name__ == '__main__'` checks. See issue #444. - exec(code, {'__name__': '__main__', '__file__': path}) + exec(code, {"__name__": "__main__", "__file__": path}) # Run interactive shell. else: diff --git a/ptpython/history_browser.py b/ptpython/history_browser.py index 798a280f..b7fe0865 100644 --- a/ptpython/history_browser.py +++ b/ptpython/history_browser.py @@ -85,7 +85,7 @@ class BORDER: - " Box drawing characters. " + "Box drawing characters." HORIZONTAL = "\u2501" VERTICAL = "\u2503" TOP_LEFT = "\u250f" @@ -420,7 +420,7 @@ def update_default_buffer(self): def _toggle_help(history): - " Display/hide help. " + "Display/hide help." help_buffer_control = history.history_layout.help_buffer_control if history.app.layout.current_control == help_buffer_control: @@ -430,7 +430,7 @@ def _toggle_help(history): def _select_other_window(history): - " Toggle focus between left/right window. " + "Toggle focus between left/right window." current_buffer = history.app.current_buffer layout = history.history_layout.layout @@ -513,17 +513,17 @@ def _(event): # Eager: ignore the Emacs [Ctrl-X Ctrl-X] binding. @handle("c-w", filter=main_buffer_focussed) def _(event): - " Select other window. " + "Select other window." _select_other_window(history) @handle("f4") def _(event): - " Switch between Emacs/Vi mode. " + "Switch between Emacs/Vi mode." python_input.vi_mode = not python_input.vi_mode @handle("f1") def _(event): - " Display/hide help. " + "Display/hide help." _toggle_help(history) @handle("enter", filter=help_focussed) @@ -531,7 +531,7 @@ def _(event): @handle("c-g", filter=help_focussed) @handle("escape", filter=help_focussed) def _(event): - " Leave help. " + "Leave help." event.app.layout.focus_previous() @handle("q", filter=main_buffer_focussed) @@ -539,19 +539,19 @@ def _(event): @handle("c-c", filter=main_buffer_focussed) @handle("c-g", filter=main_buffer_focussed) def _(event): - " Cancel and go back. " + "Cancel and go back." event.app.exit(result=None) @handle("enter", filter=main_buffer_focussed) def _(event): - " Accept input. " + "Accept input." event.app.exit(result=history.default_buffer.text) enable_system_bindings = Condition(lambda: python_input.enable_system_bindings) @handle("c-z", filter=enable_system_bindings) def _(event): - " Suspend to background. " + "Suspend to background." event.app.suspend_to_background() return bindings @@ -630,7 +630,7 @@ def _default_buffer_pos_changed(self, _): ) def _history_buffer_pos_changed(self, _): - """ When the cursor changes in the history buffer. Synchronize. """ + """When the cursor changes in the history buffer. Synchronize.""" # Only when this buffer has the focus. if self.app.current_buffer == self.history_buffer: line_no = self.history_buffer.document.cursor_position_row diff --git a/ptpython/key_bindings.py b/ptpython/key_bindings.py index 86317f90..ae23a3df 100644 --- a/ptpython/key_bindings.py +++ b/ptpython/key_bindings.py @@ -203,7 +203,7 @@ def _(event): @handle("c-c", filter=has_focus(python_input.default_buffer)) def _(event): - " Abort when Control-C has been pressed. " + "Abort when Control-C has been pressed." event.app.exit(exception=KeyboardInterrupt, style="class:aborting") return bindings @@ -222,7 +222,7 @@ def load_sidebar_bindings(python_input): @handle("c-p", filter=sidebar_visible) @handle("k", filter=sidebar_visible) def _(event): - " Go to previous option. " + "Go to previous option." python_input.selected_option_index = ( python_input.selected_option_index - 1 ) % python_input.option_count @@ -231,7 +231,7 @@ def _(event): @handle("c-n", filter=sidebar_visible) @handle("j", filter=sidebar_visible) def _(event): - " Go to next option. " + "Go to next option." python_input.selected_option_index = ( python_input.selected_option_index + 1 ) % python_input.option_count @@ -240,14 +240,14 @@ def _(event): @handle("l", filter=sidebar_visible) @handle(" ", filter=sidebar_visible) def _(event): - " Select next value for current option. " + "Select next value for current option." option = python_input.selected_option option.activate_next() @handle("left", filter=sidebar_visible) @handle("h", filter=sidebar_visible) def _(event): - " Select previous value for current option. " + "Select previous value for current option." option = python_input.selected_option option.activate_previous() @@ -257,7 +257,7 @@ def _(event): @handle("enter", filter=sidebar_visible) @handle("escape", filter=sidebar_visible) def _(event): - " Hide sidebar. " + "Hide sidebar." python_input.show_sidebar = False event.app.layout.focus_last() diff --git a/ptpython/layout.py b/ptpython/layout.py index e7b3f554..dc6b19bb 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -64,7 +64,7 @@ class CompletionVisualisation(Enum): - " Visualisation method for the completions. " + "Visualisation method for the completions." NONE = "none" POP_UP = "pop-up" MULTI_COLUMN = "multi-column" @@ -116,7 +116,7 @@ def select_item(mouse_event: MouseEvent) -> None: @if_mousedown def goto_next(mouse_event: MouseEvent) -> None: - " Select item and go to next value. " + "Select item and go to next value." python_input.selected_option_index = index option = python_input.selected_option option.activate_next() @@ -472,7 +472,7 @@ def show_sidebar_button_info(python_input: "PythonInput") -> Container: @if_mousedown def toggle_sidebar(mouse_event: MouseEvent) -> None: - " Click handler for the menu. " + "Click handler for the menu." python_input.show_sidebar = not python_input.show_sidebar version = sys.version_info @@ -544,7 +544,7 @@ def get_text_fragments() -> StyleAndTextTuples: @Condition def extra_condition() -> bool: - " Only show when... " + "Only show when..." b = python_input.default_buffer return ( diff --git a/ptpython/prompt_style.py b/ptpython/prompt_style.py index 24e5f883..e7334af2 100644 --- a/ptpython/prompt_style.py +++ b/ptpython/prompt_style.py @@ -16,7 +16,7 @@ class PromptStyle(metaclass=ABCMeta): @abstractmethod def in_prompt(self) -> AnyFormattedText: - " Return the input tokens. " + "Return the input tokens." return [] @abstractmethod @@ -31,7 +31,7 @@ def in2_prompt(self, width: int) -> AnyFormattedText: @abstractmethod def out_prompt(self) -> AnyFormattedText: - " Return the output tokens. " + "Return the output tokens." return [] diff --git a/ptpython/python_input.py b/ptpython/python_input.py index fce0242b..2b75d6e5 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -192,7 +192,7 @@ def __init__( output: Optional[Output] = None, # For internal use. extra_key_bindings: Optional[KeyBindings] = None, - create_app = True, + create_app=True, _completer: Optional[Completer] = None, _validator: Optional[Validator] = None, _lexer: Optional[Lexer] = None, @@ -396,7 +396,6 @@ def __init__( else: self._app = None - def _accept_handler(self, buff: Buffer) -> bool: app = get_app() app.exit(result=buff.text) @@ -405,12 +404,12 @@ def _accept_handler(self, buff: Buffer) -> bool: @property def option_count(self) -> int: - " Return the total amount of options. (In all categories together.) " + "Return the total amount of options. (In all categories together.)" return sum(len(category.options) for category in self.options) @property def selected_option(self) -> Option: - " Return the currently selected option. " + "Return the currently selected option." i = 0 for category in self.options: for o in category.options: @@ -533,7 +532,7 @@ def disable(attribute: str) -> bool: def simple_option( title: str, description: str, field_name: str, values: Optional[List] = None ) -> Option: - " Create Simple on/of option. " + "Create Simple on/of option." values = values or ["off", "on"] def get_current_value(): diff --git a/ptpython/repl.py b/ptpython/repl.py index 2c186515..c0026fbf 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -79,7 +79,7 @@ def __init__(self, *a, **kw) -> None: self._load_start_paths() def _load_start_paths(self) -> None: - " Start the Read-Eval-Print Loop. " + "Start the Read-Eval-Print Loop." if self._startup_paths: for path in self._startup_paths: if os.path.exists(path): @@ -157,9 +157,7 @@ async def run_and_show_expression_async(self, text): else: # Print. if result is not None: - await loop.run_in_executor( - None, lambda: self.show_result(result) - ) + await loop.run_in_executor(None, lambda: self.show_result(result)) # Loop. self.current_statement_index += 1 @@ -287,7 +285,7 @@ def get_compiler_flags(self) -> int: return super().get_compiler_flags() | PyCF_ALLOW_TOP_LEVEL_AWAIT def _compile_with_flags(self, code: str, mode: str): - " Compile code with the right compiler flags. " + "Compile code with the right compiler flags." return compile( code, "", @@ -578,13 +576,13 @@ def _remove_from_namespace(self) -> None: def _lex_python_traceback(tb): - " Return token list for traceback string. " + "Return token list for traceback string." lexer = PythonTracebackLexer() return lexer.get_tokens(tb) def _lex_python_result(tb): - " Return token list for Python string. " + "Return token list for Python string." lexer = PythonLexer() # Use `get_tokens_unprocessed`, so that we get exactly the same string, # without line endings appended. `print_formatted_text` already appends a @@ -604,7 +602,9 @@ def enable_deprecation_warnings() -> None: warnings.filterwarnings("default", category=DeprecationWarning, module="__main__") -def run_config(repl: PythonInput, config_file: str = "~/.config/ptpython/config.py") -> None: +def run_config( + repl: PythonInput, config_file: str = "~/.config/ptpython/config.py" +) -> None: """ Execute REPL config file. @@ -752,7 +752,7 @@ def no(event: KeyPressEvent) -> None: @bindings.add("") def _(event: KeyPressEvent) -> None: - " Disallow inserting other text. " + "Disallow inserting other text." pass style From 71c74fe8bf826aa156cce35fca01fba9a8ff6d5c Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Mon, 24 May 2021 14:25:25 +0200 Subject: [PATCH 371/470] Fixed several typing issues. --- ptpython/python_input.py | 2 +- ptpython/repl.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 2b75d6e5..1785f523 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -388,7 +388,7 @@ def __init__( # Create an app if requested. If not, the global get_app() is returned # for self.app via property getter. if create_app: - self._app = self._create_application(input, output) + self._app: Optional[Application] = self._create_application(input, output) # Setting vi_mode will not work unless the prompt_toolkit # application has been created. if vi_mode: diff --git a/ptpython/repl.py b/ptpython/repl.py index c0026fbf..64c9dc14 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -294,7 +294,7 @@ def _compile_with_flags(self, code: str, mode: str): dont_inherit=True, ) - def _format_result_output(self, result: object) -> AnyFormattedText: + def _format_result_output(self, result: object) -> StyleAndTextTuples: """ Format __repr__ for an `eval` result. @@ -312,7 +312,7 @@ def _format_result_output(self, result: object) -> AnyFormattedText: except BaseException as e: # Calling repr failed. self._handle_exception(e) - return None + return [] try: compile(result_repr, "", "eval") @@ -502,7 +502,7 @@ def create_pager_prompt(self) -> PromptSession["PagerResult"]: """ return create_pager_prompt(self._current_style, self.title) - def _format_exception_output(self, e: BaseException) -> AnyFormattedText: + def _format_exception_output(self, e: BaseException) -> PygmentsTokens: # Instead of just calling ``traceback.format_exc``, we take the # traceback and skip the bottom calls of this framework. t, v, tb = sys.exc_info() @@ -531,7 +531,7 @@ def _format_exception_output(self, e: BaseException) -> AnyFormattedText: tokens = list(_lex_python_traceback(tb_str)) else: tokens = [(Token, tb_str)] - return tokens + return PygmentsTokens(tokens) def _handle_exception(self, e: BaseException) -> None: output = self.app.output @@ -539,7 +539,7 @@ def _handle_exception(self, e: BaseException) -> None: tokens = self._format_exception_output(e) print_formatted_text( - PygmentsTokens(tokens), + tokens, style=self._current_style, style_transformation=self.style_transformation, include_default_pygments_style=False, From b74af76490ee6cba674f916b51e0495729988fb6 Mon Sep 17 00:00:00 2001 From: stonebig Date: Sun, 30 May 2021 18:12:52 +0200 Subject: [PATCH 372/470] Make Black optional Make Black optional --- ptpython/repl.py | 17 +++++++++++------ setup.py | 6 ++++-- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/ptpython/repl.py b/ptpython/repl.py index 64c9dc14..b158b93c 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -323,12 +323,17 @@ def _format_result_output(self, result: object) -> StyleAndTextTuples: if self.enable_output_formatting: # Inline import. Slightly speed up start-up time if black is # not used. - import black - - result_repr = black.format_str( - result_repr, - mode=black.FileMode(line_length=self.app.output.get_size().columns), - ) + try: + import black + except ImportError: + pass # no Black package in your installation + else: + result_repr = black.format_str( + result_repr, + mode=black.FileMode( + line_length=self.app.output.get_size().columns + ), + ) formatted_result_repr = to_formatted_text( PygmentsTokens(list(_lex_python_result(result_repr))) diff --git a/setup.py b/setup.py index 3f735073..40b23cf1 100644 --- a/setup.py +++ b/setup.py @@ -23,7 +23,6 @@ # Use prompt_toolkit 3.0.18, because of the `in_thread` option. "prompt_toolkit>=3.0.18,<3.1.0", "pygments", - "black", ], python_requires=">=3.6", classifiers=[ @@ -47,5 +46,8 @@ % sys.version_info[:2], ] }, - extras_require={"ptipython": ["ipython"]}, # For ptipython, we need to have IPython + extras_require={ + "ptipython": ["ipython"], # For ptipython, we need to have IPython + "all": ["black"], # Black not always possible on PyPy + }, ) From 4b49c5bc8841d854c35b6a5586718f05d84ae5cf Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sat, 26 Jun 2021 21:43:25 +0200 Subject: [PATCH 373/470] Release 3.0.18 --- CHANGELOG | 7 +++++++ setup.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 8f946b4b..d3f64ac1 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,13 @@ CHANGELOG ========= +3.0.18: 2020-06-26 +------------------ + +Fixes: +- Made "black" an optional dependency. + + 3.0.17: 2020-03-22 ------------------ diff --git a/setup.py b/setup.py index 40b23cf1..ad17cfad 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ setup( name="ptpython", author="Jonathan Slenders", - version="3.0.17", + version="3.0.18", url="https://github.com/prompt-toolkit/ptpython", description="Python REPL build on top of prompt_toolkit", long_description=long_description, From 78c5a0df8d37c24d97c933a7f820afdd179bab28 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 8 Jul 2021 16:32:43 +0200 Subject: [PATCH 374/470] Fix handling of SystemExit --- ptpython/repl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ptpython/repl.py b/ptpython/repl.py index b158b93c..e6647c9f 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -98,7 +98,7 @@ def run_and_show_expression(self, expression): except KeyboardInterrupt: # KeyboardInterrupt doesn't inherit from Exception. raise except SystemExit: - return + raise except BaseException as e: self._handle_exception(e) else: From b6d9bc7a18a030a57892008d6f819d3900918e2d Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 8 Jul 2021 16:38:06 +0200 Subject: [PATCH 375/470] Fix for black integration. Use black.Mode instead of black.FileMode. --- .github/workflows/test.yaml | 1 + ptpython/repl.py | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 00ed1b00..0368ba7b 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -23,6 +23,7 @@ jobs: sudo apt remove python3-pip python -m pip install --upgrade pip python -m pip install . black isort mypy pytest readme_renderer + python -m pip install . types-dataclasses # Needed for Python 3.6 pip list - name: Type Checker run: | diff --git a/ptpython/repl.py b/ptpython/repl.py index e6647c9f..455e5f38 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -330,9 +330,7 @@ def _format_result_output(self, result: object) -> StyleAndTextTuples: else: result_repr = black.format_str( result_repr, - mode=black.FileMode( - line_length=self.app.output.get_size().columns - ), + mode=black.Mode(line_length=self.app.output.get_size().columns), ) formatted_result_repr = to_formatted_text( From 70dd3bd8d0785bcd76d6da82355a1a1aaa7e33d1 Mon Sep 17 00:00:00 2001 From: baldulin Date: Fri, 11 Jun 2021 20:53:12 +0200 Subject: [PATCH 376/470] Enable use of await in assignment expressions This tries to fix #447 and some other bugs concerning expressions, like for instance `for` loops. Which cannot contain awaitables otherwise. --- ptpython/repl.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/ptpython/repl.py b/ptpython/repl.py index 455e5f38..b3411cb8 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -271,9 +271,14 @@ async def eval_async(self, line: str) -> object: self._store_eval_result(result) return result - # If not a valid `eval` expression, run using `exec` instead. + # If not a valid `eval` expression, compile as `exec` expression + # but still run with eval to get an awaitable in case of a + # awaitable expression. code = self._compile_with_flags(line, "exec") - exec(code, self.get_globals(), self.get_locals()) + result = eval(code, self.get_globals(), self.get_locals()) + + if _has_coroutine_flag(code): + result = await result return None From 3bf39985671de1ed37584fcea37c6fa850e37fc7 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 8 Jul 2021 17:16:24 +0200 Subject: [PATCH 377/470] Fix last commit: allow await in assignment expressions when the REPL itself is not running async. --- ptpython/repl.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ptpython/repl.py b/ptpython/repl.py index b3411cb8..220c673f 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -239,7 +239,10 @@ def eval(self, line: str) -> object: # above, then `sys.exc_info()` would not report the right error. # See issue: https://github.com/prompt-toolkit/ptpython/issues/435 code = self._compile_with_flags(line, "exec") - exec(code, self.get_globals(), self.get_locals()) + result = eval(code, self.get_globals(), self.get_locals()) + + if _has_coroutine_flag(code): + result = asyncio.get_event_loop().run_until_complete(result) return None From 52705a77d31dc0914386c10422e89303aa5ca0c5 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 8 Jul 2021 17:25:08 +0200 Subject: [PATCH 378/470] Release 3.0.19 --- CHANGELOG | 9 +++++++++ setup.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index d3f64ac1..6a1eb218 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,15 @@ CHANGELOG ========= +3.0.19: 2020-07-08 +------------------ + +Fixes: +- Fix handling of `SystemExit` (fixes "ValueError: I/O operation on closed + file"). +- Allow usage of `await` in assignment expressions or for-loops. + + 3.0.18: 2020-06-26 ------------------ diff --git a/setup.py b/setup.py index ad17cfad..faab112d 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ setup( name="ptpython", author="Jonathan Slenders", - version="3.0.18", + version="3.0.19", url="https://github.com/prompt-toolkit/ptpython", description="Python REPL build on top of prompt_toolkit", long_description=long_description, From ae608c27427af160ec5c06a30851cb339e24b0bb Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Fri, 6 Aug 2021 15:56:26 +0200 Subject: [PATCH 379/470] Don't crash when trying to complete broken mappings. Show the traceback when something goes wrong while reading input in the REPL due to completer bugs or other bugs. Don't crash the REPL. --- ptpython/completer.py | 8 ++++++++ ptpython/repl.py | 12 ++++++++++++ 2 files changed, 20 insertions(+) diff --git a/ptpython/completer.py b/ptpython/completer.py index 285398c2..d235a024 100644 --- a/ptpython/completer.py +++ b/ptpython/completer.py @@ -505,6 +505,10 @@ def abbr_meta(text: str) -> str: display=f"[{k_repr}]", display_meta=abbr_meta(self._do_repr(result[k])), ) + except KeyError: + # `result[k]` lookup failed. Trying to complete + # broken object. + pass except ReprFailedError: pass @@ -521,6 +525,10 @@ def abbr_meta(text: str) -> str: display=f"[{k_repr}]", display_meta=abbr_meta(self._do_repr(result[k])), ) + except KeyError: + # `result[k]` lookup failed. Trying to complete + # broken object. + pass except ReprFailedError: pass diff --git a/ptpython/repl.py b/ptpython/repl.py index 220c673f..d451a61f 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -135,6 +135,12 @@ def run(self) -> None: text = self.read() except EOFError: return + except BaseException as e: + # Something went wrong while reading input. + # (E.g., a bug in the completer that propagates. Don't + # crash the REPL.) + traceback.print_exc() + continue # Run it; display the result (or errors if applicable). self.run_and_show_expression(text) @@ -192,6 +198,12 @@ async def run_async(self) -> None: text = await loop.run_in_executor(None, self.read) except EOFError: return + except BaseException: + # Something went wrong while reading input. + # (E.g., a bug in the completer that propagates. Don't + # crash the REPL.) + traceback.print_exc() + continue # Eval. await self.run_and_show_expression_async(text) From 667805397637edd66d82d891fed0819178f987fd Mon Sep 17 00:00:00 2001 From: jlamelas Date: Mon, 13 Sep 2021 17:55:47 +0200 Subject: [PATCH 380/470] Raising Import Error if Mode not in black - older versions don't have Mode. Adding formating --- ptpython/repl.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ptpython/repl.py b/ptpython/repl.py index d451a61f..b55b5d56 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -345,6 +345,9 @@ def _format_result_output(self, result: object) -> StyleAndTextTuples: # not used. try: import black + + if not hasattr(black, "Mode"): + raise ImportError except ImportError: pass # no Black package in your installation else: From e9df9075c20451b3e2852104f5a6f672de51485b Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 14 Sep 2021 12:11:25 +0200 Subject: [PATCH 381/470] Show parentheses after the completions for methods when using the `DictionaryCompleter`. --- ptpython/completer.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ptpython/completer.py b/ptpython/completer.py index d235a024..51a4086b 100644 --- a/ptpython/completer.py +++ b/ptpython/completer.py @@ -553,9 +553,8 @@ def _get_attribute_completions( def get_suffix(name: str) -> str: try: obj = getattr(result, name, None) - if inspect.isfunction(obj): + if inspect.isfunction(obj) or inspect.ismethod(obj): return "()" - if isinstance(obj, dict): return "{}" if isinstance(obj, (list, tuple)): From 2ba2174f361fc3f5cf000fe59c4e64bf3d9ddead Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 14 Sep 2021 12:21:03 +0200 Subject: [PATCH 382/470] Fix dates in changelog (2020 -> 2021). --- CHANGELOG | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 6a1eb218..d561685c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,7 +1,8 @@ CHANGELOG ========= -3.0.19: 2020-07-08 + +3.0.19: 2021-07-08 ------------------ Fixes: @@ -10,14 +11,14 @@ Fixes: - Allow usage of `await` in assignment expressions or for-loops. -3.0.18: 2020-06-26 +3.0.18: 2021-06-26 ------------------ Fixes: - Made "black" an optional dependency. -3.0.17: 2020-03-22 +3.0.17: 2021-03-22 ------------------ Fixes: @@ -26,7 +27,7 @@ Fixes: - Fix race condition during retrieval of signatures. -3.0.16: 2020-02-11 +3.0.16: 2021-02-11 ------------------ (Commit 7f619e was missing in previous release.) @@ -38,7 +39,7 @@ Fixes: completions were missed out if the fuzzy completer doesn't find them. -3.0.15: 2020-02-11 +3.0.15: 2021-02-11 ------------------ New features: @@ -48,7 +49,7 @@ Fixes: - Fix `AttributeError` during retrieval of signatures with type annotations. -3.0.14: 2020-02-10 +3.0.14: 2021-02-10 ------------------ New features: @@ -67,7 +68,7 @@ Fixes: - Hide signature when sidebar is visible. -3.0.13: 2020-01-26 +3.0.13: 2021-01-26 ------------------ New features: @@ -82,7 +83,7 @@ Fixes: - Fix line ending bug in pager. -3.0.12: 2020-01-24 +3.0.12: 2021-01-24 ------------------ New features: @@ -96,7 +97,7 @@ Fixes: - Properly handle `SystemExit`. -3.0.11: 2020-01-20 +3.0.11: 2021-01-20 ------------------ New features: @@ -119,7 +120,7 @@ Fixes: - Don't execute PYTHONSTARTUP when -i flag was given. -3.0.10: 2020-01-13 +3.0.10: 2021-01-13 ------------------ Fixes: @@ -128,7 +129,7 @@ Fixes: default. -3.0.9: 2020-01-10 +3.0.9: 2021-01-10 ----------------- New features: @@ -137,7 +138,7 @@ New features: - Show REPL title in pager. -3.0.8: 2020-01-05 +3.0.8: 2021-01-05 ----------------- New features: From d8b5c90d20cc1e8d88837aa7163289950c231f74 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 14 Sep 2021 12:21:48 +0200 Subject: [PATCH 383/470] Release 3.0.20 --- CHANGELOG | 11 +++++++++++ setup.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index d561685c..69a95e7d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,17 @@ CHANGELOG ========= +3.0.20: 2021-09-14 +------------------ + +New features: +- For `DictionaryCompleter`: show parentheses after methods. + +Fixes: +- Don't crash when trying to complete broken mappings in `DictionaryCompleter`. +- Don't crash when an older version of `black` is installed that is not + compatible. + 3.0.19: 2021-07-08 ------------------ diff --git a/setup.py b/setup.py index faab112d..72a0e8b2 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ setup( name="ptpython", author="Jonathan Slenders", - version="3.0.19", + version="3.0.20", url="https://github.com/prompt-toolkit/ptpython", description="Python REPL build on top of prompt_toolkit", long_description=long_description, From 5449dc4ed494aacd9627ac4a97e24ee2113724f2 Mon Sep 17 00:00:00 2001 From: Rik Date: Fri, 22 Oct 2021 11:39:35 +0200 Subject: [PATCH 384/470] Added docs to example config about code colorscheme usage --- examples/ptpython_config/config.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/examples/ptpython_config/config.py b/examples/ptpython_config/config.py index 24275728..bf9d05fe 100644 --- a/examples/ptpython_config/config.py +++ b/examples/ptpython_config/config.py @@ -106,8 +106,13 @@ def configure(repl): repl.enable_input_validation = True # Use this colorscheme for the code. + # Ptpython uses Pygments for code styling, so you can choose from Pygments' + # color schemes. See: + # https://pygments.org/docs/styles/ + # https://pygments.org/demo/ repl.use_code_colorscheme("default") - # repl.use_code_colorscheme("pastie") + # A colorscheme that looks good on dark backgrounds is 'native': + # repl.use_code_colorscheme("native") # Set color depth (keep in mind that not all terminals support true color). From 52490b3235f62ee0d2c68a3b4eea83715a50e8af Mon Sep 17 00:00:00 2001 From: Jack Desert Date: Thu, 19 Aug 2021 09:35:52 -0500 Subject: [PATCH 385/470] Demonstrate Help Menu in README --- README.rst | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index ae12f4d7..1edc5403 100644 --- a/README.rst +++ b/README.rst @@ -50,6 +50,40 @@ Features [2] If the terminal supports it (most terminals do), this allows pasting without going into paste mode. It will keep the indentation. +Command Line Options +******************** + +The help menu shows basic command-line options. + +:: + + $ ptpython --help + usage: ptpython [-h] [--vi] [-i] [--light-bg] [--dark-bg] [--config-file CONFIG_FILE] + [--history-file HISTORY_FILE] [-V] + [args ...] + + ptpython: Interactive Python shell. + + positional arguments: + args Script and arguments + + optional arguments: + -h, --help show this help message and exit + --vi Enable Vi key bindings + -i, --interactive Start interactive shell after executing this file. + --light-bg Run on a light background (use dark colors for text). + --dark-bg Run on a dark background (use light colors for text). + --config-file CONFIG_FILE + Location of configuration file. + --history-file HISTORY_FILE + Location of history file. + -V, --version show program's version number and exit + + environment variables: + PTPYTHON_CONFIG_HOME: a configuration directory to use + PYTHONSTARTUP: file executed on interactive startup (no default) + + __pt_repr__: A nicer repr with colors ************************************* @@ -211,7 +245,7 @@ FAQ **Q**: The ``Meta``-key doesn't work. -**A**: For some terminals you have to enable the Alt-key to act as meta key, but you +**A**: For some terminals you have to enable the Alt-key to act as meta key, but you can also type ``Escape`` before any key instead. From d31915d4bc97e72415d63681b9d480375da829cc Mon Sep 17 00:00:00 2001 From: Jack Desert Date: Thu, 19 Aug 2021 09:56:21 -0500 Subject: [PATCH 386/470] Alerting Users that Config is Not Used when Embedding --- README.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.rst b/README.rst index 1edc5403..15464ba4 100644 --- a/README.rst +++ b/README.rst @@ -143,6 +143,8 @@ like this: else: sys.exit(embed(globals(), locals())) +Note config file support currently only works when invoking `ptpython` directly. +That it, the config file will be ignored when embedding ptpython in an application. Multiline editing ***************** @@ -193,6 +195,9 @@ is looked for. Have a look at this example to see what is possible: `config.py `_ +Note config file support currently only works when invoking `ptpython` directly. +That it, the config file will be ignored when embedding ptpython in an application. + IPython support *************** From 0af5c10cf19462f27e43914b8dbed81e5b0f53ae Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Mon, 20 Jun 2022 22:46:36 +0200 Subject: [PATCH 387/470] Added py.typed to package_data in setup.py --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 72a0e8b2..a8214f27 100644 --- a/setup.py +++ b/setup.py @@ -16,6 +16,7 @@ description="Python REPL build on top of prompt_toolkit", long_description=long_description, packages=find_packages("."), + package_data={"ptpython": ["py.typed"]}, install_requires=[ "appdirs", "importlib_metadata;python_version<'3.8'", From 042ecc3199b89819a071048fbd27e551ae25e114 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Fri, 28 Oct 2022 20:18:02 +0000 Subject: [PATCH 388/470] Improve DictionaryCompleter performance for slow mappings. The performance was bad when we had a huge custom mapping with an expensive `__getitem__`. --- ptpython/completer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ptpython/completer.py b/ptpython/completer.py index 51a4086b..22698f8e 100644 --- a/ptpython/completer.py +++ b/ptpython/completer.py @@ -495,7 +495,7 @@ def abbr_meta(text: str) -> str: else: break - for k in result: + for k, v in result.items(): if str(k).startswith(str(key_obj)): try: k_repr = self._do_repr(k) @@ -503,7 +503,7 @@ def abbr_meta(text: str) -> str: k_repr + "]", -len(key), display=f"[{k_repr}]", - display_meta=abbr_meta(self._do_repr(result[k])), + display_meta=abbr_meta(self._do_repr(v)), ) except KeyError: # `result[k]` lookup failed. Trying to complete From 05d4aed170babf345e2daed90fff812349044ce4 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Fri, 28 Oct 2022 21:36:16 +0000 Subject: [PATCH 389/470] Added more precise types in various places. --- ptpython/completer.py | 25 ++++-- ptpython/entry_points/run_ptpython.py | 12 +-- ptpython/eventloop.py | 14 +-- ptpython/history_browser.py | 122 +++++++++++++++++--------- ptpython/ipython.py | 26 ++++-- ptpython/key_bindings.py | 61 +++++++------ ptpython/layout.py | 64 ++++++++------ ptpython/python_input.py | 52 +++++++---- ptpython/repl.py | 15 ++-- ptpython/signatures.py | 9 +- ptpython/utils.py | 36 ++++++-- ptpython/validator.py | 9 +- setup.cfg | 39 ++++++++ 13 files changed, 324 insertions(+), 160 deletions(-) diff --git a/ptpython/completer.py b/ptpython/completer.py index 22698f8e..2b6795d4 100644 --- a/ptpython/completer.py +++ b/ptpython/completer.py @@ -4,7 +4,7 @@ import keyword import re from enum import Enum -from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, List, Optional +from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, List, Optional, Tuple from prompt_toolkit.completion import ( CompleteEvent, @@ -21,6 +21,7 @@ from ptpython.utils import get_jedi_script_from_document if TYPE_CHECKING: + import jedi.api.classes from prompt_toolkit.contrib.regular_languages.compiler import _CompiledGrammar __all__ = ["PythonCompleter", "CompletePrivateAttributes", "HidePrivateCompleter"] @@ -43,8 +44,8 @@ class PythonCompleter(Completer): def __init__( self, - get_globals: Callable[[], dict], - get_locals: Callable[[], dict], + get_globals: Callable[[], Dict[str, Any]], + get_locals: Callable[[], Dict[str, Any]], enable_dictionary_completion: Callable[[], bool], ) -> None: super().__init__() @@ -200,7 +201,11 @@ class JediCompleter(Completer): Autocompleter that uses the Jedi library. """ - def __init__(self, get_globals, get_locals) -> None: + def __init__( + self, + get_globals: Callable[[], Dict[str, Any]], + get_locals: Callable[[], Dict[str, Any]], + ) -> None: super().__init__() self.get_globals = get_globals @@ -296,7 +301,11 @@ class DictionaryCompleter(Completer): function calls, so it only triggers attribute access. """ - def __init__(self, get_globals, get_locals): + def __init__( + self, + get_globals: Callable[[], Dict[str, Any]], + get_locals: Callable[[], Dict[str, Any]], + ) -> None: super().__init__() self.get_globals = get_globals @@ -574,7 +583,7 @@ def _sort_attribute_names(self, names: List[str]) -> List[str]: underscore names to the end. """ - def sort_key(name: str): + def sort_key(name: str) -> Tuple[int, str]: if name.startswith("__"): return (2, name) # Double underscore comes latest. if name.startswith("_"): @@ -639,7 +648,9 @@ class ReprFailedError(Exception): _builtin_names = [] -def _get_style_for_jedi_completion(jedi_completion) -> str: +def _get_style_for_jedi_completion( + jedi_completion: "jedi.api.classes.Completion", +) -> str: """ Return completion style to use for this name. """ diff --git a/ptpython/entry_points/run_ptpython.py b/ptpython/entry_points/run_ptpython.py index 5ebe2b95..edffa44d 100644 --- a/ptpython/entry_points/run_ptpython.py +++ b/ptpython/entry_points/run_ptpython.py @@ -26,16 +26,16 @@ import pathlib import sys from textwrap import dedent -from typing import Tuple +from typing import IO, Optional, Tuple import appdirs from prompt_toolkit.formatted_text import HTML from prompt_toolkit.shortcuts import print_formatted_text -from ptpython.repl import embed, enable_deprecation_warnings, run_config +from ptpython.repl import PythonRepl, embed, enable_deprecation_warnings, run_config try: - from importlib import metadata + from importlib import metadata # type: ignore except ImportError: import importlib_metadata as metadata # type: ignore @@ -44,7 +44,7 @@ class _Parser(argparse.ArgumentParser): - def print_help(self): + def print_help(self, file: Optional[IO[str]] = None) -> None: super().print_help() print( dedent( @@ -84,7 +84,7 @@ def create_parser() -> _Parser: "-V", "--version", action="version", - version=metadata.version("ptpython"), # type: ignore + version=metadata.version("ptpython"), ) parser.add_argument("args", nargs="*", help="Script and arguments") return parser @@ -190,7 +190,7 @@ def run() -> None: enable_deprecation_warnings() # Apply config file - def configure(repl) -> None: + def configure(repl: PythonRepl) -> None: if os.path.exists(config_file): run_config(repl, config_file) diff --git a/ptpython/eventloop.py b/ptpython/eventloop.py index c841972d..63dd7408 100644 --- a/ptpython/eventloop.py +++ b/ptpython/eventloop.py @@ -10,10 +10,12 @@ import sys import time +from prompt_toolkit.eventloop import InputHookContext + __all__ = ["inputhook"] -def _inputhook_tk(inputhook_context): +def _inputhook_tk(inputhook_context: InputHookContext) -> None: """ Inputhook for Tk. Run the Tk eventloop until prompt-toolkit needs to process the next input. @@ -23,9 +25,9 @@ def _inputhook_tk(inputhook_context): import _tkinter # Keep this imports inline! - root = tkinter._default_root + root = tkinter._default_root # type: ignore - def wait_using_filehandler(): + def wait_using_filehandler() -> None: """ Run the TK eventloop until the file handler that we got from the inputhook becomes readable. @@ -34,7 +36,7 @@ def wait_using_filehandler(): # to process. stop = [False] - def done(*a): + def done(*a: object) -> None: stop[0] = True root.createfilehandler(inputhook_context.fileno(), _tkinter.READABLE, done) @@ -46,7 +48,7 @@ def done(*a): root.deletefilehandler(inputhook_context.fileno()) - def wait_using_polling(): + def wait_using_polling() -> None: """ Windows TK doesn't support 'createfilehandler'. So, run the TK eventloop and poll until input is ready. @@ -65,7 +67,7 @@ def wait_using_polling(): wait_using_polling() -def inputhook(inputhook_context): +def inputhook(inputhook_context: InputHookContext) -> None: # Only call the real input hook when the 'Tkinter' library was loaded. if "Tkinter" in sys.modules or "tkinter" in sys.modules: _inputhook_tk(inputhook_context) diff --git a/ptpython/history_browser.py b/ptpython/history_browser.py index b7fe0865..08725ee0 100644 --- a/ptpython/history_browser.py +++ b/ptpython/history_browser.py @@ -5,6 +5,7 @@ run as a sub application of the Repl/PythonInput. """ from functools import partial +from typing import TYPE_CHECKING, Callable, List, Optional, Set from prompt_toolkit.application import Application from prompt_toolkit.application.current import get_app @@ -12,8 +13,11 @@ from prompt_toolkit.document import Document from prompt_toolkit.enums import DEFAULT_BUFFER from prompt_toolkit.filters import Condition, has_focus +from prompt_toolkit.formatted_text.base import StyleAndTextTuples from prompt_toolkit.formatted_text.utils import fragment_list_to_text +from prompt_toolkit.history import History from prompt_toolkit.key_binding import KeyBindings +from prompt_toolkit.key_binding.key_processor import KeyPressEvent from prompt_toolkit.layout.containers import ( ConditionalContainer, Container, @@ -24,13 +28,23 @@ VSplit, Window, WindowAlign, + WindowRenderInfo, +) +from prompt_toolkit.layout.controls import ( + BufferControl, + FormattedTextControl, + UIContent, ) -from prompt_toolkit.layout.controls import BufferControl, FormattedTextControl from prompt_toolkit.layout.dimension import Dimension as D from prompt_toolkit.layout.layout import Layout from prompt_toolkit.layout.margins import Margin, ScrollbarMargin -from prompt_toolkit.layout.processors import Processor, Transformation +from prompt_toolkit.layout.processors import ( + Processor, + Transformation, + TransformationInput, +) from prompt_toolkit.lexers import PygmentsLexer +from prompt_toolkit.mouse_events import MouseEvent from prompt_toolkit.widgets import Frame from prompt_toolkit.widgets.toolbars import ArgToolbar, SearchToolbar from pygments.lexers import Python3Lexer as PythonLexer @@ -40,10 +54,15 @@ from .utils import if_mousedown +if TYPE_CHECKING: + from .python_input import PythonInput + HISTORY_COUNT = 2000 __all__ = ["HistoryLayout", "PythonHistory"] +E = KeyPressEvent + HELP_TEXT = """ This interface is meant to select multiple lines from the history and execute them together. @@ -109,7 +128,7 @@ class HistoryLayout: application. """ - def __init__(self, history): + def __init__(self, history: "PythonHistory") -> None: search_toolbar = SearchToolbar() self.help_buffer_control = BufferControl( @@ -201,19 +220,19 @@ def __init__(self, history): self.layout = Layout(self.root_container, history_window) -def _get_top_toolbar_fragments(): +def _get_top_toolbar_fragments() -> StyleAndTextTuples: return [("class:status-bar.title", "History browser - Insert from history")] -def _get_bottom_toolbar_fragments(history): +def _get_bottom_toolbar_fragments(history: "PythonHistory") -> StyleAndTextTuples: python_input = history.python_input @if_mousedown - def f1(mouse_event): + def f1(mouse_event: MouseEvent) -> None: _toggle_help(history) @if_mousedown - def tab(mouse_event): + def tab(mouse_event: MouseEvent) -> None: _select_other_window(history) return ( @@ -239,14 +258,16 @@ class HistoryMargin(Margin): This displays a green bar for the selected entries. """ - def __init__(self, history): + def __init__(self, history: "PythonHistory") -> None: self.history_buffer = history.history_buffer self.history_mapping = history.history_mapping - def get_width(self, ui_content): + def get_width(self, get_ui_content: Callable[[], UIContent]) -> int: return 2 - def create_margin(self, window_render_info, width, height): + def create_margin( + self, window_render_info: WindowRenderInfo, width: int, height: int + ) -> StyleAndTextTuples: document = self.history_buffer.document lines_starting_new_entries = self.history_mapping.lines_starting_new_entries @@ -255,7 +276,7 @@ def create_margin(self, window_render_info, width, height): current_lineno = document.cursor_position_row visible_line_to_input_line = window_render_info.visible_line_to_input_line - result = [] + result: StyleAndTextTuples = [] for y in range(height): line_number = visible_line_to_input_line.get(y) @@ -286,14 +307,16 @@ class ResultMargin(Margin): The margin to be shown in the result pane. """ - def __init__(self, history): + def __init__(self, history: "PythonHistory") -> None: self.history_mapping = history.history_mapping self.history_buffer = history.history_buffer - def get_width(self, ui_content): + def get_width(self, get_ui_content: Callable[[], UIContent]) -> int: return 2 - def create_margin(self, window_render_info, width, height): + def create_margin( + self, window_render_info: WindowRenderInfo, width: int, height: int + ) -> StyleAndTextTuples: document = self.history_buffer.document current_lineno = document.cursor_position_row @@ -303,7 +326,7 @@ def create_margin(self, window_render_info, width, height): visible_line_to_input_line = window_render_info.visible_line_to_input_line - result = [] + result: StyleAndTextTuples = [] for y in range(height): line_number = visible_line_to_input_line.get(y) @@ -324,7 +347,7 @@ def create_margin(self, window_render_info, width, height): return result - def invalidation_hash(self, document): + def invalidation_hash(self, document: Document) -> int: return document.cursor_position_row @@ -333,13 +356,15 @@ class GrayExistingText(Processor): Turn the existing input, before and after the inserted code gray. """ - def __init__(self, history_mapping): + def __init__(self, history_mapping: "HistoryMapping") -> None: self.history_mapping = history_mapping self._lines_before = len( history_mapping.original_document.text_before_cursor.splitlines() ) - def apply_transformation(self, transformation_input): + def apply_transformation( + self, transformation_input: TransformationInput + ) -> Transformation: lineno = transformation_input.lineno fragments = transformation_input.fragments @@ -357,17 +382,22 @@ class HistoryMapping: Keep a list of all the lines from the history and the selected lines. """ - def __init__(self, history, python_history, original_document): + def __init__( + self, + history: "PythonHistory", + python_history: History, + original_document: Document, + ) -> None: self.history = history self.python_history = python_history self.original_document = original_document self.lines_starting_new_entries = set() - self.selected_lines = set() + self.selected_lines: Set[int] = set() # Process history. history_strings = python_history.get_strings() - history_lines = [] + history_lines: List[str] = [] for entry_nr, entry in list(enumerate(history_strings))[-HISTORY_COUNT:]: self.lines_starting_new_entries.add(len(history_lines)) @@ -389,7 +419,7 @@ def __init__(self, history, python_history, original_document): else: self.result_line_offset = 0 - def get_new_document(self, cursor_pos=None): + def get_new_document(self, cursor_pos: Optional[int] = None) -> Document: """ Create a `Document` instance that contains the resulting text. """ @@ -413,13 +443,13 @@ def get_new_document(self, cursor_pos=None): cursor_pos = len(text) return Document(text, cursor_pos) - def update_default_buffer(self): + def update_default_buffer(self) -> None: b = self.history.default_buffer b.set_document(self.get_new_document(b.cursor_position), bypass_readonly=True) -def _toggle_help(history): +def _toggle_help(history: "PythonHistory") -> None: "Display/hide help." help_buffer_control = history.history_layout.help_buffer_control @@ -429,7 +459,7 @@ def _toggle_help(history): history.app.layout.current_control = help_buffer_control -def _select_other_window(history): +def _select_other_window(history: "PythonHistory") -> None: "Toggle focus between left/right window." current_buffer = history.app.current_buffer layout = history.history_layout.layout @@ -441,7 +471,11 @@ def _select_other_window(history): layout.current_control = history.history_layout.history_buffer_control -def create_key_bindings(history, python_input, history_mapping): +def create_key_bindings( + history: "PythonHistory", + python_input: "PythonInput", + history_mapping: HistoryMapping, +) -> KeyBindings: """ Key bindings. """ @@ -449,7 +483,7 @@ def create_key_bindings(history, python_input, history_mapping): handle = bindings.add @handle(" ", filter=has_focus(history.history_buffer)) - def _(event): + def _(event: E) -> None: """ Space: select/deselect line from history pane. """ @@ -486,7 +520,7 @@ def _(event): @handle(" ", filter=has_focus(DEFAULT_BUFFER)) @handle("delete", filter=has_focus(DEFAULT_BUFFER)) @handle("c-h", filter=has_focus(DEFAULT_BUFFER)) - def _(event): + def _(event: E) -> None: """ Space: remove line from default pane. """ @@ -512,17 +546,17 @@ def _(event): @handle("c-x", filter=main_buffer_focussed, eager=True) # Eager: ignore the Emacs [Ctrl-X Ctrl-X] binding. @handle("c-w", filter=main_buffer_focussed) - def _(event): + def _(event: E) -> None: "Select other window." _select_other_window(history) @handle("f4") - def _(event): + def _(event: E) -> None: "Switch between Emacs/Vi mode." python_input.vi_mode = not python_input.vi_mode @handle("f1") - def _(event): + def _(event: E) -> None: "Display/hide help." _toggle_help(history) @@ -530,7 +564,7 @@ def _(event): @handle("c-c", filter=help_focussed) @handle("c-g", filter=help_focussed) @handle("escape", filter=help_focussed) - def _(event): + def _(event: E) -> None: "Leave help." event.app.layout.focus_previous() @@ -538,19 +572,19 @@ def _(event): @handle("f3", filter=main_buffer_focussed) @handle("c-c", filter=main_buffer_focussed) @handle("c-g", filter=main_buffer_focussed) - def _(event): + def _(event: E) -> None: "Cancel and go back." event.app.exit(result=None) @handle("enter", filter=main_buffer_focussed) - def _(event): + def _(event: E) -> None: "Accept input." event.app.exit(result=history.default_buffer.text) enable_system_bindings = Condition(lambda: python_input.enable_system_bindings) @handle("c-z", filter=enable_system_bindings) - def _(event): + def _(event: E) -> None: "Suspend to background." event.app.suspend_to_background() @@ -558,7 +592,9 @@ def _(event): class PythonHistory: - def __init__(self, python_input, original_document): + def __init__( + self, python_input: "PythonInput", original_document: Document + ) -> None: """ Create an `Application` for the history screen. This has to be run as a sub application of `python_input`. @@ -577,12 +613,14 @@ def __init__(self, python_input, original_document): + document.get_start_of_line_position(), ) + def accept_handler(buffer: Buffer) -> bool: + get_app().exit(result=self.default_buffer.text) + return False + self.history_buffer = Buffer( document=document, on_cursor_position_changed=self._history_buffer_pos_changed, - accept_handler=( - lambda buff: get_app().exit(result=self.default_buffer.text) - ), + accept_handler=accept_handler, read_only=True, ) @@ -597,7 +635,7 @@ def __init__(self, python_input, original_document): self.history_layout = HistoryLayout(self) - self.app = Application( + self.app: Application[str] = Application( layout=self.history_layout.layout, full_screen=True, style=python_input._current_style, @@ -605,7 +643,7 @@ def __init__(self, python_input, original_document): key_bindings=create_key_bindings(self, python_input, history_mapping), ) - def _default_buffer_pos_changed(self, _): + def _default_buffer_pos_changed(self, _: Buffer) -> None: """When the cursor changes in the default buffer. Synchronize with history buffer.""" # Only when this buffer has the focus. @@ -629,7 +667,7 @@ def _default_buffer_pos_changed(self, _): ) ) - def _history_buffer_pos_changed(self, _): + def _history_buffer_pos_changed(self, _: Buffer) -> None: """When the cursor changes in the history buffer. Synchronize.""" # Only when this buffer has the focus. if self.app.current_buffer == self.history_buffer: diff --git a/ptpython/ipython.py b/ptpython/ipython.py index 91633340..9eafa995 100644 --- a/ptpython/ipython.py +++ b/ptpython/ipython.py @@ -8,6 +8,7 @@ offer. """ +from typing import Iterable from warnings import warn from IPython import utils as ipy_utils @@ -15,6 +16,7 @@ from IPython.terminal.embed import InteractiveShellEmbed as _InteractiveShellEmbed from IPython.terminal.ipapp import load_default_config from prompt_toolkit.completion import ( + CompleteEvent, Completer, Completion, PathCompleter, @@ -25,15 +27,17 @@ from prompt_toolkit.contrib.regular_languages.completion import GrammarCompleter from prompt_toolkit.contrib.regular_languages.lexer import GrammarLexer from prompt_toolkit.document import Document -from prompt_toolkit.formatted_text import PygmentsTokens +from prompt_toolkit.formatted_text import AnyFormattedText, PygmentsTokens from prompt_toolkit.lexers import PygmentsLexer, SimpleLexer from prompt_toolkit.styles import Style from pygments.lexers import BashLexer, PythonLexer from ptpython.prompt_style import PromptStyle -from .python_input import PythonCompleter, PythonInput, PythonValidator +from .completer import PythonCompleter +from .python_input import PythonInput from .style import default_ui_style +from .validator import PythonValidator __all__ = ["embed"] @@ -46,13 +50,13 @@ class IPythonPrompt(PromptStyle): def __init__(self, prompts): self.prompts = prompts - def in_prompt(self): + def in_prompt(self) -> AnyFormattedText: return PygmentsTokens(self.prompts.in_prompt_tokens()) - def in2_prompt(self, width): + def in2_prompt(self, width: int) -> AnyFormattedText: return PygmentsTokens(self.prompts.continuation_prompt_tokens()) - def out_prompt(self): + def out_prompt(self) -> AnyFormattedText: return [] @@ -61,7 +65,7 @@ def __init__(self, *args, **kwargs): super(IPythonValidator, self).__init__(*args, **kwargs) self.isp = IPythonInputSplitter() - def validate(self, document): + def validate(self, document: Document) -> None: document = Document(text=self.isp.transform_cell(document.text)) super(IPythonValidator, self).validate(document) @@ -142,7 +146,9 @@ class MagicsCompleter(Completer): def __init__(self, magics_manager): self.magics_manager = magics_manager - def get_completions(self, document, complete_event): + def get_completions( + self, document: Document, complete_event: CompleteEvent + ) -> Iterable[Completion]: text = document.text_before_cursor.lstrip() for m in sorted(self.magics_manager.magics["line"]): @@ -154,7 +160,9 @@ class AliasCompleter(Completer): def __init__(self, alias_manager): self.alias_manager = alias_manager - def get_completions(self, document, complete_event): + def get_completions( + self, document: Document, complete_event: CompleteEvent + ) -> Iterable[Completion]: text = document.text_before_cursor.lstrip() # aliases = [a for a, _ in self.alias_manager.aliases] aliases = self.alias_manager.aliases @@ -240,7 +248,7 @@ def get_globals(): self.python_input = python_input - def prompt_for_code(self): + def prompt_for_code(self) -> str: try: return self.python_input.app.run() except KeyboardInterrupt: diff --git a/ptpython/key_bindings.py b/ptpython/key_bindings.py index ae23a3df..147a321d 100644 --- a/ptpython/key_bindings.py +++ b/ptpython/key_bindings.py @@ -1,4 +1,7 @@ +from typing import TYPE_CHECKING + from prompt_toolkit.application import get_app +from prompt_toolkit.buffer import Buffer from prompt_toolkit.document import Document from prompt_toolkit.enums import DEFAULT_BUFFER from prompt_toolkit.filters import ( @@ -11,19 +14,25 @@ ) from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.key_binding.bindings.named_commands import get_by_name +from prompt_toolkit.key_binding.key_processor import KeyPressEvent from prompt_toolkit.keys import Keys from .utils import document_is_multiline_python +if TYPE_CHECKING: + from .python_input import PythonInput + __all__ = [ "load_python_bindings", "load_sidebar_bindings", "load_confirm_exit_bindings", ] +E = KeyPressEvent + @Condition -def tab_should_insert_whitespace(): +def tab_should_insert_whitespace() -> bool: """ When the 'tab' key is pressed with only whitespace character before the cursor, do autocompletion. Otherwise, insert indentation. @@ -38,7 +47,7 @@ def tab_should_insert_whitespace(): return bool(b.text and (not before_cursor or before_cursor.isspace())) -def load_python_bindings(python_input): +def load_python_bindings(python_input: "PythonInput") -> KeyBindings: """ Custom key bindings. """ @@ -48,14 +57,14 @@ def load_python_bindings(python_input): handle = bindings.add @handle("c-l") - def _(event): + def _(event: E) -> None: """ Clear whole screen and render again -- also when the sidebar is visible. """ event.app.renderer.clear() @handle("c-z") - def _(event): + def _(event: E) -> None: """ Suspend. """ @@ -67,7 +76,7 @@ def _(event): handle("c-w")(get_by_name("backward-kill-word")) @handle("f2") - def _(event): + def _(event: E) -> None: """ Show/hide sidebar. """ @@ -78,21 +87,21 @@ def _(event): event.app.layout.focus_last() @handle("f3") - def _(event): + def _(event: E) -> None: """ Select from the history. """ python_input.enter_history() @handle("f4") - def _(event): + def _(event: E) -> None: """ Toggle between Vi and Emacs mode. """ python_input.vi_mode = not python_input.vi_mode @handle("f6") - def _(event): + def _(event: E) -> None: """ Enable/Disable paste mode. """ @@ -101,14 +110,14 @@ def _(event): @handle( "tab", filter=~sidebar_visible & ~has_selection & tab_should_insert_whitespace ) - def _(event): + def _(event: E) -> None: """ When tab should insert whitespace, do that instead of completion. """ event.app.current_buffer.insert_text(" ") @Condition - def is_multiline(): + def is_multiline() -> bool: return document_is_multiline_python(python_input.default_buffer.document) @handle( @@ -120,7 +129,7 @@ def is_multiline(): & ~is_multiline, ) @handle(Keys.Escape, Keys.Enter, filter=~sidebar_visible & emacs_mode) - def _(event): + def _(event: E) -> None: """ Accept input (for single line input). """ @@ -143,7 +152,7 @@ def _(event): & has_focus(DEFAULT_BUFFER) & is_multiline, ) - def _(event): + def _(event: E) -> None: """ Behaviour of the Enter key. @@ -153,11 +162,11 @@ def _(event): b = event.current_buffer empty_lines_required = python_input.accept_input_on_enter or 10000 - def at_the_end(b): + def at_the_end(b: Buffer) -> bool: """we consider the cursor at the end when there is no text after the cursor, or only whitespace.""" text = b.document.text_after_cursor - return text == "" or (text.isspace() and not "\n" in text) + return text == "" or (text.isspace() and "\n" not in text) if python_input.paste_mode: # In paste mode, always insert text. @@ -187,7 +196,7 @@ def at_the_end(b): not get_app().current_buffer.text ), ) - def _(event): + def _(event: E) -> None: """ Override Control-D exit, to ask for confirmation. """ @@ -202,14 +211,14 @@ def _(event): event.app.exit(exception=EOFError) @handle("c-c", filter=has_focus(python_input.default_buffer)) - def _(event): + def _(event: E) -> None: "Abort when Control-C has been pressed." event.app.exit(exception=KeyboardInterrupt, style="class:aborting") return bindings -def load_sidebar_bindings(python_input): +def load_sidebar_bindings(python_input: "PythonInput") -> KeyBindings: """ Load bindings for the navigation in the sidebar. """ @@ -221,7 +230,7 @@ def load_sidebar_bindings(python_input): @handle("up", filter=sidebar_visible) @handle("c-p", filter=sidebar_visible) @handle("k", filter=sidebar_visible) - def _(event): + def _(event: E) -> None: "Go to previous option." python_input.selected_option_index = ( python_input.selected_option_index - 1 @@ -230,7 +239,7 @@ def _(event): @handle("down", filter=sidebar_visible) @handle("c-n", filter=sidebar_visible) @handle("j", filter=sidebar_visible) - def _(event): + def _(event: E) -> None: "Go to next option." python_input.selected_option_index = ( python_input.selected_option_index + 1 @@ -239,14 +248,14 @@ def _(event): @handle("right", filter=sidebar_visible) @handle("l", filter=sidebar_visible) @handle(" ", filter=sidebar_visible) - def _(event): + def _(event: E) -> None: "Select next value for current option." option = python_input.selected_option option.activate_next() @handle("left", filter=sidebar_visible) @handle("h", filter=sidebar_visible) - def _(event): + def _(event: E) -> None: "Select previous value for current option." option = python_input.selected_option option.activate_previous() @@ -256,7 +265,7 @@ def _(event): @handle("c-d", filter=sidebar_visible) @handle("enter", filter=sidebar_visible) @handle("escape", filter=sidebar_visible) - def _(event): + def _(event: E) -> None: "Hide sidebar." python_input.show_sidebar = False event.app.layout.focus_last() @@ -264,7 +273,7 @@ def _(event): return bindings -def load_confirm_exit_bindings(python_input): +def load_confirm_exit_bindings(python_input: "PythonInput") -> KeyBindings: """ Handle yes/no key presses when the exit confirmation is shown. """ @@ -277,14 +286,14 @@ def load_confirm_exit_bindings(python_input): @handle("Y", filter=confirmation_visible) @handle("enter", filter=confirmation_visible) @handle("c-d", filter=confirmation_visible) - def _(event): + def _(event: E) -> None: """ Really quit. """ event.app.exit(exception=EOFError, style="class:exiting") @handle(Keys.Any, filter=confirmation_visible) - def _(event): + def _(event: E) -> None: """ Cancel exit. """ @@ -294,7 +303,7 @@ def _(event): return bindings -def auto_newline(buffer): +def auto_newline(buffer: Buffer) -> None: r""" Insert \n at the cursor position. Also add necessary padding. """ diff --git a/ptpython/layout.py b/ptpython/layout.py index dc6b19bb..365f381b 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -5,7 +5,7 @@ import sys from enum import Enum from inspect import _ParameterKind as ParameterKind -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Any, List, Optional, Type from prompt_toolkit.application import get_app from prompt_toolkit.enums import DEFAULT_BUFFER, SEARCH_BUFFER @@ -15,10 +15,15 @@ is_done, renderer_height_is_known, ) -from prompt_toolkit.formatted_text import fragment_list_width, to_formatted_text +from prompt_toolkit.formatted_text import ( + AnyFormattedText, + fragment_list_width, + to_formatted_text, +) from prompt_toolkit.formatted_text.base import StyleAndTextTuples from prompt_toolkit.key_binding.vi_state import InputMode from prompt_toolkit.layout.containers import ( + AnyContainer, ConditionalContainer, Container, Float, @@ -40,9 +45,10 @@ HighlightIncrementalSearchProcessor, HighlightMatchingBracketProcessor, HighlightSelectionProcessor, + Processor, TabsProcessor, ) -from prompt_toolkit.lexers import SimpleLexer +from prompt_toolkit.lexers import Lexer, SimpleLexer from prompt_toolkit.mouse_events import MouseEvent from prompt_toolkit.selection import SelectionType from prompt_toolkit.widgets.toolbars import ( @@ -55,6 +61,7 @@ from pygments.lexers import PythonLexer from .filters import HasSignature, ShowDocstring, ShowSidebar, ShowSignature +from .prompt_style import PromptStyle from .utils import if_mousedown if TYPE_CHECKING: @@ -98,7 +105,7 @@ def python_sidebar(python_input: "PythonInput") -> Window: def get_text_fragments() -> StyleAndTextTuples: tokens: StyleAndTextTuples = [] - def append_category(category: "OptionCategory") -> None: + def append_category(category: "OptionCategory[Any]") -> None: tokens.extend( [ ("class:sidebar", " "), @@ -150,10 +157,10 @@ def goto_next(mouse_event: MouseEvent) -> None: return tokens class Control(FormattedTextControl): - def move_cursor_down(self): + def move_cursor_down(self) -> None: python_input.selected_option_index += 1 - def move_cursor_up(self): + def move_cursor_up(self) -> None: python_input.selected_option_index -= 1 return Window( @@ -165,12 +172,12 @@ def move_cursor_up(self): ) -def python_sidebar_navigation(python_input): +def python_sidebar_navigation(python_input: "PythonInput") -> Window: """ Create the `Layout` showing the navigation information for the sidebar. """ - def get_text_fragments(): + def get_text_fragments() -> StyleAndTextTuples: # Show navigation info. return [ ("class:sidebar", " "), @@ -191,13 +198,13 @@ def get_text_fragments(): ) -def python_sidebar_help(python_input): +def python_sidebar_help(python_input: "PythonInput") -> Container: """ Create the `Layout` for the help text for the current item in the sidebar. """ token = "class:sidebar.helptext" - def get_current_description(): + def get_current_description() -> str: """ Return the description of the selected option. """ @@ -209,7 +216,7 @@ def get_current_description(): i += 1 return "" - def get_help_text(): + def get_help_text() -> StyleAndTextTuples: return [(token, get_current_description())] return ConditionalContainer( @@ -225,7 +232,7 @@ def get_help_text(): ) -def signature_toolbar(python_input): +def signature_toolbar(python_input: "PythonInput") -> Container: """ Return the `Layout` for the signature. """ @@ -311,21 +318,23 @@ class PythonPromptMargin(PromptMargin): It shows something like "In [1]:". """ - def __init__(self, python_input) -> None: + def __init__(self, python_input: "PythonInput") -> None: self.python_input = python_input - def get_prompt_style(): + def get_prompt_style() -> PromptStyle: return python_input.all_prompt_styles[python_input.prompt_style] def get_prompt() -> StyleAndTextTuples: return to_formatted_text(get_prompt_style().in_prompt()) - def get_continuation(width, line_number, is_soft_wrap): + def get_continuation( + width: int, line_number: int, is_soft_wrap: bool + ) -> StyleAndTextTuples: if python_input.show_line_numbers and not is_soft_wrap: text = ("%i " % (line_number + 1)).rjust(width) return [("class:line-number", text)] else: - return get_prompt_style().in2_prompt(width) + return to_formatted_text(get_prompt_style().in2_prompt(width)) super().__init__(get_prompt, get_continuation) @@ -510,7 +519,7 @@ def get_text_fragments() -> StyleAndTextTuples: def create_exit_confirmation( - python_input: "PythonInput", style="class:exit-confirmation" + python_input: "PythonInput", style: str = "class:exit-confirmation" ) -> Container: """ Create `Layout` for the exit message. @@ -567,22 +576,22 @@ class PtPythonLayout: def __init__( self, python_input: "PythonInput", - lexer=PythonLexer, - extra_body=None, - extra_toolbars=None, - extra_buffer_processors=None, + lexer: Lexer, + extra_body: Optional[AnyContainer] = None, + extra_toolbars: Optional[List[AnyContainer]] = None, + extra_buffer_processors: Optional[List[Processor]] = None, input_buffer_height: Optional[AnyDimension] = None, ) -> None: D = Dimension - extra_body = [extra_body] if extra_body else [] + extra_body_list: List[AnyContainer] = [extra_body] if extra_body else [] extra_toolbars = extra_toolbars or [] - extra_buffer_processors = extra_buffer_processors or [] + input_buffer_height = input_buffer_height or D(min=6) search_toolbar = SearchToolbar(python_input.search_buffer) - def create_python_input_window(): - def menu_position(): + def create_python_input_window() -> Window: + def menu_position() -> Optional[int]: """ When there is no autocompletion menu to be shown, and we have a signature, set the pop-up position at `bracket_start`. @@ -593,6 +602,7 @@ def menu_position(): row, col = python_input.signatures[0].bracket_start index = b.document.translate_row_col_to_index(row - 1, col) return index + return None return Window( BufferControl( @@ -622,7 +632,7 @@ def menu_position(): processor=AppendAutoSuggestion(), filter=~is_done ), ] - + extra_buffer_processors, + + (extra_buffer_processors or []), menu_position=menu_position, # Make sure that we always see the result of an reverse-i-search: preview_search=True, @@ -654,7 +664,7 @@ def menu_position(): [ FloatContainer( content=HSplit( - [create_python_input_window()] + extra_body + [create_python_input_window()] + extra_body_list ), floats=[ Float( diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 1785f523..c5611179 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -6,7 +6,18 @@ from asyncio import get_event_loop from functools import partial -from typing import TYPE_CHECKING, Any, Callable, Dict, Generic, List, Optional, TypeVar +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Dict, + Generic, + List, + Mapping, + Optional, + Tuple, + TypeVar, +) from prompt_toolkit.application import Application, get_app from prompt_toolkit.auto_suggest import ( @@ -44,6 +55,7 @@ load_open_in_editor_bindings, ) from prompt_toolkit.key_binding.vi_state import InputMode +from prompt_toolkit.layout.containers import AnyContainer from prompt_toolkit.lexers import DynamicLexer, Lexer, SimpleLexer from prompt_toolkit.output import ColorDepth, Output from prompt_toolkit.styles import ( @@ -88,8 +100,8 @@ def __lt__(self, __other: Any) -> bool: _T = TypeVar("_T", bound="_SupportsLessThan") -class OptionCategory: - def __init__(self, title: str, options: List["Option"]) -> None: +class OptionCategory(Generic[_T]): + def __init__(self, title: str, options: List["Option[_T]"]) -> None: self.title = title self.options = options @@ -113,7 +125,7 @@ def __init__( get_current_value: Callable[[], _T], # We accept `object` as return type for the select functions, because # often they return an unused boolean. Maybe this can be improved. - get_values: Callable[[], Dict[_T, Callable[[], object]]], + get_values: Callable[[], Mapping[_T, Callable[[], object]]], ) -> None: self.title = title self.description = description @@ -121,7 +133,7 @@ def __init__( self.get_values = get_values @property - def values(self) -> Dict[_T, Callable[[], object]]: + def values(self) -> Mapping[_T, Callable[[], object]]: return self.get_values() def activate_next(self, _previous: bool = False) -> None: @@ -192,12 +204,12 @@ def __init__( output: Optional[Output] = None, # For internal use. extra_key_bindings: Optional[KeyBindings] = None, - create_app=True, + create_app: bool = True, _completer: Optional[Completer] = None, _validator: Optional[Validator] = None, _lexer: Optional[Lexer] = None, _extra_buffer_processors=None, - _extra_layout_body=None, + _extra_layout_body: Optional[AnyContainer] = None, _extra_toolbars=None, _input_buffer_height=None, ) -> None: @@ -239,7 +251,7 @@ def __init__( self.history = InMemoryHistory() self._input_buffer_height = _input_buffer_height - self._extra_layout_body = _extra_layout_body or [] + self._extra_layout_body = _extra_layout_body self._extra_toolbars = _extra_toolbars or [] self._extra_buffer_processors = _extra_buffer_processors or [] @@ -388,7 +400,9 @@ def __init__( # Create an app if requested. If not, the global get_app() is returned # for self.app via property getter. if create_app: - self._app: Optional[Application] = self._create_application(input, output) + self._app: Optional[Application[str]] = self._create_application( + input, output + ) # Setting vi_mode will not work unless the prompt_toolkit # application has been created. if vi_mode: @@ -408,7 +422,7 @@ def option_count(self) -> int: return sum(len(category.options) for category in self.options) @property - def selected_option(self) -> Option: + def selected_option(self) -> Option[Any]: "Return the currently selected option." i = 0 for category in self.options: @@ -514,7 +528,7 @@ def _generate_style(self) -> BaseStyle: self.ui_styles[self._current_ui_style_name], ) - def _create_options(self) -> List[OptionCategory]: + def _create_options(self) -> List[OptionCategory[Any]]: """ Create a list of `Option` instances for the options sidebar. """ @@ -530,15 +544,17 @@ def disable(attribute: str) -> bool: return True def simple_option( - title: str, description: str, field_name: str, values: Optional[List] = None - ) -> Option: + title: str, + description: str, + field_name: str, + values: Tuple[str, str] = ("off", "on"), + ) -> Option[str]: "Create Simple on/of option." - values = values or ["off", "on"] - def get_current_value(): + def get_current_value() -> str: return values[bool(getattr(self, field_name))] - def get_values(): + def get_values() -> Dict[str, Callable[[], bool]]: return { values[1]: lambda: enable(field_name), values[0]: lambda: disable(field_name), @@ -848,7 +864,7 @@ def get_values(): def _create_application( self, input: Optional[Input], output: Optional[Output] - ) -> Application: + ) -> Application[str]: """ Create an `Application` instance. """ @@ -926,7 +942,7 @@ def vi_mode(self, value: bool) -> None: self.editing_mode = EditingMode.EMACS @property - def app(self) -> Application: + def app(self) -> Application[str]: if self._app is None: return get_app() return self._app diff --git a/ptpython/repl.py b/ptpython/repl.py index b55b5d56..3c729c0f 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -44,6 +44,7 @@ from .python_input import PythonInput +PyCF_ALLOW_TOP_LEVEL_AWAIT: int try: from ast import PyCF_ALLOW_TOP_LEVEL_AWAIT # type: ignore except ImportError: @@ -90,7 +91,7 @@ def _load_start_paths(self) -> None: output = self.app.output output.write("WARNING | File not found: {}\n\n".format(path)) - def run_and_show_expression(self, expression): + def run_and_show_expression(self, expression: str) -> None: try: # Eval. try: @@ -135,7 +136,7 @@ def run(self) -> None: text = self.read() except EOFError: return - except BaseException as e: + except BaseException: # Something went wrong while reading input. # (E.g., a bug in the completer that propagates. Don't # crash the REPL.) @@ -149,7 +150,7 @@ def run(self) -> None: clear_title() self._remove_from_namespace() - async def run_and_show_expression_async(self, text): + async def run_and_show_expression_async(self, text: str): loop = asyncio.get_event_loop() try: @@ -349,7 +350,7 @@ def _format_result_output(self, result: object) -> StyleAndTextTuples: if not hasattr(black, "Mode"): raise ImportError except ImportError: - pass # no Black package in your installation + pass # no Black package in your installation else: result_repr = black.format_str( result_repr, @@ -725,17 +726,17 @@ def get_locals(): configure(repl) # Start repl. - patch_context: ContextManager = ( + patch_context: ContextManager[None] = ( patch_stdout_context() if patch_stdout else DummyContext() ) if return_asyncio_coroutine: - async def coroutine(): + async def coroutine() -> None: with patch_context: await repl.run_async() - return coroutine() + return coroutine() # type: ignore else: with patch_context: repl.run() diff --git a/ptpython/signatures.py b/ptpython/signatures.py index 228b99b2..e836d33e 100644 --- a/ptpython/signatures.py +++ b/ptpython/signatures.py @@ -8,13 +8,16 @@ import inspect from inspect import Signature as InspectSignature from inspect import _ParameterKind as ParameterKind -from typing import Any, Dict, List, Optional, Sequence, Tuple +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence, Tuple from prompt_toolkit.document import Document from .completer import DictionaryCompleter from .utils import get_jedi_script_from_document +if TYPE_CHECKING: + import jedi.api.classes + __all__ = ["Signature", "get_signatures_using_jedi", "get_signatures_using_eval"] @@ -120,7 +123,9 @@ def get_annotation_name(annotation: object) -> str: ) @classmethod - def from_jedi_signature(cls, signature) -> "Signature": + def from_jedi_signature( + cls, signature: "jedi.api.classes.Signature" + ) -> "Signature": parameters = [] for p in signature.params: diff --git a/ptpython/utils.py b/ptpython/utils.py index 2fb24a41..ef96ca4b 100644 --- a/ptpython/utils.py +++ b/ptpython/utils.py @@ -2,12 +2,31 @@ For internal use only. """ import re -from typing import Callable, Iterable, Type, TypeVar, cast - +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Dict, + Iterable, + Optional, + Type, + TypeVar, + cast, +) + +from prompt_toolkit.document import Document from prompt_toolkit.formatted_text import to_formatted_text from prompt_toolkit.formatted_text.utils import fragment_list_to_text from prompt_toolkit.mouse_events import MouseEvent, MouseEventType +if TYPE_CHECKING: + from jedi import Interpreter + + # See: prompt_toolkit/key_binding/key_bindings.py + # Annotating these return types as `object` is what works best, because + # `NotImplemented` is typed `Any`. + NotImplementedOrNone = object + __all__ = [ "has_unclosed_brackets", "get_jedi_script_from_document", @@ -45,7 +64,9 @@ def has_unclosed_brackets(text: str) -> bool: return False -def get_jedi_script_from_document(document, locals, globals): +def get_jedi_script_from_document( + document: Document, locals: Dict[str, Any], globals: Dict[str, Any] +) -> "Interpreter": import jedi # We keep this import in-line, to improve start-up time. # Importing Jedi is 'slow'. @@ -78,7 +99,7 @@ def get_jedi_script_from_document(document, locals, globals): _multiline_string_delims = re.compile("""[']{3}|["]{3}""") -def document_is_multiline_python(document): +def document_is_multiline_python(document: Document) -> bool: """ Determine whether this is a multiline Python document. """ @@ -133,7 +154,7 @@ def if_mousedown(handler: _T) -> _T: by the Window.) """ - def handle_if_mouse_down(mouse_event: MouseEvent): + def handle_if_mouse_down(mouse_event: MouseEvent) -> "NotImplementedOrNone": if mouse_event.event_type == MouseEventType.MOUSE_DOWN: return handler(mouse_event) else: @@ -142,7 +163,7 @@ def handle_if_mouse_down(mouse_event: MouseEvent): return cast(_T, handle_if_mouse_down) -_T_type = TypeVar("_T_type", bound=Type) +_T_type = TypeVar("_T_type", bound=type) def ptrepr_to_repr(cls: _T_type) -> _T_type: @@ -154,7 +175,8 @@ def ptrepr_to_repr(cls: _T_type) -> _T_type: "@ptrepr_to_repr can only be applied to classes that have a `__pt_repr__` method." ) - def __repr__(self) -> str: + def __repr__(self: object) -> str: + assert hasattr(cls, "__pt_repr__") return fragment_list_to_text(to_formatted_text(cls.__pt_repr__(self))) cls.__repr__ = __repr__ # type:ignore diff --git a/ptpython/validator.py b/ptpython/validator.py index 0f6a4eaf..ffac5839 100644 --- a/ptpython/validator.py +++ b/ptpython/validator.py @@ -1,3 +1,6 @@ +from typing import Callable, Optional + +from prompt_toolkit.document import Document from prompt_toolkit.validation import ValidationError, Validator from .utils import unindent_code @@ -13,10 +16,10 @@ class PythonValidator(Validator): active compiler flags. """ - def __init__(self, get_compiler_flags=None): + def __init__(self, get_compiler_flags: Optional[Callable[[], int]] = None) -> None: self.get_compiler_flags = get_compiler_flags - def validate(self, document): + def validate(self, document: Document) -> None: """ Check input for Python syntax errors. """ @@ -45,7 +48,7 @@ def validate(self, document): # fixed in Python 3.) # TODO: This is not correct if indentation was removed. index = document.translate_row_col_to_index( - e.lineno - 1, (e.offset or 1) - 1 + (e.lineno or 1) - 1, (e.offset or 1) - 1 ) raise ValidationError(index, f"Syntax Error: {e}") except TypeError as e: diff --git a/setup.cfg b/setup.cfg index 3c6e79cf..80dfec6a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,41 @@ [bdist_wheel] universal=1 + +[flake8] +exclude=__init__.py +max_line_length=150 +ignore= + E114, + E116, + E117, + E121, + E122, + E123, + E125, + E126, + E127, + E128, + E131, + E171, + E203, + E211, + E221, + E227, + E231, + E241, + E251, + E301, + E402, + E501, + E701, + E702, + E704, + E731, + E741, + F401, + F403, + F405, + F811, + W503, + W504, + E722 From 1b7652d3f2fd35ea96789df60063516503811e68 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 22 Nov 2022 09:10:12 +0000 Subject: [PATCH 390/470] Call Filter super() in PythonInputFilter. --- ptpython/filters.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ptpython/filters.py b/ptpython/filters.py index 1adac135..be85edf7 100644 --- a/ptpython/filters.py +++ b/ptpython/filters.py @@ -10,6 +10,7 @@ class PythonInputFilter(Filter): def __init__(self, python_input: "PythonInput") -> None: + super().__init__() self.python_input = python_input def __call__(self) -> bool: From d387b8e559803402105fe25e2c6d41f1beb583c8 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 22 Nov 2022 09:11:22 +0000 Subject: [PATCH 391/470] Update test.yaml workflow. Test on 3.10 and skip mypy on 3.6. --- .github/workflows/test.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 0368ba7b..ef806cff 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.6, 3.7, 3.8, 3.9] + python-version: [3.6, 3.7, 3.8, 3.9, "3.10"] steps: - uses: actions/checkout@v2 @@ -30,6 +30,7 @@ jobs: mypy ptpython isort -c --profile black ptpython examples setup.py black --check ptpython examples setup.py + if: matrix.python-version != '3.6' - name: Run Tests run: | ./tests/run_tests.py From 8bbdc53179085371d4f88380ecb6273e86e6630d Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Fri, 25 Nov 2022 13:56:44 +0000 Subject: [PATCH 392/470] Make ptipython respect more config changes See: https://github.com/prompt-toolkit/ptpython/pull/110 --- ptpython/ipython.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/ptpython/ipython.py b/ptpython/ipython.py index 9eafa995..db2a2049 100644 --- a/ptpython/ipython.py +++ b/ptpython/ipython.py @@ -277,6 +277,25 @@ def initialize_extensions(shell, extensions): shell.showtraceback() +def run_exec_lines(shell, exec_lines): + """ + Partial copy of run_exec_lines code from IPython.core.shellapp . + """ + try: + iter(exec_lines) + except TypeError: + pass + else: + try: + for line in exec_lines: + try: + shell.run_cell(line, store_history=False) + except: + shell.showtraceback() + except: + shell.showtraceback() + + def embed(**kwargs): """ Copied from `IPython/terminal/embed.py`, but using our `InteractiveShellEmbed` instead. @@ -290,6 +309,7 @@ def embed(**kwargs): kwargs["config"] = config shell = InteractiveShellEmbed.instance(**kwargs) initialize_extensions(shell, config["InteractiveShellApp"]["extensions"]) + run_exec_lines(shell, config["InteractiveShellApp"]["exec_lines"]) run_startup_scripts(shell) shell(header=header, stack_depth=2, compile_flags=compile_flags) From 100f4ae839e94dec1170523700e569058ca36aac Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Fri, 25 Nov 2022 14:50:21 +0000 Subject: [PATCH 393/470] Release 3.0.21 --- CHANGELOG | 16 ++++++++++++++++ setup.py | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 69a95e7d..ebc39c9c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,22 @@ CHANGELOG ========= +3.0.21: 2022-11-25 +------------------ + +New features: +- Make ptipython respect more config changes. + (See: https://github.com/prompt-toolkit/ptpython/pull/110 ) +- Improved performance of `DictionaryCompleter` for slow mappings. + +Fixes: +- Call `super()` in `PythonInputFilter`. This will prevent potentially breakage + with an upcoming prompt_toolkit change. + (See: https://github.com/prompt-toolkit/python-prompt-toolkit/pull/1690 ) +- Improved type annotations. +- Added `py.typed` to the `package_data`. + + 3.0.20: 2021-09-14 ------------------ diff --git a/setup.py b/setup.py index a8214f27..274be8ee 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ setup( name="ptpython", author="Jonathan Slenders", - version="3.0.20", + version="3.0.21", url="https://github.com/prompt-toolkit/ptpython", description="Python REPL build on top of prompt_toolkit", long_description=long_description, From b7205ac5657e0edf8a5877a1381a03beb66b9193 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 6 Dec 2022 20:37:12 +0000 Subject: [PATCH 394/470] Improve rendering performance when there are many completions. (Make computing the "meta" text for the completion menu lazy.) --- ptpython/completer.py | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/ptpython/completer.py b/ptpython/completer.py index 2b6795d4..9252106e 100644 --- a/ptpython/completer.py +++ b/ptpython/completer.py @@ -476,14 +476,22 @@ def _get_item_lookup_completions( Complete dictionary keys. """ - def abbr_meta(text: str) -> str: + def meta_repr(value: object) -> Callable[[], str]: "Abbreviate meta text, make sure it fits on one line." - # Take first line, if multiple lines. - if len(text) > 20: - text = text[:20] + "..." - if "\n" in text: - text = text.split("\n", 1)[0] + "..." - return text + # We return a function, so that it gets computed when it's needed. + # When there are many completions, that improves the performance + # quite a bit (for the multi-column completion menu, we only need + # to display one meta text). + def get_value_repr() -> str: + text = self._do_repr(value) + + # Take first line, if multiple lines. + if "\n" in text: + text = text.split("\n", 1)[0] + "..." + + return text + + return get_value_repr match = self.item_lookup_pattern.search(document.text_before_cursor) if match is not None: @@ -512,12 +520,8 @@ def abbr_meta(text: str) -> str: k_repr + "]", -len(key), display=f"[{k_repr}]", - display_meta=abbr_meta(self._do_repr(v)), + display_meta=meta_repr(v), ) - except KeyError: - # `result[k]` lookup failed. Trying to complete - # broken object. - pass except ReprFailedError: pass @@ -532,7 +536,7 @@ def abbr_meta(text: str) -> str: k_repr + "]", -len(key), display=f"[{k_repr}]", - display_meta=abbr_meta(self._do_repr(result[k])), + display_meta=meta_repr(result[k]), ) except KeyError: # `result[k]` lookup failed. Trying to complete From d34704775faa5cd0926cfce9a4dcf3c26d0a178a Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 6 Dec 2022 20:56:40 +0000 Subject: [PATCH 395/470] Remove Python 3.6 from GitHub workflow (not supported anymore). --- .github/workflows/test.yaml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index ef806cff..7ec86626 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.6, 3.7, 3.8, 3.9, "3.10"] + python-version: [3.7, 3.8, 3.9, "3.10"] steps: - uses: actions/checkout@v2 @@ -23,14 +23,12 @@ jobs: sudo apt remove python3-pip python -m pip install --upgrade pip python -m pip install . black isort mypy pytest readme_renderer - python -m pip install . types-dataclasses # Needed for Python 3.6 pip list - name: Type Checker run: | mypy ptpython isort -c --profile black ptpython examples setup.py black --check ptpython examples setup.py - if: matrix.python-version != '3.6' - name: Run Tests run: | ./tests/run_tests.py From b6fbf018ce252cb36dd296f5c93cdeb633c7acf1 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 6 Dec 2022 22:18:45 +0000 Subject: [PATCH 396/470] Release 3.0.22 --- CHANGELOG | 7 +++++++ setup.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index ebc39c9c..916a5422 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,13 @@ CHANGELOG ========= +3.0.22: 2022-12-06 +------------------ + +New features: +- Improve rendering performance when there are many completions. + + 3.0.21: 2022-11-25 ------------------ diff --git a/setup.py b/setup.py index 274be8ee..2725dac4 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ setup( name="ptpython", author="Jonathan Slenders", - version="3.0.21", + version="3.0.22", url="https://github.com/prompt-toolkit/ptpython", description="Python REPL build on top of prompt_toolkit", long_description=long_description, From 7a6b54026611d5ae9f6730cc476dceb79911654d Mon Sep 17 00:00:00 2001 From: Jongwook Choi Date: Wed, 7 Dec 2022 18:31:16 -0500 Subject: [PATCH 397/470] Fix documentation to correct ptpython.ipython import Fixes #506 --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 15464ba4..2db3f695 100644 --- a/README.rst +++ b/README.rst @@ -213,7 +213,7 @@ This is also available for embedding: .. code:: python - from ptpython.ipython.repl import embed + from ptpython.ipython import embed embed(globals(), locals()) From af89ce2e82b09132daa3f6a62961e98d1105fbb3 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 21 Feb 2023 14:51:53 +0000 Subject: [PATCH 398/470] Fix code formatting (new Black version). --- ptpython/completer.py | 5 +---- ptpython/python_input.py | 2 +- ptpython/repl.py | 3 ++- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/ptpython/completer.py b/ptpython/completer.py index 9252106e..95383aaf 100644 --- a/ptpython/completer.py +++ b/ptpython/completer.py @@ -189,7 +189,6 @@ def get_completions( ): # If we are inside a string, Don't do Jedi completion. if not self._path_completer_grammar.match(document.text_before_cursor): - # Do Jedi Python completions. yield from self._jedi_completer.get_completions( document, complete_event @@ -399,7 +398,6 @@ def _lookup(self, expression: str, temp_locals: Dict[str, Any]) -> object: def get_completions( self, document: Document, complete_event: CompleteEvent ) -> Iterable[Completion]: - # First, find all for-loops, and assign the first item of the # collections they're iterating to the iterator variable, so that we # can provide code completion on the iterators. @@ -454,7 +452,6 @@ def _get_expression_completions( result = self.eval_expression(document, temp_locals) if result is not None: - if isinstance( result, (list, tuple, dict, collections_abc.Mapping, collections_abc.Sequence), @@ -478,6 +475,7 @@ def _get_item_lookup_completions( def meta_repr(value: object) -> Callable[[], str]: "Abbreviate meta text, make sure it fits on one line." + # We return a function, so that it gets computed when it's needed. # When there are many completions, that improves the performance # quite a bit (for the multi-column completion menu, we only need @@ -617,7 +615,6 @@ def __init__( def get_completions( self, document: Document, complete_event: CompleteEvent ) -> Iterable[Completion]: - completions = list(self.completer.get_completions(document, complete_event)) complete_private_attributes = self.complete_private_attributes() hide_private = False diff --git a/ptpython/python_input.py b/ptpython/python_input.py index c5611179..1a766c46 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -213,7 +213,6 @@ def __init__( _extra_toolbars=None, _input_buffer_height=None, ) -> None: - self.get_globals: _GetNamespace = get_globals or (lambda: {}) self.get_locals: _GetNamespace = get_locals or self.get_globals @@ -1043,6 +1042,7 @@ def read(self) -> str: This can raise EOFError, when Control-D is pressed. """ + # Capture the current input_mode in order to restore it after reset, # for ViState.reset() sets it to InputMode.INSERT unconditionally and # doesn't accept any arguments. diff --git a/ptpython/repl.py b/ptpython/repl.py index 3c729c0f..604d2b4a 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -96,7 +96,8 @@ def run_and_show_expression(self, expression: str) -> None: # Eval. try: result = self.eval(expression) - except KeyboardInterrupt: # KeyboardInterrupt doesn't inherit from Exception. + except KeyboardInterrupt: + # KeyboardInterrupt doesn't inherit from Exception. raise except SystemExit: raise From 1720189d9870a1059eacf7499b042c5f1ee5cf8d Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 21 Feb 2023 13:57:28 +0000 Subject: [PATCH 399/470] Don't print exception twice in exception handler. The exception formatting itself already prints the exception message. Printing the exception again leads to lots of duplicated output if the exception contains a long multiline message. --- ptpython/repl.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/ptpython/repl.py b/ptpython/repl.py index 604d2b4a..342852ff 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -573,8 +573,6 @@ def _handle_exception(self, e: BaseException) -> None: include_default_pygments_style=False, output=output, ) - - output.write("%s\n" % e) output.flush() def _handle_keyboard_interrupt(self, e: KeyboardInterrupt) -> None: From ec697aa4983085bbfa0cd0bfa78722f6cd1ff5b9 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 21 Feb 2023 16:33:17 +0000 Subject: [PATCH 400/470] Add Python 3.11 to GitHub workflow. --- .github/workflows/test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 7ec86626..31837db3 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.7, 3.8, 3.9, "3.10"] + python-version: [3.7, 3.8, 3.9, "3.10", "3.11"] steps: - uses: actions/checkout@v2 From 3f24501e6e52a10669acf9c37d4dbdee24a00266 Mon Sep 17 00:00:00 2001 From: Itay R <0xItx@users.noreply.github.com> Date: Wed, 22 Feb 2023 12:20:28 +0200 Subject: [PATCH 401/470] Add macOS path to config.py's docstring (#501) --- examples/ptpython_config/config.py | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/ptpython_config/config.py b/examples/ptpython_config/config.py index bf9d05fe..2b51dfc4 100644 --- a/examples/ptpython_config/config.py +++ b/examples/ptpython_config/config.py @@ -3,6 +3,7 @@ Copy this file to $XDG_CONFIG_HOME/ptpython/config.py On Linux, this is: ~/.config/ptpython/config.py +On macOS, this is: ~/Library/Application Support/ptpython/config.py """ from prompt_toolkit.filters import ViInsertMode from prompt_toolkit.key_binding.key_processor import KeyPress From be972abf8d37f0cdc5553a945ef2d22bda4341b1 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 21 Feb 2023 16:46:09 +0000 Subject: [PATCH 402/470] Drop Python 3.6. Now that prompt_toolkit itself dropped Python 3.6 support, we can drop Python 3.6 too. --- ptpython/__init__.py | 2 + ptpython/__main__.py | 2 + ptpython/completer.py | 40 ++++++++-------- ptpython/contrib/asyncssh_repl.py | 4 +- ptpython/entry_points/run_ptipython.py | 4 +- ptpython/entry_points/run_ptpython.py | 6 ++- ptpython/eventloop.py | 2 + ptpython/filters.py | 4 +- ptpython/history_browser.py | 32 ++++++------- ptpython/ipython.py | 6 ++- ptpython/key_bindings.py | 8 ++-- ptpython/layout.py | 46 +++++++++--------- ptpython/lexer.py | 4 +- ptpython/prompt_style.py | 4 +- ptpython/python_input.py | 65 ++++++++++++++------------ ptpython/repl.py | 20 ++++---- ptpython/signatures.py | 24 +++++----- ptpython/style.py | 8 ++-- ptpython/utils.py | 8 ++-- ptpython/validator.py | 4 +- setup.py | 4 +- tests/run_tests.py | 2 + 22 files changed, 169 insertions(+), 130 deletions(-) diff --git a/ptpython/__init__.py b/ptpython/__init__.py index 4908eba8..63c6233d 100644 --- a/ptpython/__init__.py +++ b/ptpython/__init__.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from .repl import embed __all__ = ["embed"] diff --git a/ptpython/__main__.py b/ptpython/__main__.py index 83340a7b..c0062613 100644 --- a/ptpython/__main__.py +++ b/ptpython/__main__.py @@ -1,6 +1,8 @@ """ Make `python -m ptpython` an alias for running `./ptpython`. """ +from __future__ import annotations + from .entry_points.run_ptpython import run run() diff --git a/ptpython/completer.py b/ptpython/completer.py index 95383aaf..f610916e 100644 --- a/ptpython/completer.py +++ b/ptpython/completer.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import ast import collections.abc as collections_abc import inspect @@ -44,8 +46,8 @@ class PythonCompleter(Completer): def __init__( self, - get_globals: Callable[[], Dict[str, Any]], - get_locals: Callable[[], Dict[str, Any]], + get_globals: Callable[[], dict[str, Any]], + get_locals: Callable[[], dict[str, Any]], enable_dictionary_completion: Callable[[], bool], ) -> None: super().__init__() @@ -58,8 +60,8 @@ def __init__( self._jedi_completer = JediCompleter(get_globals, get_locals) self._dictionary_completer = DictionaryCompleter(get_globals, get_locals) - self._path_completer_cache: Optional[GrammarCompleter] = None - self._path_completer_grammar_cache: Optional["_CompiledGrammar"] = None + self._path_completer_cache: GrammarCompleter | None = None + self._path_completer_grammar_cache: _CompiledGrammar | None = None @property def _path_completer(self) -> GrammarCompleter: @@ -74,7 +76,7 @@ def _path_completer(self) -> GrammarCompleter: return self._path_completer_cache @property - def _path_completer_grammar(self) -> "_CompiledGrammar": + def _path_completer_grammar(self) -> _CompiledGrammar: """ Return the grammar for matching paths inside strings inside Python code. @@ -85,7 +87,7 @@ def _path_completer_grammar(self) -> "_CompiledGrammar": self._path_completer_grammar_cache = self._create_path_completer_grammar() return self._path_completer_grammar_cache - def _create_path_completer_grammar(self) -> "_CompiledGrammar": + def _create_path_completer_grammar(self) -> _CompiledGrammar: def unwrapper(text: str) -> str: return re.sub(r"\\(.)", r"\1", text) @@ -202,8 +204,8 @@ class JediCompleter(Completer): def __init__( self, - get_globals: Callable[[], Dict[str, Any]], - get_locals: Callable[[], Dict[str, Any]], + get_globals: Callable[[], dict[str, Any]], + get_locals: Callable[[], dict[str, Any]], ) -> None: super().__init__() @@ -241,7 +243,7 @@ def get_completions( # Jedi issue: "KeyError: u'a_lambda'." # https://github.com/jonathanslenders/ptpython/issues/89 pass - except IOError: + except OSError: # Jedi issue: "IOError: No such file or directory." # https://github.com/jonathanslenders/ptpython/issues/71 pass @@ -302,8 +304,8 @@ class DictionaryCompleter(Completer): def __init__( self, - get_globals: Callable[[], Dict[str, Any]], - get_locals: Callable[[], Dict[str, Any]], + get_globals: Callable[[], dict[str, Any]], + get_locals: Callable[[], dict[str, Any]], ) -> None: super().__init__() @@ -385,7 +387,7 @@ def __init__( re.VERBOSE, ) - def _lookup(self, expression: str, temp_locals: Dict[str, Any]) -> object: + def _lookup(self, expression: str, temp_locals: dict[str, Any]) -> object: """ Do lookup of `object_var` in the context. `temp_locals` is a dictionary, used for the locals. @@ -429,7 +431,7 @@ def _do_repr(self, obj: object) -> str: except BaseException: raise ReprFailedError - def eval_expression(self, document: Document, locals: Dict[str, Any]) -> object: + def eval_expression(self, document: Document, locals: dict[str, Any]) -> object: """ Evaluate """ @@ -444,7 +446,7 @@ def _get_expression_completions( self, document: Document, complete_event: CompleteEvent, - temp_locals: Dict[str, Any], + temp_locals: dict[str, Any], ) -> Iterable[Completion]: """ Complete the [ or . operator after an object. @@ -467,7 +469,7 @@ def _get_item_lookup_completions( self, document: Document, complete_event: CompleteEvent, - temp_locals: Dict[str, Any], + temp_locals: dict[str, Any], ) -> Iterable[Completion]: """ Complete dictionary keys. @@ -547,7 +549,7 @@ def _get_attribute_completions( self, document: Document, complete_event: CompleteEvent, - temp_locals: Dict[str, Any], + temp_locals: dict[str, Any], ) -> Iterable[Completion]: """ Complete attribute names. @@ -579,13 +581,13 @@ def get_suffix(name: str) -> str: suffix = get_suffix(name) yield Completion(name, -len(attr_name), display=name + suffix) - def _sort_attribute_names(self, names: List[str]) -> List[str]: + def _sort_attribute_names(self, names: list[str]) -> list[str]: """ Sort attribute names alphabetically, but move the double underscore and underscore names to the end. """ - def sort_key(name: str) -> Tuple[int, str]: + def sort_key(name: str) -> tuple[int, str]: if name.startswith("__"): return (2, name) # Double underscore comes latest. if name.startswith("_"): @@ -650,7 +652,7 @@ class ReprFailedError(Exception): def _get_style_for_jedi_completion( - jedi_completion: "jedi.api.classes.Completion", + jedi_completion: jedi.api.classes.Completion, ) -> str: """ Return completion style to use for this name. diff --git a/ptpython/contrib/asyncssh_repl.py b/ptpython/contrib/asyncssh_repl.py index 4c36217d..0347aded 100644 --- a/ptpython/contrib/asyncssh_repl.py +++ b/ptpython/contrib/asyncssh_repl.py @@ -6,6 +6,8 @@ should make sure not to use Python 3-only syntax, because this package should be installable in Python 2 as well! """ +from __future__ import annotations + import asyncio from typing import Any, Optional, TextIO, cast @@ -29,7 +31,7 @@ class ReplSSHServerSession(asyncssh.SSHServerSession): """ def __init__( - self, get_globals: _GetNamespace, get_locals: Optional[_GetNamespace] = None + self, get_globals: _GetNamespace, get_locals: _GetNamespace | None = None ) -> None: self._chan: Any = None diff --git a/ptpython/entry_points/run_ptipython.py b/ptpython/entry_points/run_ptipython.py index 21d70637..b660a0ac 100644 --- a/ptpython/entry_points/run_ptipython.py +++ b/ptpython/entry_points/run_ptipython.py @@ -1,4 +1,6 @@ #!/usr/bin/env python +from __future__ import annotations + import os import sys @@ -58,7 +60,7 @@ def run(user_ns=None): code = compile(f.read(), path, "exec") exec(code, user_ns, user_ns) else: - print("File not found: {}\n\n".format(path)) + print(f"File not found: {path}\n\n") sys.exit(1) # Apply config file diff --git a/ptpython/entry_points/run_ptpython.py b/ptpython/entry_points/run_ptpython.py index edffa44d..1b4074d4 100644 --- a/ptpython/entry_points/run_ptpython.py +++ b/ptpython/entry_points/run_ptpython.py @@ -21,6 +21,8 @@ PTPYTHON_CONFIG_HOME: a configuration directory to use PYTHONSTARTUP: file executed on interactive startup (no default) """ +from __future__ import annotations + import argparse import os import pathlib @@ -44,7 +46,7 @@ class _Parser(argparse.ArgumentParser): - def print_help(self, file: Optional[IO[str]] = None) -> None: + def print_help(self, file: IO[str] | None = None) -> None: super().print_help() print( dedent( @@ -90,7 +92,7 @@ def create_parser() -> _Parser: return parser -def get_config_and_history_file(namespace: argparse.Namespace) -> Tuple[str, str]: +def get_config_and_history_file(namespace: argparse.Namespace) -> tuple[str, str]: """ Check which config/history files to use, ensure that the directories for these files exist, and return the config and history path. diff --git a/ptpython/eventloop.py b/ptpython/eventloop.py index 63dd7408..14ab64be 100644 --- a/ptpython/eventloop.py +++ b/ptpython/eventloop.py @@ -7,6 +7,8 @@ in readline. ``prompt-toolkit`` doesn't understand that input hook, but this will fix it for Tk.) """ +from __future__ import annotations + import sys import time diff --git a/ptpython/filters.py b/ptpython/filters.py index be85edf7..a2079fd3 100644 --- a/ptpython/filters.py +++ b/ptpython/filters.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from typing import TYPE_CHECKING from prompt_toolkit.filters import Filter @@ -9,7 +11,7 @@ class PythonInputFilter(Filter): - def __init__(self, python_input: "PythonInput") -> None: + def __init__(self, python_input: PythonInput) -> None: super().__init__() self.python_input = python_input diff --git a/ptpython/history_browser.py b/ptpython/history_browser.py index 08725ee0..81cc63ae 100644 --- a/ptpython/history_browser.py +++ b/ptpython/history_browser.py @@ -4,6 +4,8 @@ `create_history_application` creates an `Application` instance that runs will run as a sub application of the Repl/PythonInput. """ +from __future__ import annotations + from functools import partial from typing import TYPE_CHECKING, Callable, List, Optional, Set @@ -128,7 +130,7 @@ class HistoryLayout: application. """ - def __init__(self, history: "PythonHistory") -> None: + def __init__(self, history: PythonHistory) -> None: search_toolbar = SearchToolbar() self.help_buffer_control = BufferControl( @@ -224,7 +226,7 @@ def _get_top_toolbar_fragments() -> StyleAndTextTuples: return [("class:status-bar.title", "History browser - Insert from history")] -def _get_bottom_toolbar_fragments(history: "PythonHistory") -> StyleAndTextTuples: +def _get_bottom_toolbar_fragments(history: PythonHistory) -> StyleAndTextTuples: python_input = history.python_input @if_mousedown @@ -258,7 +260,7 @@ class HistoryMargin(Margin): This displays a green bar for the selected entries. """ - def __init__(self, history: "PythonHistory") -> None: + def __init__(self, history: PythonHistory) -> None: self.history_buffer = history.history_buffer self.history_mapping = history.history_mapping @@ -307,7 +309,7 @@ class ResultMargin(Margin): The margin to be shown in the result pane. """ - def __init__(self, history: "PythonHistory") -> None: + def __init__(self, history: PythonHistory) -> None: self.history_mapping = history.history_mapping self.history_buffer = history.history_buffer @@ -356,7 +358,7 @@ class GrayExistingText(Processor): Turn the existing input, before and after the inserted code gray. """ - def __init__(self, history_mapping: "HistoryMapping") -> None: + def __init__(self, history_mapping: HistoryMapping) -> None: self.history_mapping = history_mapping self._lines_before = len( history_mapping.original_document.text_before_cursor.splitlines() @@ -384,7 +386,7 @@ class HistoryMapping: def __init__( self, - history: "PythonHistory", + history: PythonHistory, python_history: History, original_document: Document, ) -> None: @@ -393,11 +395,11 @@ def __init__( self.original_document = original_document self.lines_starting_new_entries = set() - self.selected_lines: Set[int] = set() + self.selected_lines: set[int] = set() # Process history. history_strings = python_history.get_strings() - history_lines: List[str] = [] + history_lines: list[str] = [] for entry_nr, entry in list(enumerate(history_strings))[-HISTORY_COUNT:]: self.lines_starting_new_entries.add(len(history_lines)) @@ -419,7 +421,7 @@ def __init__( else: self.result_line_offset = 0 - def get_new_document(self, cursor_pos: Optional[int] = None) -> Document: + def get_new_document(self, cursor_pos: int | None = None) -> Document: """ Create a `Document` instance that contains the resulting text. """ @@ -449,7 +451,7 @@ def update_default_buffer(self) -> None: b.set_document(self.get_new_document(b.cursor_position), bypass_readonly=True) -def _toggle_help(history: "PythonHistory") -> None: +def _toggle_help(history: PythonHistory) -> None: "Display/hide help." help_buffer_control = history.history_layout.help_buffer_control @@ -459,7 +461,7 @@ def _toggle_help(history: "PythonHistory") -> None: history.app.layout.current_control = help_buffer_control -def _select_other_window(history: "PythonHistory") -> None: +def _select_other_window(history: PythonHistory) -> None: "Toggle focus between left/right window." current_buffer = history.app.current_buffer layout = history.history_layout.layout @@ -472,8 +474,8 @@ def _select_other_window(history: "PythonHistory") -> None: def create_key_bindings( - history: "PythonHistory", - python_input: "PythonInput", + history: PythonHistory, + python_input: PythonInput, history_mapping: HistoryMapping, ) -> KeyBindings: """ @@ -592,9 +594,7 @@ def _(event: E) -> None: class PythonHistory: - def __init__( - self, python_input: "PythonInput", original_document: Document - ) -> None: + def __init__(self, python_input: PythonInput, original_document: Document) -> None: """ Create an `Application` for the history screen. This has to be run as a sub application of `python_input`. diff --git a/ptpython/ipython.py b/ptpython/ipython.py index db2a2049..fb4b5ed9 100644 --- a/ptpython/ipython.py +++ b/ptpython/ipython.py @@ -8,6 +8,8 @@ offer. """ +from __future__ import annotations + from typing import Iterable from warnings import warn @@ -62,12 +64,12 @@ def out_prompt(self) -> AnyFormattedText: class IPythonValidator(PythonValidator): def __init__(self, *args, **kwargs): - super(IPythonValidator, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.isp = IPythonInputSplitter() def validate(self, document: Document) -> None: document = Document(text=self.isp.transform_cell(document.text)) - super(IPythonValidator, self).validate(document) + super().validate(document) def create_ipython_grammar(): diff --git a/ptpython/key_bindings.py b/ptpython/key_bindings.py index 147a321d..6b4c1862 100644 --- a/ptpython/key_bindings.py +++ b/ptpython/key_bindings.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from typing import TYPE_CHECKING from prompt_toolkit.application import get_app @@ -47,7 +49,7 @@ def tab_should_insert_whitespace() -> bool: return bool(b.text and (not before_cursor or before_cursor.isspace())) -def load_python_bindings(python_input: "PythonInput") -> KeyBindings: +def load_python_bindings(python_input: PythonInput) -> KeyBindings: """ Custom key bindings. """ @@ -218,7 +220,7 @@ def _(event: E) -> None: return bindings -def load_sidebar_bindings(python_input: "PythonInput") -> KeyBindings: +def load_sidebar_bindings(python_input: PythonInput) -> KeyBindings: """ Load bindings for the navigation in the sidebar. """ @@ -273,7 +275,7 @@ def _(event: E) -> None: return bindings -def load_confirm_exit_bindings(python_input: "PythonInput") -> KeyBindings: +def load_confirm_exit_bindings(python_input: PythonInput) -> KeyBindings: """ Handle yes/no key presses when the exit confirmation is shown. """ diff --git a/ptpython/layout.py b/ptpython/layout.py index 365f381b..2c6395ce 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -1,6 +1,8 @@ """ Creation of the `Layout` instance for the Python input/REPL. """ +from __future__ import annotations + import platform import sys from enum import Enum @@ -78,26 +80,26 @@ class CompletionVisualisation(Enum): TOOLBAR = "toolbar" -def show_completions_toolbar(python_input: "PythonInput") -> Condition: +def show_completions_toolbar(python_input: PythonInput) -> Condition: return Condition( lambda: python_input.completion_visualisation == CompletionVisualisation.TOOLBAR ) -def show_completions_menu(python_input: "PythonInput") -> Condition: +def show_completions_menu(python_input: PythonInput) -> Condition: return Condition( lambda: python_input.completion_visualisation == CompletionVisualisation.POP_UP ) -def show_multi_column_completions_menu(python_input: "PythonInput") -> Condition: +def show_multi_column_completions_menu(python_input: PythonInput) -> Condition: return Condition( lambda: python_input.completion_visualisation == CompletionVisualisation.MULTI_COLUMN ) -def python_sidebar(python_input: "PythonInput") -> Window: +def python_sidebar(python_input: PythonInput) -> Window: """ Create the `Layout` for the sidebar with the configurable options. """ @@ -105,7 +107,7 @@ def python_sidebar(python_input: "PythonInput") -> Window: def get_text_fragments() -> StyleAndTextTuples: tokens: StyleAndTextTuples = [] - def append_category(category: "OptionCategory[Any]") -> None: + def append_category(category: OptionCategory[Any]) -> None: tokens.extend( [ ("class:sidebar", " "), @@ -172,7 +174,7 @@ def move_cursor_up(self) -> None: ) -def python_sidebar_navigation(python_input: "PythonInput") -> Window: +def python_sidebar_navigation(python_input: PythonInput) -> Window: """ Create the `Layout` showing the navigation information for the sidebar. """ @@ -198,7 +200,7 @@ def get_text_fragments() -> StyleAndTextTuples: ) -def python_sidebar_help(python_input: "PythonInput") -> Container: +def python_sidebar_help(python_input: PythonInput) -> Container: """ Create the `Layout` for the help text for the current item in the sidebar. """ @@ -232,7 +234,7 @@ def get_help_text() -> StyleAndTextTuples: ) -def signature_toolbar(python_input: "PythonInput") -> Container: +def signature_toolbar(python_input: PythonInput) -> Container: """ Return the `Layout` for the signature. """ @@ -318,7 +320,7 @@ class PythonPromptMargin(PromptMargin): It shows something like "In [1]:". """ - def __init__(self, python_input: "PythonInput") -> None: + def __init__(self, python_input: PythonInput) -> None: self.python_input = python_input def get_prompt_style() -> PromptStyle: @@ -339,7 +341,7 @@ def get_continuation( super().__init__(get_prompt, get_continuation) -def status_bar(python_input: "PythonInput") -> Container: +def status_bar(python_input: PythonInput) -> Container: """ Create the `Layout` for the status bar. """ @@ -412,7 +414,7 @@ def get_text_fragments() -> StyleAndTextTuples: ) -def get_inputmode_fragments(python_input: "PythonInput") -> StyleAndTextTuples: +def get_inputmode_fragments(python_input: PythonInput) -> StyleAndTextTuples: """ Return current input mode as a list of (token, text) tuples for use in a toolbar. @@ -440,7 +442,7 @@ def toggle_vi_mode(mouse_event: MouseEvent) -> None: recording_register = app.vi_state.recording_register if recording_register: append((token, " ")) - append((token + " class:record", "RECORD({})".format(recording_register))) + append((token + " class:record", f"RECORD({recording_register})")) append((token, " - ")) if app.current_buffer.selection_state is not None: @@ -473,7 +475,7 @@ def toggle_vi_mode(mouse_event: MouseEvent) -> None: return result -def show_sidebar_button_info(python_input: "PythonInput") -> Container: +def show_sidebar_button_info(python_input: PythonInput) -> Container: """ Create `Layout` for the information in the right-bottom corner. (The right part of the status bar.) @@ -519,7 +521,7 @@ def get_text_fragments() -> StyleAndTextTuples: def create_exit_confirmation( - python_input: "PythonInput", style: str = "class:exit-confirmation" + python_input: PythonInput, style: str = "class:exit-confirmation" ) -> Container: """ Create `Layout` for the exit message. @@ -543,7 +545,7 @@ def get_text_fragments() -> StyleAndTextTuples: ) -def meta_enter_message(python_input: "PythonInput") -> Container: +def meta_enter_message(python_input: PythonInput) -> Container: """ Create the `Layout` for the 'Meta+Enter` message. """ @@ -575,15 +577,15 @@ def extra_condition() -> bool: class PtPythonLayout: def __init__( self, - python_input: "PythonInput", + python_input: PythonInput, lexer: Lexer, - extra_body: Optional[AnyContainer] = None, - extra_toolbars: Optional[List[AnyContainer]] = None, - extra_buffer_processors: Optional[List[Processor]] = None, - input_buffer_height: Optional[AnyDimension] = None, + extra_body: AnyContainer | None = None, + extra_toolbars: list[AnyContainer] | None = None, + extra_buffer_processors: list[Processor] | None = None, + input_buffer_height: AnyDimension | None = None, ) -> None: D = Dimension - extra_body_list: List[AnyContainer] = [extra_body] if extra_body else [] + extra_body_list: list[AnyContainer] = [extra_body] if extra_body else [] extra_toolbars = extra_toolbars or [] input_buffer_height = input_buffer_height or D(min=6) @@ -591,7 +593,7 @@ def __init__( search_toolbar = SearchToolbar(python_input.search_buffer) def create_python_input_window() -> Window: - def menu_position() -> Optional[int]: + def menu_position() -> int | None: """ When there is no autocompletion menu to be shown, and we have a signature, set the pop-up position at `bracket_start`. diff --git a/ptpython/lexer.py b/ptpython/lexer.py index 62e470f8..81924c9d 100644 --- a/ptpython/lexer.py +++ b/ptpython/lexer.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from typing import Callable, Optional from prompt_toolkit.document import Document @@ -17,7 +19,7 @@ class PtpythonLexer(Lexer): use a Python 3 lexer. """ - def __init__(self, python_lexer: Optional[Lexer] = None) -> None: + def __init__(self, python_lexer: Lexer | None = None) -> None: self.python_lexer = python_lexer or PygmentsLexer(PythonLexer) self.system_lexer = PygmentsLexer(BashLexer) diff --git a/ptpython/prompt_style.py b/ptpython/prompt_style.py index e7334af2..96b738f7 100644 --- a/ptpython/prompt_style.py +++ b/ptpython/prompt_style.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from abc import ABCMeta, abstractmethod from typing import TYPE_CHECKING @@ -40,7 +42,7 @@ class IPythonPrompt(PromptStyle): A prompt resembling the IPython prompt. """ - def __init__(self, python_input: "PythonInput") -> None: + def __init__(self, python_input: PythonInput) -> None: self.python_input = python_input def in_prompt(self) -> AnyFormattedText: diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 1a766c46..e8170f2b 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -2,7 +2,7 @@ Application for reading Python input. This can be used for creation of Python REPLs. """ -import __future__ +from __future__ import annotations from asyncio import get_event_loop from functools import partial @@ -84,6 +84,11 @@ from .utils import unindent_code from .validator import PythonValidator +# Isort introduces a SyntaxError, if we'd write `import __future__`. +# https://github.com/PyCQA/isort/issues/2100 +__future__ = __import__("__future__") + + __all__ = ["PythonInput"] @@ -101,7 +106,7 @@ def __lt__(self, __other: Any) -> bool: class OptionCategory(Generic[_T]): - def __init__(self, title: str, options: List["Option[_T]"]) -> None: + def __init__(self, title: str, options: list[Option[_T]]) -> None: self.title = title self.options = options @@ -194,22 +199,22 @@ class PythonInput: def __init__( self, - get_globals: Optional[_GetNamespace] = None, - get_locals: Optional[_GetNamespace] = None, - history_filename: Optional[str] = None, + get_globals: _GetNamespace | None = None, + get_locals: _GetNamespace | None = None, + history_filename: str | None = None, vi_mode: bool = False, - color_depth: Optional[ColorDepth] = None, + color_depth: ColorDepth | None = None, # Input/output. - input: Optional[Input] = None, - output: Optional[Output] = None, + input: Input | None = None, + output: Output | None = None, # For internal use. - extra_key_bindings: Optional[KeyBindings] = None, + extra_key_bindings: KeyBindings | None = None, create_app: bool = True, - _completer: Optional[Completer] = None, - _validator: Optional[Validator] = None, - _lexer: Optional[Lexer] = None, + _completer: Completer | None = None, + _validator: Validator | None = None, + _lexer: Lexer | None = None, _extra_buffer_processors=None, - _extra_layout_body: Optional[AnyContainer] = None, + _extra_layout_body: AnyContainer | None = None, _extra_toolbars=None, _input_buffer_height=None, ) -> None: @@ -309,7 +314,7 @@ def __init__( self.show_exit_confirmation: bool = False # The title to be displayed in the terminal. (None or string.) - self.terminal_title: Optional[str] = None + self.terminal_title: str | None = None self.exit_message: str = "Do you really want to exit?" self.insert_blank_line_after_output: bool = True # (For the REPL.) @@ -324,7 +329,7 @@ def __init__( self.prompt_style: str = "classic" # The currently active style. # Styles selectable from the menu. - self.all_prompt_styles: Dict[str, PromptStyle] = { + self.all_prompt_styles: dict[str, PromptStyle] = { "ipython": IPythonPrompt(self), "classic": ClassicPrompt(), } @@ -338,7 +343,7 @@ def __init__( ].out_prompt() #: Load styles. - self.code_styles: Dict[str, BaseStyle] = get_all_code_styles() + self.code_styles: dict[str, BaseStyle] = get_all_code_styles() self.ui_styles = get_all_ui_styles() self._current_code_style_name: str = "default" self._current_ui_style_name: str = "default" @@ -360,7 +365,7 @@ def __init__( self.current_statement_index: int = 1 # Code signatures. (This is set asynchronously after a timeout.) - self.signatures: List[Signature] = [] + self.signatures: list[Signature] = [] # Boolean indicating whether we have a signatures thread running. # (Never run more than one at the same time.) @@ -399,9 +404,7 @@ def __init__( # Create an app if requested. If not, the global get_app() is returned # for self.app via property getter. if create_app: - self._app: Optional[Application[str]] = self._create_application( - input, output - ) + self._app: Application[str] | None = self._create_application(input, output) # Setting vi_mode will not work unless the prompt_toolkit # application has been created. if vi_mode: @@ -527,7 +530,7 @@ def _generate_style(self) -> BaseStyle: self.ui_styles[self._current_ui_style_name], ) - def _create_options(self) -> List[OptionCategory[Any]]: + def _create_options(self) -> list[OptionCategory[Any]]: """ Create a list of `Option` instances for the options sidebar. """ @@ -546,14 +549,14 @@ def simple_option( title: str, description: str, field_name: str, - values: Tuple[str, str] = ("off", "on"), + values: tuple[str, str] = ("off", "on"), ) -> Option[str]: "Create Simple on/of option." def get_current_value() -> str: return values[bool(getattr(self, field_name))] - def get_values() -> Dict[str, Callable[[], bool]]: + def get_values() -> dict[str, Callable[[], bool]]: return { values[1]: lambda: enable(field_name), values[0]: lambda: disable(field_name), @@ -730,10 +733,10 @@ def get_values() -> Dict[str, Callable[[], bool]]: title="Prompt", description="Visualisation of the prompt. ('>>>' or 'In [1]:')", get_current_value=lambda: self.prompt_style, - get_values=lambda: dict( - (s, partial(enable, "prompt_style", s)) + get_values=lambda: { + s: partial(enable, "prompt_style", s) for s in self.all_prompt_styles - ), + }, ), simple_option( title="Blank line after input", @@ -825,10 +828,10 @@ def get_values() -> Dict[str, Callable[[], bool]]: title="User interface", description="Color scheme to use for the user interface.", get_current_value=lambda: self._current_ui_style_name, - get_values=lambda: dict( - (name, partial(self.use_ui_colorscheme, name)) + get_values=lambda: { + name: partial(self.use_ui_colorscheme, name) for name in self.ui_styles - ), + }, ), Option( title="Color depth", @@ -862,7 +865,7 @@ def get_values() -> Dict[str, Callable[[], bool]]: ] def _create_application( - self, input: Optional[Input], output: Optional[Output] + self, input: Input | None, output: Output | None ) -> Application[str]: """ Create an `Application` instance. @@ -952,7 +955,7 @@ def _on_input_timeout(self, buff: Buffer) -> None: in another thread, get the signature of the current code. """ - def get_signatures_in_executor(document: Document) -> List[Signature]: + def get_signatures_in_executor(document: Document) -> list[Signature]: # First, get signatures from Jedi. If we didn't found any and if # "dictionary completion" (eval-based completion) is enabled, then # get signatures using eval. diff --git a/ptpython/repl.py b/ptpython/repl.py index 342852ff..a3dd788e 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -7,6 +7,8 @@ embed(globals(), locals(), vi_mode=False) """ +from __future__ import annotations + import asyncio import builtins import os @@ -53,7 +55,7 @@ __all__ = ["PythonRepl", "enable_deprecation_warnings", "run_config", "embed"] -def _get_coroutine_flag() -> Optional[int]: +def _get_coroutine_flag() -> int | None: for k, v in COMPILER_FLAG_NAMES.items(): if v == "COROUTINE": return k @@ -62,7 +64,7 @@ def _get_coroutine_flag() -> Optional[int]: return None -COROUTINE_FLAG: Optional[int] = _get_coroutine_flag() +COROUTINE_FLAG: int | None = _get_coroutine_flag() def _has_coroutine_flag(code: types.CodeType) -> bool: @@ -89,7 +91,7 @@ def _load_start_paths(self) -> None: exec(code, self.get_globals(), self.get_locals()) else: output = self.app.output - output.write("WARNING | File not found: {}\n\n".format(path)) + output.write(f"WARNING | File not found: {path}\n\n") def run_and_show_expression(self, expression: str) -> None: try: @@ -300,7 +302,7 @@ async def eval_async(self, line: str) -> object: return None def _store_eval_result(self, result: object) -> None: - locals: Dict[str, Any] = self.get_locals() + locals: dict[str, Any] = self.get_locals() locals["_"] = locals["_%i" % self.current_statement_index] = result def get_compiler_flags(self) -> int: @@ -524,7 +526,7 @@ def show_pager() -> None: flush_page() - def create_pager_prompt(self) -> PromptSession["PagerResult"]: + def create_pager_prompt(self) -> PromptSession[PagerResult]: """ Create pager --MORE-- prompt. """ @@ -651,7 +653,7 @@ def enter_to_continue() -> None: # Run the config file in an empty namespace. try: - namespace: Dict[str, Any] = {} + namespace: dict[str, Any] = {} with open(config_file, "rb") as f: code = compile(f.read(), config_file, "exec") @@ -670,10 +672,10 @@ def enter_to_continue() -> None: def embed( globals=None, locals=None, - configure: Optional[Callable[[PythonRepl], None]] = None, + configure: Callable[[PythonRepl], None] | None = None, vi_mode: bool = False, - history_filename: Optional[str] = None, - title: Optional[str] = None, + history_filename: str | None = None, + title: str | None = None, startup_paths=None, patch_stdout: bool = False, return_asyncio_coroutine: bool = False, diff --git a/ptpython/signatures.py b/ptpython/signatures.py index e836d33e..5a6f286a 100644 --- a/ptpython/signatures.py +++ b/ptpython/signatures.py @@ -5,6 +5,8 @@ Either with the Jedi library, or using `inspect.signature` if Jedi fails and we can use `eval()` to evaluate the function object. """ +from __future__ import annotations + import inspect from inspect import Signature as InspectSignature from inspect import _ParameterKind as ParameterKind @@ -25,8 +27,8 @@ class Parameter: def __init__( self, name: str, - annotation: Optional[str], - default: Optional[str], + annotation: str | None, + default: str | None, kind: ParameterKind, ) -> None: self.name = name @@ -66,9 +68,9 @@ def __init__( name: str, docstring: str, parameters: Sequence[Parameter], - index: Optional[int] = None, + index: int | None = None, returns: str = "", - bracket_start: Tuple[int, int] = (0, 0), + bracket_start: tuple[int, int] = (0, 0), ) -> None: self.name = name self.docstring = docstring @@ -84,7 +86,7 @@ def from_inspect_signature( docstring: str, signature: InspectSignature, index: int, - ) -> "Signature": + ) -> Signature: parameters = [] def get_annotation_name(annotation: object) -> str: @@ -123,9 +125,7 @@ def get_annotation_name(annotation: object) -> str: ) @classmethod - def from_jedi_signature( - cls, signature: "jedi.api.classes.Signature" - ) -> "Signature": + def from_jedi_signature(cls, signature: jedi.api.classes.Signature) -> Signature: parameters = [] for p in signature.params: @@ -160,8 +160,8 @@ def __repr__(self) -> str: def get_signatures_using_jedi( - document: Document, locals: Dict[str, Any], globals: Dict[str, Any] -) -> List[Signature]: + document: Document, locals: dict[str, Any], globals: dict[str, Any] +) -> list[Signature]: script = get_jedi_script_from_document(document, locals, globals) # Show signatures in help text. @@ -195,8 +195,8 @@ def get_signatures_using_jedi( def get_signatures_using_eval( - document: Document, locals: Dict[str, Any], globals: Dict[str, Any] -) -> List[Signature]: + document: Document, locals: dict[str, Any], globals: dict[str, Any] +) -> list[Signature]: """ Look for the signature of the function before the cursor position without use of Jedi. This uses a similar approach as the `DictionaryCompleter` of diff --git a/ptpython/style.py b/ptpython/style.py index 4b54d0cd..199d5abf 100644 --- a/ptpython/style.py +++ b/ptpython/style.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from typing import Dict from prompt_toolkit.styles import BaseStyle, Style, merge_styles @@ -8,11 +10,11 @@ __all__ = ["get_all_code_styles", "get_all_ui_styles", "generate_style"] -def get_all_code_styles() -> Dict[str, BaseStyle]: +def get_all_code_styles() -> dict[str, BaseStyle]: """ Return a mapping from style names to their classes. """ - result: Dict[str, BaseStyle] = { + result: dict[str, BaseStyle] = { name: style_from_pygments_cls(get_style_by_name(name)) for name in get_all_styles() } @@ -20,7 +22,7 @@ def get_all_code_styles() -> Dict[str, BaseStyle]: return result -def get_all_ui_styles() -> Dict[str, BaseStyle]: +def get_all_ui_styles() -> dict[str, BaseStyle]: """ Return a dict mapping {ui_style_name -> style_dict}. """ diff --git a/ptpython/utils.py b/ptpython/utils.py index ef96ca4b..53488997 100644 --- a/ptpython/utils.py +++ b/ptpython/utils.py @@ -1,6 +1,8 @@ """ For internal use only. """ +from __future__ import annotations + import re from typing import ( TYPE_CHECKING, @@ -65,8 +67,8 @@ def has_unclosed_brackets(text: str) -> bool: def get_jedi_script_from_document( - document: Document, locals: Dict[str, Any], globals: Dict[str, Any] -) -> "Interpreter": + document: Document, locals: dict[str, Any], globals: dict[str, Any] +) -> Interpreter: import jedi # We keep this import in-line, to improve start-up time. # Importing Jedi is 'slow'. @@ -154,7 +156,7 @@ def if_mousedown(handler: _T) -> _T: by the Window.) """ - def handle_if_mouse_down(mouse_event: MouseEvent) -> "NotImplementedOrNone": + def handle_if_mouse_down(mouse_event: MouseEvent) -> NotImplementedOrNone: if mouse_event.event_type == MouseEventType.MOUSE_DOWN: return handler(mouse_event) else: diff --git a/ptpython/validator.py b/ptpython/validator.py index ffac5839..3b36d273 100644 --- a/ptpython/validator.py +++ b/ptpython/validator.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from typing import Callable, Optional from prompt_toolkit.document import Document @@ -16,7 +18,7 @@ class PythonValidator(Validator): active compiler flags. """ - def __init__(self, get_compiler_flags: Optional[Callable[[], int]] = None) -> None: + def __init__(self, get_compiler_flags: Callable[[], int] | None = None) -> None: self.get_compiler_flags = get_compiler_flags def validate(self, document: Document) -> None: diff --git a/setup.py b/setup.py index 2725dac4..c4087f9c 100644 --- a/setup.py +++ b/setup.py @@ -25,10 +25,10 @@ "prompt_toolkit>=3.0.18,<3.1.0", "pygments", ], - python_requires=">=3.6", + python_requires=">=3.7", classifiers=[ "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3 :: Only", diff --git a/tests/run_tests.py b/tests/run_tests.py index 2f945163..0de37430 100755 --- a/tests/run_tests.py +++ b/tests/run_tests.py @@ -1,4 +1,6 @@ #!/usr/bin/env python +from __future__ import annotations + import unittest import ptpython.completer From 2d4b0b0d04973e49cf0fe35a71e62c4ca486eed1 Mon Sep 17 00:00:00 2001 From: Tim Gates Date: Sun, 17 Jul 2022 08:46:58 +1000 Subject: [PATCH 403/470] docs: Fix a few typos There are small typos in: - docs/concurrency-challenges.rst - examples/ptpython_config/config.py - ptpython/completer.py - ptpython/history_browser.py - ptpython/key_bindings.py - ptpython/repl.py Fixes: - Should read `returns` rather than `retuns`. - Should read `parentheses` rather than `parethesis`. - Should read `output` rather than `ouptut`. - Should read `navigation` rather than `navigaton`. - Should read `executor` rather than `excecutor`. - Should read `depending` rather than `deponding`. Signed-off-by: Tim Gates --- docs/concurrency-challenges.rst | 2 +- examples/ptpython_config/config.py | 2 +- ptpython/completer.py | 2 +- ptpython/history_browser.py | 2 +- ptpython/key_bindings.py | 2 +- ptpython/repl.py | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/concurrency-challenges.rst b/docs/concurrency-challenges.rst index b56d9698..0ff9c6c3 100644 --- a/docs/concurrency-challenges.rst +++ b/docs/concurrency-challenges.rst @@ -67,7 +67,7 @@ When a normal blocking embed is used: When an awaitable embed is used, for embedding in a coroutine, but having the event loop continue: * We run the input method from the blocking embed in an asyncio executor - and do an `await loop.run_in_excecutor(...)`. + and do an `await loop.run_in_executor(...)`. * The "eval" happens again in the main thread. * "print" is also similar, except that the pager code (if used) runs in an executor too. diff --git a/examples/ptpython_config/config.py b/examples/ptpython_config/config.py index 2b51dfc4..2f3f49dd 100644 --- a/examples/ptpython_config/config.py +++ b/examples/ptpython_config/config.py @@ -50,7 +50,7 @@ def configure(repl): # Swap light/dark colors on or off repl.swap_light_and_dark = False - # Highlight matching parethesis. + # Highlight matching parentheses. repl.highlight_matching_parenthesis = True # Line wrapping. (Instead of horizontal scrolling.) diff --git a/ptpython/completer.py b/ptpython/completer.py index f610916e..3c5dd32f 100644 --- a/ptpython/completer.py +++ b/ptpython/completer.py @@ -599,7 +599,7 @@ def sort_key(name: str) -> tuple[int, str]: class HidePrivateCompleter(Completer): """ - Wrapper around completer that hides private fields, deponding on whether or + Wrapper around completer that hides private fields, depending on whether or not public fields are shown. (The reason this is implemented as a `Completer` wrapper is because this diff --git a/ptpython/history_browser.py b/ptpython/history_browser.py index 81cc63ae..eea81c2e 100644 --- a/ptpython/history_browser.py +++ b/ptpython/history_browser.py @@ -599,7 +599,7 @@ def __init__(self, python_input: PythonInput, original_document: Document) -> No Create an `Application` for the history screen. This has to be run as a sub application of `python_input`. - When this application runs and returns, it retuns the selected lines. + When this application runs and returns, it returns the selected lines. """ self.python_input = python_input diff --git a/ptpython/key_bindings.py b/ptpython/key_bindings.py index 6b4c1862..d7bb575e 100644 --- a/ptpython/key_bindings.py +++ b/ptpython/key_bindings.py @@ -159,7 +159,7 @@ def _(event: E) -> None: Behaviour of the Enter key. Auto indent after newline/Enter. - (When not in Vi navigaton mode, and when multiline is enabled.) + (When not in Vi navigation mode, and when multiline is enabled.) """ b = event.current_buffer empty_lines_required = python_input.accept_input_on_enter or 10000 diff --git a/ptpython/repl.py b/ptpython/repl.py index a3dd788e..02a5075d 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -405,7 +405,7 @@ def _format_result_output(self, result: object) -> StyleAndTextTuples: def show_result(self, result: object) -> None: """ - Show __repr__ for an `eval` result and print to ouptut. + Show __repr__ for an `eval` result and print to output. """ formatted_text_output = self._format_result_output(result) From ea6b2c51db96e260b2ce32574938bd844f7a01ce Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Wed, 22 Feb 2023 10:35:43 +0000 Subject: [PATCH 404/470] Fix completer suffix for mappings/sequences. --- ptpython/completer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ptpython/completer.py b/ptpython/completer.py index 3c5dd32f..f28d2b16 100644 --- a/ptpython/completer.py +++ b/ptpython/completer.py @@ -568,9 +568,9 @@ def get_suffix(name: str) -> str: obj = getattr(result, name, None) if inspect.isfunction(obj) or inspect.ismethod(obj): return "()" - if isinstance(obj, dict): + if isinstance(obj, collections_abc.Mapping): return "{}" - if isinstance(obj, (list, tuple)): + if isinstance(obj, collections_abc.Sequence): return "[]" except: pass From ee047a2701fcd269592a626e947bf9625db5eb6d Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Wed, 22 Feb 2023 10:36:12 +0000 Subject: [PATCH 405/470] Add cursor shape support. --- ptpython/layout.py | 2 +- ptpython/python_input.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/ptpython/layout.py b/ptpython/layout.py index 2c6395ce..d15e52e2 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -151,7 +151,7 @@ def goto_next(mouse_event: MouseEvent) -> None: append_category(category) for option in category.options: - append(i, option.title, "%s" % option.get_current_value()) + append(i, option.title, "%s" % (option.get_current_value(),)) i += 1 tokens.pop() # Remove last newline. diff --git a/ptpython/python_input.py b/ptpython/python_input.py index e8170f2b..da19076b 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -34,6 +34,12 @@ ThreadedCompleter, merge_completers, ) +from prompt_toolkit.cursor_shapes import ( + AnyCursorShapeConfig, + CursorShape, + DynamicCursorShapeConfig, + ModalCursorShapeConfig, +) from prompt_toolkit.document import Document from prompt_toolkit.enums import DEFAULT_BUFFER, EditingMode from prompt_toolkit.filters import Condition @@ -325,6 +331,18 @@ def __init__( self.search_buffer: Buffer = Buffer() self.docstring_buffer: Buffer = Buffer(read_only=True) + # Cursor shapes. + self.cursor_shape_config = "Block" + self.all_cursor_shape_configs: Dict[str, AnyCursorShapeConfig] = { + "Block": CursorShape.BLOCK, + "Underline": CursorShape.UNDERLINE, + "Beam": CursorShape.BEAM, + "Modal (vi)": ModalCursorShapeConfig(), + "Blink block": CursorShape.BLINKING_BLOCK, + "Blink under": CursorShape.BLINKING_UNDERLINE, + "Blink beam": CursorShape.BLINKING_BEAM, + } + # Tokens to be shown at the prompt. self.prompt_style: str = "classic" # The currently active style. @@ -584,6 +602,16 @@ def get_values() -> dict[str, Callable[[], bool]]: "Vi": lambda: enable("vi_mode"), }, ), + Option( + title="Cursor shape", + description="Change the cursor style, possibly according " + "to the Vi input mode.", + get_current_value=lambda: self.cursor_shape_config, + get_values=lambda: dict( + (s, partial(enable, "cursor_shape_config", s)) + for s in self.all_cursor_shape_configs + ), + ), simple_option( title="Paste mode", description="When enabled, don't indent automatically.", @@ -896,6 +924,9 @@ def _create_application( style_transformation=self.style_transformation, include_default_pygments_style=False, reverse_vi_search_direction=True, + cursor=DynamicCursorShapeConfig( + lambda: self.all_cursor_shape_configs[self.cursor_shape_config] + ), input=input, output=output, ) From f8399dd5a13d4bb8e5cd98365f2435cdfaf628a8 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Wed, 22 Feb 2023 10:52:04 +0000 Subject: [PATCH 406/470] Set minimum prompt_toolkit version to 3.0.28, because of cursor shape support. --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index c4087f9c..ce5be98d 100644 --- a/setup.py +++ b/setup.py @@ -21,8 +21,8 @@ "appdirs", "importlib_metadata;python_version<'3.8'", "jedi>=0.16.0", - # Use prompt_toolkit 3.0.18, because of the `in_thread` option. - "prompt_toolkit>=3.0.18,<3.1.0", + # Use prompt_toolkit 3.0.28, because of cursor shape support. + "prompt_toolkit>=3.0.28,<3.1.0", "pygments", ], python_requires=">=3.7", From 44f0c6e57d616d41de458daccbf36e8d8eb5fb3d Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Wed, 22 Feb 2023 10:52:15 +0000 Subject: [PATCH 407/470] Release 3.0.23 --- CHANGELOG | 11 +++++++++++ setup.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 916a5422..645ca60b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,17 @@ CHANGELOG ========= +3.0.23: 2023-02-22 +------------------ + +Fixes: +- Don't print exception messages twice for unhandled exceptions. +- Added cursor shape support. + +Breaking changes: +- Drop Python 3.6 support. + + 3.0.22: 2022-12-06 ------------------ diff --git a/setup.py b/setup.py index ce5be98d..18d2911a 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ setup( name="ptpython", author="Jonathan Slenders", - version="3.0.22", + version="3.0.23", url="https://github.com/prompt-toolkit/ptpython", description="Python REPL build on top of prompt_toolkit", long_description=long_description, From 91d2c3589310452a0f79f2fa1a4a6847fc095481 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Wed, 12 Apr 2023 15:07:11 +0000 Subject: [PATCH 408/470] Fix various typos. --- CHANGELOG | 2 +- ptpython/completer.py | 6 +++--- ptpython/python_input.py | 4 ++-- ptpython/utils.py | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 645ca60b..e753cfd9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -191,7 +191,7 @@ New features: - Optional pager for displaying outputs that don't fit on the screen. - Added --light-bg and --dark-bg flags to automatically optimize the brightness of the colors according to the terminal background. -- Addd `PTPYTHON_CONFIG_HOME` for explicitely setting the config directory. +- Add `PTPYTHON_CONFIG_HOME` for explicitly setting the config directory. - Show completion suffixes (like '(' for functions). Fixes: diff --git a/ptpython/completer.py b/ptpython/completer.py index f28d2b16..85a96d7c 100644 --- a/ptpython/completer.py +++ b/ptpython/completer.py @@ -259,7 +259,7 @@ def get_completions( # See: https://github.com/jonathanslenders/ptpython/issues/223 pass except Exception: - # Supress all other Jedi exceptions. + # Suppress all other Jedi exceptions. pass else: # Move function parameters to the top. @@ -367,7 +367,7 @@ def __init__( rf""" {expression} - # Dict loopup to complete (square bracket open + start of + # Dict lookup to complete (square bracket open + start of # string). \[ \s* ([^\[\]]*)$ @@ -380,7 +380,7 @@ def __init__( rf""" {expression} - # Attribute loopup to complete (dot + varname). + # Attribute lookup to complete (dot + varname). \. \s* ([a-zA-Z0-9_]*)$ """, diff --git a/ptpython/python_input.py b/ptpython/python_input.py index da19076b..0c7fef6f 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -379,7 +379,7 @@ def __init__( self.options = self._create_options() self.selected_option_index: int = 0 - #: Incremeting integer counting the current statement. + #: Incrementing integer counting the current statement. self.current_statement_index: int = 1 # Code signatures. (This is set asynchronously after a timeout.) @@ -835,7 +835,7 @@ def get_values() -> dict[str, Callable[[], bool]]: [ simple_option( title="Syntax highlighting", - description="Use colors for syntax highligthing", + description="Use colors for syntax highlighting", field_name="enable_syntax_highlighting", ), simple_option( diff --git a/ptpython/utils.py b/ptpython/utils.py index 53488997..d973d726 100644 --- a/ptpython/utils.py +++ b/ptpython/utils.py @@ -91,7 +91,7 @@ def get_jedi_script_from_document( # Workaround Jedi issue #514: for https://github.com/davidhalter/jedi/issues/514 return None except KeyError: - # Workaroud for a crash when the input is "u'", the start of a unicode string. + # Workaround for a crash when the input is "u'", the start of a unicode string. return None except Exception: # Workaround for: https://github.com/jonathanslenders/ptpython/issues/91 From 6c2d650649e5003d9ee01c01df508c11c6b28e9b Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Fri, 3 Nov 2023 13:07:14 +0000 Subject: [PATCH 409/470] Use ruff for linting and formatting. - Removed unused typing imports. - Renamed ambiguous variable. - Fix dict literal usage. - Ruff formatting. - Removed unnecessary trailing commas. --- .github/workflows/test.yaml | 6 ++-- ptpython/completer.py | 2 +- ptpython/contrib/asyncssh_repl.py | 2 +- ptpython/entry_points/run_ptpython.py | 6 ++-- ptpython/history_browser.py | 3 +- ptpython/layout.py | 22 ++++++------- ptpython/lexer.py | 2 +- ptpython/python_input.py | 21 +++--------- ptpython/repl.py | 12 +++---- ptpython/signatures.py | 3 +- ptpython/style.py | 2 -- ptpython/utils.py | 12 +------ ptpython/validator.py | 2 +- pyproject.toml | 47 +++++++++++++++++++-------- setup.py | 10 +++--- 15 files changed, 75 insertions(+), 77 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 31837db3..9a50f3bc 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -22,13 +22,13 @@ jobs: run: | sudo apt remove python3-pip python -m pip install --upgrade pip - python -m pip install . black isort mypy pytest readme_renderer + python -m pip install . ruff mypy pytest readme_renderer pip list - name: Type Checker run: | mypy ptpython - isort -c --profile black ptpython examples setup.py - black --check ptpython examples setup.py + ruff . + ruff format --check . - name: Run Tests run: | ./tests/run_tests.py diff --git a/ptpython/completer.py b/ptpython/completer.py index 85a96d7c..91d66474 100644 --- a/ptpython/completer.py +++ b/ptpython/completer.py @@ -6,7 +6,7 @@ import keyword import re from enum import Enum -from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, List, Optional, Tuple +from typing import TYPE_CHECKING, Any, Callable, Iterable from prompt_toolkit.completion import ( CompleteEvent, diff --git a/ptpython/contrib/asyncssh_repl.py b/ptpython/contrib/asyncssh_repl.py index 0347aded..051519de 100644 --- a/ptpython/contrib/asyncssh_repl.py +++ b/ptpython/contrib/asyncssh_repl.py @@ -9,7 +9,7 @@ from __future__ import annotations import asyncio -from typing import Any, Optional, TextIO, cast +from typing import Any, TextIO, cast import asyncssh from prompt_toolkit.data_structures import Size diff --git a/ptpython/entry_points/run_ptpython.py b/ptpython/entry_points/run_ptpython.py index 1b4074d4..c0b4078b 100644 --- a/ptpython/entry_points/run_ptpython.py +++ b/ptpython/entry_points/run_ptpython.py @@ -28,7 +28,7 @@ import pathlib import sys from textwrap import dedent -from typing import IO, Optional, Tuple +from typing import IO import appdirs from prompt_toolkit.formatted_text import HTML @@ -72,12 +72,12 @@ def create_parser() -> _Parser: "--light-bg", action="store_true", help="Run on a light background (use dark colors for text).", - ), + ) parser.add_argument( "--dark-bg", action="store_true", help="Run on a dark background (use light colors for text).", - ), + ) parser.add_argument( "--config-file", type=str, help="Location of configuration file." ) diff --git a/ptpython/history_browser.py b/ptpython/history_browser.py index eea81c2e..b667be12 100644 --- a/ptpython/history_browser.py +++ b/ptpython/history_browser.py @@ -7,7 +7,7 @@ from __future__ import annotations from functools import partial -from typing import TYPE_CHECKING, Callable, List, Optional, Set +from typing import TYPE_CHECKING, Callable from prompt_toolkit.application import Application from prompt_toolkit.application.current import get_app @@ -107,6 +107,7 @@ class BORDER: "Box drawing characters." + HORIZONTAL = "\u2501" VERTICAL = "\u2503" TOP_LEFT = "\u250f" diff --git a/ptpython/layout.py b/ptpython/layout.py index d15e52e2..2c1ec15f 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -7,7 +7,7 @@ import sys from enum import Enum from inspect import _ParameterKind as ParameterKind -from typing import TYPE_CHECKING, Any, List, Optional, Type +from typing import TYPE_CHECKING, Any from prompt_toolkit.application import get_app from prompt_toolkit.enums import DEFAULT_BUFFER, SEARCH_BUFFER @@ -17,11 +17,7 @@ is_done, renderer_height_is_known, ) -from prompt_toolkit.formatted_text import ( - AnyFormattedText, - fragment_list_width, - to_formatted_text, -) +from prompt_toolkit.formatted_text import fragment_list_width, to_formatted_text from prompt_toolkit.formatted_text.base import StyleAndTextTuples from prompt_toolkit.key_binding.vi_state import InputMode from prompt_toolkit.layout.containers import ( @@ -60,7 +56,6 @@ SystemToolbar, ValidationToolbar, ) -from pygments.lexers import PythonLexer from .filters import HasSignature, ShowDocstring, ShowSidebar, ShowSignature from .prompt_style import PromptStyle @@ -74,6 +69,7 @@ class CompletionVisualisation(Enum): "Visualisation method for the completions." + NONE = "none" POP_UP = "pop-up" MULTI_COLUMN = "multi-column" @@ -151,7 +147,7 @@ def goto_next(mouse_event: MouseEvent) -> None: append_category(category) for option in category.options: - append(i, option.title, "%s" % (option.get_current_value(),)) + append(i, option.title, str(option.get_current_value())) i += 1 tokens.pop() # Remove last newline. @@ -302,13 +298,15 @@ def get_text_fragments() -> StyleAndTextTuples: content=Window( FormattedTextControl(get_text_fragments), height=Dimension.exact(1) ), - filter= # Show only when there is a signature - HasSignature(python_input) & + filter=HasSignature(python_input) + & # Signature needs to be shown. - ShowSignature(python_input) & + ShowSignature(python_input) + & # And no sidebar is visible. - ~ShowSidebar(python_input) & + ~ShowSidebar(python_input) + & # Not done yet. ~is_done, ) diff --git a/ptpython/lexer.py b/ptpython/lexer.py index 81924c9d..d925e95c 100644 --- a/ptpython/lexer.py +++ b/ptpython/lexer.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Callable, Optional +from typing import Callable from prompt_toolkit.document import Document from prompt_toolkit.formatted_text import StyleAndTextTuples diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 0c7fef6f..211d36c9 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -6,18 +6,7 @@ from asyncio import get_event_loop from functools import partial -from typing import ( - TYPE_CHECKING, - Any, - Callable, - Dict, - Generic, - List, - Mapping, - Optional, - Tuple, - TypeVar, -) +from typing import TYPE_CHECKING, Any, Callable, Dict, Generic, Mapping, TypeVar from prompt_toolkit.application import Application, get_app from prompt_toolkit.auto_suggest import ( @@ -333,7 +322,7 @@ def __init__( # Cursor shapes. self.cursor_shape_config = "Block" - self.all_cursor_shape_configs: Dict[str, AnyCursorShapeConfig] = { + self.all_cursor_shape_configs: dict[str, AnyCursorShapeConfig] = { "Block": CursorShape.BLOCK, "Underline": CursorShape.UNDERLINE, "Beam": CursorShape.BEAM, @@ -607,10 +596,10 @@ def get_values() -> dict[str, Callable[[], bool]]: description="Change the cursor style, possibly according " "to the Vi input mode.", get_current_value=lambda: self.cursor_shape_config, - get_values=lambda: dict( - (s, partial(enable, "cursor_shape_config", s)) + get_values=lambda: { + s: partial(enable, "cursor_shape_config", s) for s in self.all_cursor_shape_configs - ), + }, ), simple_option( title="Paste mode", diff --git a/ptpython/repl.py b/ptpython/repl.py index 02a5075d..3a74c3c3 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -18,7 +18,7 @@ import warnings from dis import COMPILER_FLAG_NAMES from enum import Enum -from typing import Any, Callable, ContextManager, Dict, Optional +from typing import Any, Callable, ContextManager from prompt_toolkit.formatted_text import ( HTML, @@ -547,12 +547,12 @@ def _format_exception_output(self, e: BaseException) -> PygmentsTokens: tblist = tblist[line_nr:] break - l = traceback.format_list(tblist) - if l: - l.insert(0, "Traceback (most recent call last):\n") - l.extend(traceback.format_exception_only(t, v)) + tb_list = traceback.format_list(tblist) + if tb_list: + tb_list.insert(0, "Traceback (most recent call last):\n") + tb_list.extend(traceback.format_exception_only(t, v)) - tb_str = "".join(l) + tb_str = "".join(tb_list) # Format exception and write to output. # (We use the default style. Most other styles result diff --git a/ptpython/signatures.py b/ptpython/signatures.py index 5a6f286a..d4cb98c2 100644 --- a/ptpython/signatures.py +++ b/ptpython/signatures.py @@ -10,7 +10,7 @@ import inspect from inspect import Signature as InspectSignature from inspect import _ParameterKind as ParameterKind -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence, Tuple +from typing import TYPE_CHECKING, Any, Sequence from prompt_toolkit.document import Document @@ -203,7 +203,6 @@ def get_signatures_using_eval( running `eval()` over the detected function name. """ # Look for open parenthesis, before cursor position. - text = document.text_before_cursor pos = document.cursor_position - 1 paren_mapping = {")": "(", "}": "{", "]": "["} diff --git a/ptpython/style.py b/ptpython/style.py index 199d5abf..c5a04e58 100644 --- a/ptpython/style.py +++ b/ptpython/style.py @@ -1,7 +1,5 @@ from __future__ import annotations -from typing import Dict - from prompt_toolkit.styles import BaseStyle, Style, merge_styles from prompt_toolkit.styles.pygments import style_from_pygments_cls from prompt_toolkit.utils import is_conemu_ansi, is_windows, is_windows_vt100_supported diff --git a/ptpython/utils.py b/ptpython/utils.py index d973d726..28887d20 100644 --- a/ptpython/utils.py +++ b/ptpython/utils.py @@ -4,17 +4,7 @@ from __future__ import annotations import re -from typing import ( - TYPE_CHECKING, - Any, - Callable, - Dict, - Iterable, - Optional, - Type, - TypeVar, - cast, -) +from typing import TYPE_CHECKING, Any, Callable, Iterable, TypeVar, cast from prompt_toolkit.document import Document from prompt_toolkit.formatted_text import to_formatted_text diff --git a/ptpython/validator.py b/ptpython/validator.py index 3b36d273..91b9c284 100644 --- a/ptpython/validator.py +++ b/ptpython/validator.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Callable, Optional +from typing import Callable from prompt_toolkit.document import Document from prompt_toolkit.validation import ValidationError, Validator diff --git a/pyproject.toml b/pyproject.toml index b356239f..d9d839ed 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,13 +1,34 @@ -[tool.black] -target-version = ['py36'] - - -[tool.isort] -# isort configuration that is compatible with Black. -multi_line_output = 3 -include_trailing_comma = true -known_first_party = "ptpython" -known_third_party = "prompt_toolkit,pygments,asyncssh" -force_grid_wrap = 0 -use_parentheses = true -line_length = 88 +[tool.ruff] +target-version = "py37" +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "C", # flake8-comprehensions + "T", # Print. + "I", # isort + # "B", # flake8-bugbear + "UP", # pyupgrade + "RUF100", # unused-noqa + "Q", # quotes +] +ignore = [ + "E501", # Line too long, handled by black + "C901", # Too complex + "E722", # bare except. +] + + +[tool.ruff.per-file-ignores] +"examples/*" = ["T201"] # Print allowed in examples. +"examples/ptpython_config/config.py" = ["F401"] # Unused imports in config. +"ptpython/entry_points/run_ptipython.py" = ["T201", "F401"] # Print, import usage. +"ptpython/entry_points/run_ptpython.py" = ["T201"] # Print usage. +"ptpython/ipython.py" = ["T100"] # Import usage. +"ptpython/repl.py" = ["T201"] # Print usage. +"tests/run_tests.py" = ["F401"] # Unused imports. + + +[tool.ruff.isort] +known-first-party = ["ptpython"] +known-third-party = ["prompt_toolkit", "pygments", "asyncssh"] diff --git a/setup.py b/setup.py index 18d2911a..ae9838ea 100644 --- a/setup.py +++ b/setup.py @@ -39,12 +39,14 @@ "ptpython = ptpython.entry_points.run_ptpython:run", "ptipython = ptpython.entry_points.run_ptipython:run", "ptpython%s = ptpython.entry_points.run_ptpython:run" % sys.version_info[0], - "ptpython%s.%s = ptpython.entry_points.run_ptpython:run" - % sys.version_info[:2], + "ptpython{}.{} = ptpython.entry_points.run_ptpython:run".format( + *sys.version_info[:2] + ), "ptipython%s = ptpython.entry_points.run_ptipython:run" % sys.version_info[0], - "ptipython%s.%s = ptpython.entry_points.run_ptipython:run" - % sys.version_info[:2], + "ptipython{}.{} = ptpython.entry_points.run_ptipython:run".format( + *sys.version_info[:2] + ), ] }, extras_require={ From 945426bfaa61ecc8c602983c2e2f023a9d17df22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tero=20Ykspet=C3=A4j=C3=A4?= Date: Tue, 12 Sep 2023 09:42:54 +0300 Subject: [PATCH 410/470] Add cursor_shape_config example Describe setting the cursor shape to modal for Vi mode. List other possible options for the setting. --- examples/ptpython_config/config.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/examples/ptpython_config/config.py b/examples/ptpython_config/config.py index 2f3f49dd..9e13879a 100644 --- a/examples/ptpython_config/config.py +++ b/examples/ptpython_config/config.py @@ -70,6 +70,9 @@ def configure(repl): # Vi mode. repl.vi_mode = False + # Enable the modal cursor (when using Vi mode). Other options are 'Block', 'Underline', 'Beam', 'Blink under', 'Blink block', and 'Blink beam' + repl.cursor_shape_config = 'Modal (vi)' + # Paste mode. (When True, don't insert whitespace after new line.) repl.paste_mode = False From 9ea323855977119fb5e49f428eadcf552b2260f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E7=A9=86=E7=A9=86?= Date: Wed, 6 Sep 2023 13:36:04 +0800 Subject: [PATCH 411/470] fix top-level await in ipython --- ptpython/ipython.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ptpython/ipython.py b/ptpython/ipython.py index fb4b5ed9..ae85a265 100644 --- a/ptpython/ipython.py +++ b/ptpython/ipython.py @@ -38,6 +38,7 @@ from .completer import PythonCompleter from .python_input import PythonInput +from .repl import PyCF_ALLOW_TOP_LEVEL_AWAIT from .style import default_ui_style from .validator import PythonValidator @@ -211,6 +212,12 @@ def __init__(self, ipython_shell, *a, **kw): self.ui_styles = {"default": Style.from_dict(style_dict)} self.use_ui_colorscheme("default") + def get_compiler_flags(self): + flags = super().get_compiler_flags() + if self.ipython_shell.autoawait: + flags |= PyCF_ALLOW_TOP_LEVEL_AWAIT + return flags + class InteractiveShellEmbed(_InteractiveShellEmbed): """ From 48c7b3885c786fcb3f5f75404cd4cc2caa20bfe9 Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Sat, 22 Jul 2023 15:12:19 +0200 Subject: [PATCH 412/470] Fix IPython DeprecationWarning ptipython raises the following error since IPython 7: >>> IPython.core.inputsplitter is deprecated since IPython 7 in favor of `IPython.core.inputtransformer2` --- ptpython/ipython.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ptpython/ipython.py b/ptpython/ipython.py index ae85a265..ad0516a3 100644 --- a/ptpython/ipython.py +++ b/ptpython/ipython.py @@ -14,7 +14,7 @@ from warnings import warn from IPython import utils as ipy_utils -from IPython.core.inputsplitter import IPythonInputSplitter +from IPython.core.inputtransformer2 import TransformerManager from IPython.terminal.embed import InteractiveShellEmbed as _InteractiveShellEmbed from IPython.terminal.ipapp import load_default_config from prompt_toolkit.completion import ( @@ -66,7 +66,7 @@ def out_prompt(self) -> AnyFormattedText: class IPythonValidator(PythonValidator): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.isp = IPythonInputSplitter() + self.isp = TransformerManager() def validate(self, document: Document) -> None: document = Document(text=self.isp.transform_cell(document.text)) From d25e67874ffc745a3a65a6776c1b9aa401dca076 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Mon, 4 Sep 2023 08:01:04 -0500 Subject: [PATCH 413/470] Only interrupt run_config() for explicitly passed config_file --- ptpython/repl.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/ptpython/repl.py b/ptpython/repl.py index 3a74c3c3..ce92c660 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -630,23 +630,28 @@ def enable_deprecation_warnings() -> None: warnings.filterwarnings("default", category=DeprecationWarning, module="__main__") -def run_config( - repl: PythonInput, config_file: str = "~/.config/ptpython/config.py" -) -> None: +DEFAULT_CONFIG_FILE = "~/.config/ptpython/config.py" + + +def run_config(repl: PythonInput, config_file: str | None = None) -> None: """ Execute REPL config file. :param repl: `PythonInput` instance. :param config_file: Path of the configuration file. """ + explicit_config_file = config_file is not None + # Expand tildes. - config_file = os.path.expanduser(config_file) + config_file = os.path.expanduser( + config_file if config_file is not None else DEFAULT_CONFIG_FILE + ) def enter_to_continue() -> None: input("\nPress ENTER to continue...") # Check whether this file exists. - if not os.path.exists(config_file): + if not os.path.exists(config_file) and explicit_config_file: print("Impossible to read %r" % config_file) enter_to_continue() return From dc2163383e3dcc54eb19795fe87c0162a578bbfb Mon Sep 17 00:00:00 2001 From: Ryan Delaney Date: Fri, 10 Mar 2023 17:01:23 -0500 Subject: [PATCH 414/470] Add BSD License classifier --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index ae9838ea..ad26545a 100644 --- a/setup.py +++ b/setup.py @@ -27,6 +27,7 @@ ], python_requires=">=3.7", classifiers=[ + "License :: OSI Approved :: BSD License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.7", From 03b279ecd6a0ec670cba144f5f680a7a78fc2fc7 Mon Sep 17 00:00:00 2001 From: Ryan Delaney Date: Fri, 10 Mar 2023 17:01:38 -0500 Subject: [PATCH 415/470] Update copyright dates --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 910b80a7..89a51144 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2015, Jonathan Slenders +Copyright (c) 2015-2023, Jonathan Slenders All rights reserved. Redistribution and use in source and binary forms, with or without modification, From 46b1076cea63f7d0642b2e820d7fbcbff89336a1 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Fri, 3 Nov 2023 13:45:22 +0000 Subject: [PATCH 416/470] Fix code formatting. --- examples/ptpython_config/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/ptpython_config/config.py b/examples/ptpython_config/config.py index 9e13879a..b25850a2 100644 --- a/examples/ptpython_config/config.py +++ b/examples/ptpython_config/config.py @@ -71,7 +71,7 @@ def configure(repl): repl.vi_mode = False # Enable the modal cursor (when using Vi mode). Other options are 'Block', 'Underline', 'Beam', 'Blink under', 'Blink block', and 'Blink beam' - repl.cursor_shape_config = 'Modal (vi)' + repl.cursor_shape_config = "Modal (vi)" # Paste mode. (When True, don't insert whitespace after new line.) repl.paste_mode = False From 655b354a83aa56c423ba7ecebe4df88928c99526 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 12 Dec 2023 10:43:26 +0000 Subject: [PATCH 417/470] Refactor output printer so that it can render big outputs without memory issues. Previously, an expression like `b'\x90' * 40_000_000` would kill ptpython because it rendered the whole output at once. This implementation streams the rendering logic while it's paginating. --- ptpython/printer.py | 435 ++++++++++++++++++++++++++++++++++++++++++++ ptpython/repl.py | 400 +++++----------------------------------- pyproject.toml | 1 + 3 files changed, 478 insertions(+), 358 deletions(-) create mode 100644 ptpython/printer.py diff --git a/ptpython/printer.py b/ptpython/printer.py new file mode 100644 index 00000000..3618934e --- /dev/null +++ b/ptpython/printer.py @@ -0,0 +1,435 @@ +from __future__ import annotations + +import sys +import traceback +from dataclasses import dataclass +from enum import Enum +from typing import Generator, Iterable + +from prompt_toolkit.formatted_text import ( + HTML, + AnyFormattedText, + FormattedText, + OneStyleAndTextTuple, + StyleAndTextTuples, + fragment_list_width, + merge_formatted_text, + to_formatted_text, +) +from prompt_toolkit.formatted_text.utils import split_lines +from prompt_toolkit.input import Input +from prompt_toolkit.key_binding import KeyBindings, KeyPressEvent +from prompt_toolkit.output import Output +from prompt_toolkit.shortcuts import PromptSession, print_formatted_text +from prompt_toolkit.styles import BaseStyle, StyleTransformation +from prompt_toolkit.styles.pygments import pygments_token_to_classname +from prompt_toolkit.utils import get_cwidth +from pygments.lexers import PythonLexer, PythonTracebackLexer + +__all__ = ["OutputPrinter"] + +# Never reformat results larger than this: +MAX_REFORMAT_SIZE = 1_000_000 + + +@dataclass +class OutputPrinter: + """ + Result printer. + + Usage:: + + printer = OutputPrinter(...) + printer.display_result(...) + printer.display_exception(...) + """ + + output: Output + input: Input + style: BaseStyle + title: AnyFormattedText + style_transformation: StyleTransformation + + def display_result( + self, + result: object, + *, + out_prompt: AnyFormattedText, + reformat: bool, + highlight: bool, + paginate: bool, + ) -> None: + """ + Show __repr__ (or `__pt_repr__`) for an `eval` result and print to output. + + :param reformat: Reformat result using 'black' before printing if the + result is parsable as Python code. + :param highlight: Syntax highlight the result. + :param paginate: Show paginator when the result does not fit on the + screen. + """ + out_prompt = to_formatted_text(out_prompt) + out_prompt_width = fragment_list_width(out_prompt) + + result = self._insert_out_prompt_and_split_lines( + self._format_result_output( + result, + reformat=reformat, + highlight=highlight, + line_length=self.output.get_size().columns - out_prompt_width, + paginate=paginate, + ), + out_prompt=out_prompt, + ) + self._display_result(result, paginate=paginate) + + def display_exception( + self, e: BaseException, *, highlight: bool, paginate: bool + ) -> None: + """ + Render an exception. + """ + result = self._insert_out_prompt_and_split_lines( + self._format_exception_output(e, highlight=highlight), + out_prompt="", + ) + self._display_result(result, paginate=paginate) + + def display_style_and_text_tuples( + self, + result: Iterable[OneStyleAndTextTuple], + *, + paginate: bool, + ) -> None: + self._display_result( + self._insert_out_prompt_and_split_lines(result, out_prompt=""), + paginate=paginate, + ) + + def _display_result( + self, + lines: Iterable[StyleAndTextTuples], + *, + paginate: bool, + ) -> None: + if paginate: + self._print_paginated_formatted_text(lines) + else: + for line in lines: + self._print_formatted_text(line) + + self.output.flush() + + def _print_formatted_text(self, line: StyleAndTextTuples, end: str = "\n") -> None: + print_formatted_text( + FormattedText(line), + style=self.style, + style_transformation=self.style_transformation, + include_default_pygments_style=False, + output=self.output, + end=end, + ) + + def _format_result_output( + self, + result: object, + *, + reformat: bool, + highlight: bool, + line_length: int, + paginate: bool, + ) -> Generator[OneStyleAndTextTuple, None, None]: + """ + Format __repr__ for an `eval` result. + + Note: this can raise `KeyboardInterrupt` if either calling `__repr__`, + `__pt_repr__` or formatting the output with "Black" takes to long + and the user presses Control-C. + """ + # If __pt_repr__ is present, take this. This can return prompt_toolkit + # formatted text. + try: + if hasattr(result, "__pt_repr__"): + formatted_result_repr = to_formatted_text( + getattr(result, "__pt_repr__")() + ) + yield from formatted_result_repr + return + except KeyboardInterrupt: + raise # Don't catch here. + except: + # For bad code, `__getattr__` can raise something that's not an + # `AttributeError`. This happens already when calling `hasattr()`. + pass + + # Call `__repr__` of given object first, to turn it in a string. + try: + result_repr = repr(result) + except KeyboardInterrupt: + raise # Don't catch here. + except BaseException as e: + # Calling repr failed. + self.display_exception(e, highlight=highlight, paginate=paginate) + return + + # Determine whether it's valid Python code. If not, + # reformatting/highlighting won't be applied. + if len(result_repr) < MAX_REFORMAT_SIZE: + try: + compile(result_repr, "", "eval") + except SyntaxError: + valid_python = False + else: + valid_python = True + else: + valid_python = False + + if valid_python and reformat: + # Inline import. Slightly speed up start-up time if black is + # not used. + try: + import black + + if not hasattr(black, "Mode"): + raise ImportError + except ImportError: + pass # no Black package in your installation + else: + result_repr = black.format_str( + result_repr, + mode=black.Mode(line_length=line_length), + ) + + if valid_python and highlight: + yield from _lex_python_result(result_repr) + else: + yield ("", result_repr) + + def _insert_out_prompt_and_split_lines( + self, result: Iterable[OneStyleAndTextTuple], out_prompt: AnyFormattedText + ) -> Iterable[StyleAndTextTuples]: + r""" + Split styled result in lines (based on the \n characters in the result) + an insert output prompt on whitespace in front of each line. (This does + not yet do the soft wrapping.) + + Yield lines as a result. + """ + out_prompt = to_formatted_text(out_prompt) + out_prompt_width = fragment_list_width(out_prompt) + prefix = ("", " " * out_prompt_width) + + for i, line in enumerate(split_lines(result)): + if i == 0: + line = [*out_prompt, *line] + else: + line = [prefix, *line] + yield line + + def _apply_soft_wrapping( + self, lines: Iterable[StyleAndTextTuples] + ) -> Iterable[StyleAndTextTuples]: + """ + Apply soft wrapping to the given lines. Wrap according to the terminal + width. Insert whitespace in front of each wrapped line to align it with + the output prompt. + """ + line_length = self.output.get_size().columns + + # Iterate over hard wrapped lines. + for lineno, line in enumerate(lines): + columns_in_buffer = 0 + current_line: list[OneStyleAndTextTuple] = [] + + for style, text, *_ in line: + for c in text: + width = get_cwidth(c) + + # (Soft) wrap line if it doesn't fit. + if columns_in_buffer + width > line_length: + yield current_line + columns_in_buffer = 0 + current_line = [] + + columns_in_buffer += width + current_line.append((style, c)) + + if len(current_line) > 0: + yield current_line + + def _print_paginated_formatted_text( + self, lines: Iterable[StyleAndTextTuples] + ) -> None: + """ + Print formatted text, using --MORE-- style pagination. + (Avoid filling up the terminal's scrollback buffer.) + """ + lines = self._apply_soft_wrapping(lines) + pager_prompt = create_pager_prompt( + self.style, self.title, output=self.output, input=self.input + ) + + abort = False + print_all = False + + # Max number of lines allowed in the buffer before painting. + size = self.output.get_size() + max_rows = size.rows - 1 + + # Page buffer. + page: StyleAndTextTuples = [] + + def show_pager() -> None: + nonlocal abort, max_rows, print_all + + # Run pager prompt in another thread. + # Same as for the input. This prevents issues with nested event + # loops. + pager_result = pager_prompt.prompt(in_thread=True) + + if pager_result == PagerResult.ABORT: + print("...") + abort = True + + elif pager_result == PagerResult.NEXT_LINE: + max_rows = 1 + + elif pager_result == PagerResult.NEXT_PAGE: + max_rows = size.rows - 1 + + elif pager_result == PagerResult.PRINT_ALL: + print_all = True + + # Loop over lines. Show --MORE-- prompt when page is filled. + rows = 0 + + for lineno, line in enumerate(lines): + page.extend(line) + page.append(("", "\n")) + rows += 1 + + if rows >= max_rows: + self._print_formatted_text(page, end="") + page = [] + rows = 0 + + if not print_all: + show_pager() + if abort: + return + + self._print_formatted_text(page) + + def _format_exception_output( + self, e: BaseException, highlight: bool + ) -> Generator[OneStyleAndTextTuple, None, None]: + # Instead of just calling ``traceback.format_exc``, we take the + # traceback and skip the bottom calls of this framework. + t, v, tb = sys.exc_info() + + # Required for pdb.post_mortem() to work. + sys.last_type, sys.last_value, sys.last_traceback = t, v, tb + + tblist = list(traceback.extract_tb(tb)) + + for line_nr, tb_tuple in enumerate(tblist): + if tb_tuple[0] == "": + tblist = tblist[line_nr:] + break + + tb_list = traceback.format_list(tblist) + if tb_list: + tb_list.insert(0, "Traceback (most recent call last):\n") + tb_list.extend(traceback.format_exception_only(t, v)) + + tb_str = "".join(tb_list) + + # Format exception and write to output. + # (We use the default style. Most other styles result + # in unreadable colors for the traceback.) + if highlight: + for index, tokentype, text in PythonTracebackLexer().get_tokens_unprocessed( + tb_str + ): + yield ("class:" + pygments_token_to_classname(tokentype), text) + else: + yield ("", tb_str) + + +class PagerResult(Enum): + ABORT = "ABORT" + NEXT_LINE = "NEXT_LINE" + NEXT_PAGE = "NEXT_PAGE" + PRINT_ALL = "PRINT_ALL" + + +def create_pager_prompt( + style: BaseStyle, + title: AnyFormattedText = "", + input: Input | None = None, + output: Output | None = None, +) -> PromptSession[PagerResult]: + """ + Create a "--MORE--" prompt for paginated output. + """ + bindings = KeyBindings() + + @bindings.add("enter") + @bindings.add("down") + def next_line(event: KeyPressEvent) -> None: + event.app.exit(result=PagerResult.NEXT_LINE) + + @bindings.add("space") + def next_page(event: KeyPressEvent) -> None: + event.app.exit(result=PagerResult.NEXT_PAGE) + + @bindings.add("a") + def print_all(event: KeyPressEvent) -> None: + event.app.exit(result=PagerResult.PRINT_ALL) + + @bindings.add("q") + @bindings.add("c-c") + @bindings.add("c-d") + @bindings.add("escape", eager=True) + def no(event: KeyPressEvent) -> None: + event.app.exit(result=PagerResult.ABORT) + + @bindings.add("") + def _(event: KeyPressEvent) -> None: + "Disallow inserting other text." + pass + + session: PromptSession[PagerResult] = PromptSession( + merge_formatted_text( + [ + title, + HTML( + "" + " -- MORE -- " + "[Enter] Scroll " + "[Space] Next page " + "[a] Print all " + "[q] Quit " + ": " + ), + ] + ), + key_bindings=bindings, + erase_when_done=True, + style=style, + input=input, + output=output, + ) + return session + + +def _lex_python_result(result: str) -> Generator[tuple[str, str], None, None]: + "Return token list for Python string." + lexer = PythonLexer() + # Use `get_tokens_unprocessed`, so that we get exactly the same string, + # without line endings appended. `print_formatted_text` already appends a + # line ending, and otherwise we'll have two line endings. + tokens = lexer.get_tokens_unprocessed(result) + + for index, tokentype, text in tokens: + yield ("class:" + pygments_token_to_classname(tokentype), text) diff --git a/ptpython/repl.py b/ptpython/repl.py index ce92c660..98b01afa 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -17,33 +17,18 @@ import types import warnings from dis import COMPILER_FLAG_NAMES -from enum import Enum -from typing import Any, Callable, ContextManager - -from prompt_toolkit.formatted_text import ( - HTML, - AnyFormattedText, - FormattedText, - PygmentsTokens, - StyleAndTextTuples, - fragment_list_width, - merge_formatted_text, - to_formatted_text, -) -from prompt_toolkit.formatted_text.utils import fragment_list_to_text, split_lines -from prompt_toolkit.key_binding import KeyBindings, KeyPressEvent +from typing import Any, Callable, ContextManager, Iterable + +from prompt_toolkit.formatted_text import OneStyleAndTextTuple from prompt_toolkit.patch_stdout import patch_stdout as patch_stdout_context from prompt_toolkit.shortcuts import ( - PromptSession, clear_title, - print_formatted_text, set_title, ) -from prompt_toolkit.styles import BaseStyle -from prompt_toolkit.utils import DummyContext, get_cwidth -from pygments.lexers import PythonLexer, PythonTracebackLexer -from pygments.token import Token +from prompt_toolkit.utils import DummyContext +from pygments.lexers import PythonTracebackLexer # noqa: F401 +from .printer import OutputPrinter from .python_input import PythonInput PyCF_ALLOW_TOP_LEVEL_AWAIT: int @@ -108,7 +93,9 @@ def run_and_show_expression(self, expression: str) -> None: else: # Print. if result is not None: - self.show_result(result) + self._show_result(result) + if self.insert_blank_line_after_output: + self.app.output.write("\n") # Loop. self.current_statement_index += 1 @@ -123,6 +110,24 @@ def run_and_show_expression(self, expression: str) -> None: # any case.) self._handle_keyboard_interrupt(e) + def _get_output_printer(self) -> OutputPrinter: + return OutputPrinter( + output=self.app.output, + input=self.app.input, + style=self._current_style, + style_transformation=self.style_transformation, + title=self.title, + ) + + def _show_result(self, result: object) -> None: + self._get_output_printer().display_result( + result=result, + out_prompt=self.get_output_prompt(), + reformat=self.enable_output_formatting, + highlight=self.enable_syntax_highlighting, + paginate=self.enable_pager, + ) + def run(self) -> None: """ Run the REPL loop. @@ -167,7 +172,7 @@ async def run_and_show_expression_async(self, text: str): else: # Print. if result is not None: - await loop.run_in_executor(None, lambda: self.show_result(result)) + await loop.run_in_executor(None, lambda: self._show_result(result)) # Loop. self.current_statement_index += 1 @@ -318,264 +323,12 @@ def _compile_with_flags(self, code: str, mode: str): dont_inherit=True, ) - def _format_result_output(self, result: object) -> StyleAndTextTuples: - """ - Format __repr__ for an `eval` result. - - Note: this can raise `KeyboardInterrupt` if either calling `__repr__`, - `__pt_repr__` or formatting the output with "Black" takes to long - and the user presses Control-C. - """ - out_prompt = to_formatted_text(self.get_output_prompt()) - - # If the repr is valid Python code, use the Pygments lexer. - try: - result_repr = repr(result) - except KeyboardInterrupt: - raise # Don't catch here. - except BaseException as e: - # Calling repr failed. - self._handle_exception(e) - return [] - - try: - compile(result_repr, "", "eval") - except SyntaxError: - formatted_result_repr = to_formatted_text(result_repr) - else: - # Syntactically correct. Format with black and syntax highlight. - if self.enable_output_formatting: - # Inline import. Slightly speed up start-up time if black is - # not used. - try: - import black - - if not hasattr(black, "Mode"): - raise ImportError - except ImportError: - pass # no Black package in your installation - else: - result_repr = black.format_str( - result_repr, - mode=black.Mode(line_length=self.app.output.get_size().columns), - ) - - formatted_result_repr = to_formatted_text( - PygmentsTokens(list(_lex_python_result(result_repr))) - ) - - # If __pt_repr__ is present, take this. This can return prompt_toolkit - # formatted text. - try: - if hasattr(result, "__pt_repr__"): - formatted_result_repr = to_formatted_text( - getattr(result, "__pt_repr__")() - ) - if isinstance(formatted_result_repr, list): - formatted_result_repr = FormattedText(formatted_result_repr) - except KeyboardInterrupt: - raise # Don't catch here. - except: - # For bad code, `__getattr__` can raise something that's not an - # `AttributeError`. This happens already when calling `hasattr()`. - pass - - # Align every line to the prompt. - line_sep = "\n" + " " * fragment_list_width(out_prompt) - indented_repr: StyleAndTextTuples = [] - - lines = list(split_lines(formatted_result_repr)) - - for i, fragment in enumerate(lines): - indented_repr.extend(fragment) - - # Add indentation separator between lines, not after the last line. - if i != len(lines) - 1: - indented_repr.append(("", line_sep)) - - # Write output tokens. - if self.enable_syntax_highlighting: - formatted_output = merge_formatted_text([out_prompt, indented_repr]) - else: - formatted_output = FormattedText( - out_prompt + [("", fragment_list_to_text(formatted_result_repr))] - ) - - return to_formatted_text(formatted_output) - - def show_result(self, result: object) -> None: - """ - Show __repr__ for an `eval` result and print to output. - """ - formatted_text_output = self._format_result_output(result) - - if self.enable_pager: - self.print_paginated_formatted_text(formatted_text_output) - else: - self.print_formatted_text(formatted_text_output) - - self.app.output.flush() - - if self.insert_blank_line_after_output: - self.app.output.write("\n") - - def print_formatted_text( - self, formatted_text: StyleAndTextTuples, end: str = "\n" - ) -> None: - print_formatted_text( - FormattedText(formatted_text), - style=self._current_style, - style_transformation=self.style_transformation, - include_default_pygments_style=False, - output=self.app.output, - end=end, - ) - - def print_paginated_formatted_text( - self, - formatted_text: StyleAndTextTuples, - end: str = "\n", - ) -> None: - """ - Print formatted text, using --MORE-- style pagination. - (Avoid filling up the terminal's scrollback buffer.) - """ - pager_prompt = self.create_pager_prompt() - size = self.app.output.get_size() - - abort = False - print_all = False - - # Max number of lines allowed in the buffer before painting. - max_rows = size.rows - 1 - - # Page buffer. - rows_in_buffer = 0 - columns_in_buffer = 0 - page: StyleAndTextTuples = [] - - def flush_page() -> None: - nonlocal page, columns_in_buffer, rows_in_buffer - self.print_formatted_text(page, end="") - page = [] - columns_in_buffer = 0 - rows_in_buffer = 0 - - def show_pager() -> None: - nonlocal abort, max_rows, print_all - - # Run pager prompt in another thread. - # Same as for the input. This prevents issues with nested event - # loops. - pager_result = pager_prompt.prompt(in_thread=True) - - if pager_result == PagerResult.ABORT: - print("...") - abort = True - - elif pager_result == PagerResult.NEXT_LINE: - max_rows = 1 - - elif pager_result == PagerResult.NEXT_PAGE: - max_rows = size.rows - 1 - - elif pager_result == PagerResult.PRINT_ALL: - print_all = True - - # Loop over lines. Show --MORE-- prompt when page is filled. - - formatted_text = formatted_text + [("", end)] - lines = list(split_lines(formatted_text)) - - for lineno, line in enumerate(lines): - for style, text, *_ in line: - for c in text: - width = get_cwidth(c) - - # (Soft) wrap line if it doesn't fit. - if columns_in_buffer + width > size.columns: - # Show pager first if we get too many lines after - # wrapping. - if rows_in_buffer + 1 >= max_rows and not print_all: - page.append(("", "\n")) - flush_page() - show_pager() - if abort: - return - - rows_in_buffer += 1 - columns_in_buffer = 0 - - columns_in_buffer += width - page.append((style, c)) - - if rows_in_buffer + 1 >= max_rows and not print_all: - page.append(("", "\n")) - flush_page() - show_pager() - if abort: - return - else: - # Add line ending between lines (if `end="\n"` was given, one - # more empty line is added in `split_lines` automatically to - # take care of the final line ending). - if lineno != len(lines) - 1: - page.append(("", "\n")) - rows_in_buffer += 1 - columns_in_buffer = 0 - - flush_page() - - def create_pager_prompt(self) -> PromptSession[PagerResult]: - """ - Create pager --MORE-- prompt. - """ - return create_pager_prompt(self._current_style, self.title) - - def _format_exception_output(self, e: BaseException) -> PygmentsTokens: - # Instead of just calling ``traceback.format_exc``, we take the - # traceback and skip the bottom calls of this framework. - t, v, tb = sys.exc_info() - - # Required for pdb.post_mortem() to work. - sys.last_type, sys.last_value, sys.last_traceback = t, v, tb - - tblist = list(traceback.extract_tb(tb)) - - for line_nr, tb_tuple in enumerate(tblist): - if tb_tuple[0] == "": - tblist = tblist[line_nr:] - break - - tb_list = traceback.format_list(tblist) - if tb_list: - tb_list.insert(0, "Traceback (most recent call last):\n") - tb_list.extend(traceback.format_exception_only(t, v)) - - tb_str = "".join(tb_list) - - # Format exception and write to output. - # (We use the default style. Most other styles result - # in unreadable colors for the traceback.) - if self.enable_syntax_highlighting: - tokens = list(_lex_python_traceback(tb_str)) - else: - tokens = [(Token, tb_str)] - return PygmentsTokens(tokens) - def _handle_exception(self, e: BaseException) -> None: - output = self.app.output - - tokens = self._format_exception_output(e) - - print_formatted_text( - tokens, - style=self._current_style, - style_transformation=self.style_transformation, - include_default_pygments_style=False, - output=output, + self._get_output_printer().display_exception( + e, + highlight=self.enable_syntax_highlighting, + paginate=self.enable_pager, ) - output.flush() def _handle_keyboard_interrupt(self, e: KeyboardInterrupt) -> None: output = self.app.output @@ -602,21 +355,16 @@ def _remove_from_namespace(self) -> None: globals = self.get_globals() del globals["get_ptpython"] - -def _lex_python_traceback(tb): - "Return token list for traceback string." - lexer = PythonTracebackLexer() - return lexer.get_tokens(tb) - - -def _lex_python_result(tb): - "Return token list for Python string." - lexer = PythonLexer() - # Use `get_tokens_unprocessed`, so that we get exactly the same string, - # without line endings appended. `print_formatted_text` already appends a - # line ending, and otherwise we'll have two line endings. - tokens = lexer.get_tokens_unprocessed(tb) - return [(tokentype, value) for index, tokentype, value in tokens] + def print_paginated_formatted_text( + self, + formatted_text: Iterable[OneStyleAndTextTuple], + end: str = "\n", + ) -> None: + # Warning: This is mainly here backwards-compatibility. Some projects + # call `print_paginated_formatted_text` on the Repl object. + self._get_output_printer().display_style_and_text_tuples( + formatted_text, paginate=True + ) def enable_deprecation_warnings() -> None: @@ -746,67 +494,3 @@ async def coroutine() -> None: else: with patch_context: repl.run() - - -class PagerResult(Enum): - ABORT = "ABORT" - NEXT_LINE = "NEXT_LINE" - NEXT_PAGE = "NEXT_PAGE" - PRINT_ALL = "PRINT_ALL" - - -def create_pager_prompt( - style: BaseStyle, title: AnyFormattedText = "" -) -> PromptSession[PagerResult]: - """ - Create a "continue" prompt for paginated output. - """ - bindings = KeyBindings() - - @bindings.add("enter") - @bindings.add("down") - def next_line(event: KeyPressEvent) -> None: - event.app.exit(result=PagerResult.NEXT_LINE) - - @bindings.add("space") - def next_page(event: KeyPressEvent) -> None: - event.app.exit(result=PagerResult.NEXT_PAGE) - - @bindings.add("a") - def print_all(event: KeyPressEvent) -> None: - event.app.exit(result=PagerResult.PRINT_ALL) - - @bindings.add("q") - @bindings.add("c-c") - @bindings.add("c-d") - @bindings.add("escape", eager=True) - def no(event: KeyPressEvent) -> None: - event.app.exit(result=PagerResult.ABORT) - - @bindings.add("") - def _(event: KeyPressEvent) -> None: - "Disallow inserting other text." - pass - - style - - session: PromptSession[PagerResult] = PromptSession( - merge_formatted_text( - [ - title, - HTML( - "" - " -- MORE -- " - "[Enter] Scroll " - "[Space] Next page " - "[a] Print all " - "[q] Quit " - ": " - ), - ] - ), - key_bindings=bindings, - erase_when_done=True, - style=style, - ) - return session diff --git a/pyproject.toml b/pyproject.toml index d9d839ed..5421c454 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,7 @@ ignore = [ "ptpython/entry_points/run_ptpython.py" = ["T201"] # Print usage. "ptpython/ipython.py" = ["T100"] # Import usage. "ptpython/repl.py" = ["T201"] # Print usage. +"ptpython/printer.py" = ["T201"] # Print usage. "tests/run_tests.py" = ["F401"] # Unused imports. From 6801f94006951e5c06f232862e40fa19cd58aa82 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 12 Dec 2023 20:27:49 +0000 Subject: [PATCH 418/470] Fix type annotations in various places. --- examples/asyncio-python-embed.py | 15 +++--- examples/asyncio-ssh-python-embed.py | 18 +++---- examples/python-embed-with-custom-prompt.py | 12 ++--- examples/python-embed.py | 2 +- examples/ssh-and-telnet-embed.py | 11 ++-- ptpython/contrib/asyncssh_repl.py | 26 +++++---- ptpython/python_input.py | 58 ++++++++++++++------- ptpython/repl.py | 2 +- 8 files changed, 80 insertions(+), 64 deletions(-) diff --git a/examples/asyncio-python-embed.py b/examples/asyncio-python-embed.py index 05f52f1d..a8fbba5a 100755 --- a/examples/asyncio-python-embed.py +++ b/examples/asyncio-python-embed.py @@ -19,7 +19,7 @@ counter = [0] -async def print_counter(): +async def print_counter() -> None: """ Coroutine that prints counters and saves it in a global variable. """ @@ -29,7 +29,7 @@ async def print_counter(): await asyncio.sleep(3) -async def interactive_shell(): +async def interactive_shell() -> None: """ Coroutine that starts a Python REPL from which we can access the global counter variable. @@ -44,13 +44,10 @@ async def interactive_shell(): loop.stop() -def main(): - asyncio.ensure_future(print_counter()) - asyncio.ensure_future(interactive_shell()) - - loop.run_forever() - loop.close() +async def main() -> None: + asyncio.create_task(print_counter()) + await interactive_shell() if __name__ == "__main__": - main() + asyncio.run(main()) diff --git a/examples/asyncio-ssh-python-embed.py b/examples/asyncio-ssh-python-embed.py index 86b56073..be0689e7 100755 --- a/examples/asyncio-ssh-python-embed.py +++ b/examples/asyncio-ssh-python-embed.py @@ -32,31 +32,25 @@ def session_requested(self): return ReplSSHServerSession(self.get_namespace) -def main(port=8222): +async def main(port: int = 8222) -> None: """ Example that starts the REPL through an SSH server. """ - loop = asyncio.get_event_loop() - # Namespace exposed in the REPL. environ = {"hello": "world"} # Start SSH server. - def create_server(): + def create_server() -> MySSHServer: return MySSHServer(lambda: environ) print("Listening on :%i" % port) print('To connect, do "ssh localhost -p %i"' % port) - loop.run_until_complete( - asyncssh.create_server( - create_server, "", port, server_host_keys=["/etc/ssh/ssh_host_dsa_key"] - ) + await asyncssh.create_server( + create_server, "", port, server_host_keys=["/etc/ssh/ssh_host_dsa_key"] ) - - # Run eventloop. - loop.run_forever() + await asyncio.Future() # Wait forever. if __name__ == "__main__": - main() + asyncio.run(main()) diff --git a/examples/python-embed-with-custom-prompt.py b/examples/python-embed-with-custom-prompt.py index 968aedc5..d54da1da 100755 --- a/examples/python-embed-with-custom-prompt.py +++ b/examples/python-embed-with-custom-prompt.py @@ -2,26 +2,26 @@ """ Example of embedding a Python REPL, and setting a custom prompt. """ -from prompt_toolkit.formatted_text import HTML +from prompt_toolkit.formatted_text import HTML, AnyFormattedText from ptpython.prompt_style import PromptStyle from ptpython.repl import embed -def configure(repl): +def configure(repl) -> None: # Probably, the best is to add a new PromptStyle to `all_prompt_styles` and # activate it. This way, the other styles are still selectable from the # menu. class CustomPrompt(PromptStyle): - def in_prompt(self): + def in_prompt(self) -> AnyFormattedText: return HTML("Input[%s]: ") % ( repl.current_statement_index, ) - def in2_prompt(self, width): + def in2_prompt(self, width: int) -> AnyFormattedText: return "...: ".rjust(width) - def out_prompt(self): + def out_prompt(self) -> AnyFormattedText: return HTML("Result[%s]: ") % ( repl.current_statement_index, ) @@ -30,7 +30,7 @@ def out_prompt(self): repl.prompt_style = "custom" -def main(): +def main() -> None: embed(globals(), locals(), configure=configure) diff --git a/examples/python-embed.py b/examples/python-embed.py index ac2cd06f..49224ac2 100755 --- a/examples/python-embed.py +++ b/examples/python-embed.py @@ -4,7 +4,7 @@ from ptpython.repl import embed -def main(): +def main() -> None: embed(globals(), locals(), vi_mode=False) diff --git a/examples/ssh-and-telnet-embed.py b/examples/ssh-and-telnet-embed.py index 378784ce..62fa76d9 100755 --- a/examples/ssh-and-telnet-embed.py +++ b/examples/ssh-and-telnet-embed.py @@ -11,13 +11,16 @@ import asyncssh from prompt_toolkit import print_formatted_text -from prompt_toolkit.contrib.ssh.server import PromptToolkitSSHServer +from prompt_toolkit.contrib.ssh.server import ( + PromptToolkitSSHServer, + PromptToolkitSSHSession, +) from prompt_toolkit.contrib.telnet.server import TelnetServer from ptpython.repl import embed -def ensure_key(filename="ssh_host_key"): +def ensure_key(filename: str = "ssh_host_key") -> str: path = pathlib.Path(filename) if not path.exists(): rsa_key = asyncssh.generate_private_key("ssh-rsa") @@ -25,12 +28,12 @@ def ensure_key(filename="ssh_host_key"): return str(path) -async def interact(connection=None): +async def interact(connection: PromptToolkitSSHSession) -> None: global_dict = {**globals(), "print": print_formatted_text} await embed(return_asyncio_coroutine=True, globals=global_dict) -async def main(ssh_port=8022, telnet_port=8023): +async def main(ssh_port: int = 8022, telnet_port: int = 8023) -> None: ssh_server = PromptToolkitSSHServer(interact=interact) await asyncssh.create_server( lambda: ssh_server, "", ssh_port, server_host_keys=[ensure_key()] diff --git a/ptpython/contrib/asyncssh_repl.py b/ptpython/contrib/asyncssh_repl.py index 051519de..35da7426 100644 --- a/ptpython/contrib/asyncssh_repl.py +++ b/ptpython/contrib/asyncssh_repl.py @@ -9,20 +9,20 @@ from __future__ import annotations import asyncio -from typing import Any, TextIO, cast +from typing import Any, AnyStr, TextIO, cast import asyncssh from prompt_toolkit.data_structures import Size from prompt_toolkit.input import create_pipe_input from prompt_toolkit.output.vt100 import Vt100_Output -from ptpython.python_input import _GetNamespace +from ptpython.python_input import _GetNamespace, _Namespace from ptpython.repl import PythonRepl __all__ = ["ReplSSHServerSession"] -class ReplSSHServerSession(asyncssh.SSHServerSession): +class ReplSSHServerSession(asyncssh.SSHServerSession[str]): """ SSH server session that runs a Python REPL. @@ -35,7 +35,7 @@ def __init__( ) -> None: self._chan: Any = None - def _globals() -> dict: + def _globals() -> _Namespace: data = get_globals() data.setdefault("print", self._print) return data @@ -79,7 +79,7 @@ def _get_size(self) -> Size: width, height, pixwidth, pixheight = self._chan.get_terminal_size() return Size(rows=height, columns=width) - def connection_made(self, chan): + def connection_made(self, chan: Any) -> None: """ Client connected, run repl in coroutine. """ @@ -89,7 +89,7 @@ def connection_made(self, chan): f = asyncio.ensure_future(self.repl.run_async()) # Close channel when done. - def done(_) -> None: + def done(_: object) -> None: chan.close() self._chan = None @@ -98,24 +98,28 @@ def done(_) -> None: def shell_requested(self) -> bool: return True - def terminal_size_changed(self, width, height, pixwidth, pixheight): + def terminal_size_changed( + self, width: int, height: int, pixwidth: int, pixheight: int + ) -> None: """ When the terminal size changes, report back to CLI. """ self.repl.app._on_resize() - def data_received(self, data, datatype): + def data_received(self, data: AnyStr, datatype: int | None) -> None: """ When data is received, send to inputstream of the CLI and repaint. """ self._input_pipe.send(data) - def _print(self, *data, sep=" ", end="\n", file=None) -> None: + def _print( + self, *data: object, sep: str = " ", end: str = "\n", file: Any = None + ) -> None: """ Alternative 'print' function that prints back into the SSH channel. """ # Pop keyword-only arguments. (We cannot use the syntax from the # signature. Otherwise, Python2 will give a syntax error message when # installing.) - data = sep.join(map(str, data)) - self._chan.write(data + end) + data_as_str = sep.join(map(str, data)) + self._chan.write(data_as_str + end) diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 211d36c9..14995db4 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -6,7 +6,7 @@ from asyncio import get_event_loop from functools import partial -from typing import TYPE_CHECKING, Any, Callable, Dict, Generic, Mapping, TypeVar +from typing import TYPE_CHECKING, Any, Callable, Dict, Generic, Mapping, TypeVar, Union from prompt_toolkit.application import Application, get_app from prompt_toolkit.auto_suggest import ( @@ -31,7 +31,7 @@ ) from prompt_toolkit.document import Document from prompt_toolkit.enums import DEFAULT_BUFFER, EditingMode -from prompt_toolkit.filters import Condition +from prompt_toolkit.filters import Condition, FilterOrBool from prompt_toolkit.formatted_text import AnyFormattedText from prompt_toolkit.history import ( FileHistory, @@ -49,8 +49,13 @@ from prompt_toolkit.key_binding.bindings.open_in_editor import ( load_open_in_editor_bindings, ) +from prompt_toolkit.key_binding.key_bindings import Binding, KeyHandlerCallable +from prompt_toolkit.key_binding.key_processor import KeyPressEvent from prompt_toolkit.key_binding.vi_state import InputMode +from prompt_toolkit.keys import Keys from prompt_toolkit.layout.containers import AnyContainer +from prompt_toolkit.layout.dimension import AnyDimension +from prompt_toolkit.layout.processors import Processor from prompt_toolkit.lexers import DynamicLexer, Lexer, SimpleLexer from prompt_toolkit.output import ColorDepth, Output from prompt_toolkit.styles import ( @@ -91,22 +96,23 @@ from typing_extensions import Protocol class _SupportsLessThan(Protocol): - # Taken from typeshed. _T is used by "sorted", which needs anything + # Taken from typeshed. _T_lt is used by "sorted", which needs anything # sortable. def __lt__(self, __other: Any) -> bool: ... -_T = TypeVar("_T", bound="_SupportsLessThan") +_T_lt = TypeVar("_T_lt", bound="_SupportsLessThan") +_T_kh = TypeVar("_T_kh", bound=Union[KeyHandlerCallable, Binding]) -class OptionCategory(Generic[_T]): - def __init__(self, title: str, options: list[Option[_T]]) -> None: +class OptionCategory(Generic[_T_lt]): + def __init__(self, title: str, options: list[Option[_T_lt]]) -> None: self.title = title self.options = options -class Option(Generic[_T]): +class Option(Generic[_T_lt]): """ Ptpython configuration option that can be shown and modified from the sidebar. @@ -122,10 +128,10 @@ def __init__( self, title: str, description: str, - get_current_value: Callable[[], _T], + get_current_value: Callable[[], _T_lt], # We accept `object` as return type for the select functions, because # often they return an unused boolean. Maybe this can be improved. - get_values: Callable[[], Mapping[_T, Callable[[], object]]], + get_values: Callable[[], Mapping[_T_lt, Callable[[], object]]], ) -> None: self.title = title self.description = description @@ -133,7 +139,7 @@ def __init__( self.get_values = get_values @property - def values(self) -> Mapping[_T, Callable[[], object]]: + def values(self) -> Mapping[_T_lt, Callable[[], object]]: return self.get_values() def activate_next(self, _previous: bool = False) -> None: @@ -208,10 +214,10 @@ def __init__( _completer: Completer | None = None, _validator: Validator | None = None, _lexer: Lexer | None = None, - _extra_buffer_processors=None, + _extra_buffer_processors: list[Processor] | None = None, _extra_layout_body: AnyContainer | None = None, - _extra_toolbars=None, - _input_buffer_height=None, + _extra_toolbars: list[AnyContainer] | None = None, + _input_buffer_height: AnyDimension | None = None, ) -> None: self.get_globals: _GetNamespace = get_globals or (lambda: {}) self.get_locals: _GetNamespace = get_locals or self.get_globals @@ -466,24 +472,36 @@ def get_compiler_flags(self) -> int: return flags - @property - def add_key_binding(self) -> Callable[[_T], _T]: + def add_key_binding( + self, + *keys: Keys | str, + filter: FilterOrBool = True, + eager: FilterOrBool = False, + is_global: FilterOrBool = False, + save_before: Callable[[KeyPressEvent], bool] = (lambda e: True), + record_in_macro: FilterOrBool = True, + ) -> Callable[[_T_kh], _T_kh]: """ Shortcut for adding new key bindings. (Mostly useful for a config.py file, that receives a PythonInput/Repl instance as input.) + All arguments are identical to prompt_toolkit's `KeyBindings.add`. + :: @python_input.add_key_binding(Keys.ControlX, filter=...) def handler(event): ... """ - - def add_binding_decorator(*k, **kw): - return self.extra_key_bindings.add(*k, **kw) - - return add_binding_decorator + return self.extra_key_bindings.add( + *keys, + filter=filter, + eager=eager, + is_global=is_global, + save_before=save_before, + record_in_macro=record_in_macro, + ) def install_code_colorscheme(self, name: str, style: BaseStyle) -> None: """ diff --git a/ptpython/repl.py b/ptpython/repl.py index 98b01afa..1db2e64f 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -158,7 +158,7 @@ def run(self) -> None: clear_title() self._remove_from_namespace() - async def run_and_show_expression_async(self, text: str): + async def run_and_show_expression_async(self, text: str) -> object: loop = asyncio.get_event_loop() try: From f0193017e5a38b07e9ad1dfdac8bc5e416229089 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 12 Dec 2023 22:22:00 +0000 Subject: [PATCH 419/470] Many asyncio REPL improvements. - Added `--asyncio` flag to the `ptpython` entry point to activate the asyncio-REPL. This will ensure that an event loop is created at the start in which we can run top-level await statements. - Use `get_running_loop()` instead of `get_event_loop()`. - Better handling of `SystemExit` and control-c in the async REPL. --- ptpython/contrib/asyncssh_repl.py | 2 +- ptpython/entry_points/run_ptpython.py | 13 ++++- ptpython/python_input.py | 4 +- ptpython/repl.py | 76 +++++++++++++++++++-------- 4 files changed, 70 insertions(+), 25 deletions(-) diff --git a/ptpython/contrib/asyncssh_repl.py b/ptpython/contrib/asyncssh_repl.py index 35da7426..2f74eb2b 100644 --- a/ptpython/contrib/asyncssh_repl.py +++ b/ptpython/contrib/asyncssh_repl.py @@ -110,7 +110,7 @@ def data_received(self, data: AnyStr, datatype: int | None) -> None: """ When data is received, send to inputstream of the CLI and repaint. """ - self._input_pipe.send(data) + self._input_pipe.send(data) # type: ignore def _print( self, *data: object, sep: str = " ", end: str = "\n", file: Any = None diff --git a/ptpython/entry_points/run_ptpython.py b/ptpython/entry_points/run_ptpython.py index c0b4078b..7fa69c66 100644 --- a/ptpython/entry_points/run_ptpython.py +++ b/ptpython/entry_points/run_ptpython.py @@ -9,6 +9,7 @@ -h, --help show this help message and exit --vi Enable Vi key bindings -i, --interactive Start interactive shell after executing this file. + --asyncio Run an asyncio event loop to support top-level "await". --light-bg Run on a light background (use dark colors for text). --dark-bg Run on a dark background (use light colors for text). --config-file CONFIG_FILE @@ -24,6 +25,7 @@ from __future__ import annotations import argparse +import asyncio import os import pathlib import sys @@ -68,6 +70,11 @@ def create_parser() -> _Parser: action="store_true", help="Start interactive shell after executing this file.", ) + parser.add_argument( + "--asyncio", + action="store_true", + help='Run an asyncio event loop to support top-level "await".', + ) parser.add_argument( "--light-bg", action="store_true", @@ -206,7 +213,7 @@ def configure(repl: PythonRepl) -> None: import __main__ - embed( + embed_result = embed( # type: ignore vi_mode=a.vi, history_filename=history_file, configure=configure, @@ -214,8 +221,12 @@ def configure(repl: PythonRepl) -> None: globals=__main__.__dict__, startup_paths=startup_paths, title="Python REPL (ptpython)", + return_asyncio_coroutine=a.asyncio, ) + if a.asyncio: + asyncio.run(embed_result) + if __name__ == "__main__": run() diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 14995db4..54ddbef2 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -4,7 +4,7 @@ """ from __future__ import annotations -from asyncio import get_event_loop +from asyncio import get_running_loop from functools import partial from typing import TYPE_CHECKING, Any, Callable, Dict, Generic, Mapping, TypeVar, Union @@ -1010,7 +1010,7 @@ def get_signatures_in_executor(document: Document) -> list[Signature]: app = self.app async def on_timeout_task() -> None: - loop = get_event_loop() + loop = get_running_loop() # Never run multiple get-signature threads. if self._get_signatures_thread_running: diff --git a/ptpython/repl.py b/ptpython/repl.py index 1db2e64f..e7058ea1 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -12,6 +12,7 @@ import asyncio import builtins import os +import signal import sys import traceback import types @@ -158,27 +159,58 @@ def run(self) -> None: clear_title() self._remove_from_namespace() - async def run_and_show_expression_async(self, text: str) -> object: - loop = asyncio.get_event_loop() + async def run_and_show_expression_async(self, text: str) -> Any: + loop = asyncio.get_running_loop() + system_exit: SystemExit | None = None try: - result = await self.eval_async(text) - except KeyboardInterrupt: # KeyboardInterrupt doesn't inherit from Exception. - raise - except SystemExit: - return - except BaseException as e: - self._handle_exception(e) - else: - # Print. - if result is not None: - await loop.run_in_executor(None, lambda: self._show_result(result)) + try: + # Create `eval` task. Ensure that control-c will cancel this + # task. + async def eval() -> Any: + nonlocal system_exit + try: + return await self.eval_async(text) + except SystemExit as e: + # Don't propagate SystemExit in `create_task()`. That + # will kill the event loop. We want to handle it + # gracefully. + system_exit = e + + task = asyncio.create_task(eval()) + loop.add_signal_handler(signal.SIGINT, lambda *_: task.cancel()) + result = await task + + if system_exit is not None: + raise system_exit + except KeyboardInterrupt: + # KeyboardInterrupt doesn't inherit from Exception. + raise + except SystemExit: + raise + except BaseException as e: + self._handle_exception(e) + else: + # Print. + if result is not None: + await loop.run_in_executor(None, lambda: self._show_result(result)) - # Loop. - self.current_statement_index += 1 - self.signatures = [] - # Return the result for future consumers. - return result + # Loop. + self.current_statement_index += 1 + self.signatures = [] + # Return the result for future consumers. + return result + finally: + loop.remove_signal_handler(signal.SIGINT) + + except KeyboardInterrupt as e: + # Handle all possible `KeyboardInterrupt` errors. This can + # happen during the `eval`, but also during the + # `show_result` if something takes too long. + # (Try/catch is around the whole block, because we want to + # prevent that a Control-C keypress terminates the REPL in + # any case.) + self._handle_keyboard_interrupt(e) async def run_async(self) -> None: """ @@ -192,7 +224,7 @@ async def run_async(self) -> None: (Both for control-C to work, as well as for the code to see the right thread in which it was embedded). """ - loop = asyncio.get_event_loop() + loop = asyncio.get_running_loop() if self.terminal_title: set_title(self.terminal_title) @@ -222,6 +254,8 @@ async def run_async(self) -> None: # `KeyboardInterrupt` exceptions can end up in the event # loop selector. self._handle_keyboard_interrupt(e) + except SystemExit: + return finally: if self.terminal_title: clear_title() @@ -250,7 +284,7 @@ def eval(self, line: str) -> object: result = eval(code, self.get_globals(), self.get_locals()) if _has_coroutine_flag(code): - result = asyncio.get_event_loop().run_until_complete(result) + result = asyncio.get_running_loop().run_until_complete(result) self._store_eval_result(result) return result @@ -263,7 +297,7 @@ def eval(self, line: str) -> object: result = eval(code, self.get_globals(), self.get_locals()) if _has_coroutine_flag(code): - result = asyncio.get_event_loop().run_until_complete(result) + result = asyncio.get_running_loop().run_until_complete(result) return None From eb39a3201eb6f45f95f1d47434c5e31f3bd4ed36 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 12 Dec 2023 22:30:49 +0000 Subject: [PATCH 420/470] Show help information when starting asyncio-REPL. --- ptpython/entry_points/run_ptpython.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ptpython/entry_points/run_ptpython.py b/ptpython/entry_points/run_ptpython.py index 7fa69c66..1d4a5329 100644 --- a/ptpython/entry_points/run_ptpython.py +++ b/ptpython/entry_points/run_ptpython.py @@ -225,6 +225,8 @@ def configure(repl: PythonRepl) -> None: ) if a.asyncio: + print("Starting ptpython asyncio REPL") + print('Use "await" directly instead of "asyncio.run()".') asyncio.run(embed_result) From 96d621cf305ae4cf9a29db5d92f0a5b510470cf0 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 12 Dec 2023 22:34:23 +0000 Subject: [PATCH 421/470] Added info about top-level await to the README. --- README.rst | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.rst b/README.rst index 2db3f695..8ec9aca4 100644 --- a/README.rst +++ b/README.rst @@ -71,6 +71,7 @@ The help menu shows basic command-line options. -h, --help show this help message and exit --vi Enable Vi key bindings -i, --interactive Start interactive shell after executing this file. + --asyncio Run an asyncio event loop to support top-level "await". --light-bg Run on a light background (use dark colors for text). --dark-bg Run on a dark background (use light colors for text). --config-file CONFIG_FILE @@ -171,6 +172,20 @@ error. .. image :: https://github.com/jonathanslenders/ptpython/raw/master/docs/images/validation.png +Asyncio REPL and top level await +******************************** + +In order to get top-level ``await`` support, start ptpython as follows: + +.. code:: + + ptpython --asyncio + +This will spawn an asyncio event loop and embed the async REPL in the event +loop. After this, top-level await will work and statements like ``await +asyncio.sleep(10)`` will execute. + + Additional features ******************* From eda7f58d453c3c1b96e4357dfa203f3160cfc4c1 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Wed, 13 Dec 2023 09:33:54 +0000 Subject: [PATCH 422/470] Required prompt_toolkit 3.0.34 because of 'OneStyleAndTextTuple' import. --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index ad26545a..d091d290 100644 --- a/setup.py +++ b/setup.py @@ -21,8 +21,8 @@ "appdirs", "importlib_metadata;python_version<'3.8'", "jedi>=0.16.0", - # Use prompt_toolkit 3.0.28, because of cursor shape support. - "prompt_toolkit>=3.0.28,<3.1.0", + # Use prompt_toolkit 3.0.34, because of `OneStyleAndTextTuple` import. + "prompt_toolkit>=3.0.34,<3.1.0", "pygments", ], python_requires=">=3.7", From d2e35e7c617a015299ce10b53d30067b347b03c9 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Wed, 13 Dec 2023 09:35:13 +0000 Subject: [PATCH 423/470] Release 3.0.24 --- CHANGELOG | 20 ++++++++++++++++++++ setup.py | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index e753cfd9..879e7439 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,26 @@ CHANGELOG ========= +3.0.24: 2023-12-13 +------------------ + +Fixes: +- Don't show "Impossible to read config file" warnings when no config file was + passed to `run_config()`. +- IPython integration fixes: + * Fix top-level await in IPython. + * Fix IPython `DeprecationWarning`. +- Output printing fixes: + * Paginate exceptions if pagination is enabled. + * Handle big outputs without running out of memory. +- Asyncio REPL improvements: + * From now on, passing `--asyncio` is required to activate the asyncio-REPL. + This will ensure that an event loop is created at the start in which we can + run top-level await statements. + * Use `get_running_loop()` instead of `get_event_loop()`. + * Better handling of `SystemExit` and control-c in the async REPL. + + 3.0.23: 2023-02-22 ------------------ diff --git a/setup.py b/setup.py index d091d290..a35a4797 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ setup( name="ptpython", author="Jonathan Slenders", - version="3.0.23", + version="3.0.24", url="https://github.com/prompt-toolkit/ptpython", description="Python REPL build on top of prompt_toolkit", long_description=long_description, From 961b945abb20d4d57615da97905d5d00ab10f1fe Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Wed, 13 Dec 2023 12:12:04 +0000 Subject: [PATCH 424/470] Fix handling of 'config file does not exist' when embedding ptpython. --- ptpython/repl.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ptpython/repl.py b/ptpython/repl.py index e7058ea1..fc9b9da1 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -433,9 +433,10 @@ def enter_to_continue() -> None: input("\nPress ENTER to continue...") # Check whether this file exists. - if not os.path.exists(config_file) and explicit_config_file: - print("Impossible to read %r" % config_file) - enter_to_continue() + if not os.path.exists(config_file): + if explicit_config_file: + print(f"Impossible to read {config_file}") + enter_to_continue() return # Run the config file in an empty namespace. From 1a96f0ee6a2691c18dd91d756d045f488975faec Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 14 Dec 2023 09:33:03 +0000 Subject: [PATCH 425/470] Release 3.0.25 --- CHANGELOG | 7 +++++++ setup.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 879e7439..e8277002 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,13 @@ CHANGELOG ========= +3.0.25: 2023-12-14 +------------------ + +Fixes: +- Fix handling of 'config file does not exist' when embedding ptpython. + + 3.0.24: 2023-12-13 ------------------ diff --git a/setup.py b/setup.py index a35a4797..bc1241bb 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ setup( name="ptpython", author="Jonathan Slenders", - version="3.0.24", + version="3.0.25", url="https://github.com/prompt-toolkit/ptpython", description="Python REPL build on top of prompt_toolkit", long_description=long_description, From 17d04b9f9d4c812ed4d161c110fe9cd54069c4be Mon Sep 17 00:00:00 2001 From: tomaszchalupnik Date: Fri, 2 Feb 2024 22:28:50 +0100 Subject: [PATCH 426/470] Reraise GeneratorExit error as excepted exception --- ptpython/printer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ptpython/printer.py b/ptpython/printer.py index 3618934e..85bd9c88 100644 --- a/ptpython/printer.py +++ b/ptpython/printer.py @@ -155,7 +155,7 @@ def _format_result_output( ) yield from formatted_result_repr return - except KeyboardInterrupt: + except (GeneratorExit, KeyboardInterrupt): raise # Don't catch here. except: # For bad code, `__getattr__` can raise something that's not an From 1c558f861c2d47ad7bdf639567fa9a5c9237ade1 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 6 Feb 2024 10:16:06 +0000 Subject: [PATCH 427/470] Release 3.0.26 --- CHANGELOG | 7 +++++++ setup.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index e8277002..d8738625 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,13 @@ CHANGELOG ========= +3.0.26: 2024-02-06 +------------------ + +Fixes: +- Handle `GeneratorExit` exception when leaving the paginator. + + 3.0.25: 2023-12-14 ------------------ diff --git a/setup.py b/setup.py index bc1241bb..a54da35d 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ setup( name="ptpython", author="Jonathan Slenders", - version="3.0.25", + version="3.0.26", url="https://github.com/prompt-toolkit/ptpython", description="Python REPL build on top of prompt_toolkit", long_description=long_description, From 4f6b3a3d8a60387cf9e22e6112a320809ab91679 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 17 Dec 2023 07:11:01 -0600 Subject: [PATCH 428/470] Package: Add PyPI Links to repo, issues, and changelog --- setup.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/setup.py b/setup.py index a54da35d..38f30282 100644 --- a/setup.py +++ b/setup.py @@ -15,6 +15,14 @@ url="https://github.com/prompt-toolkit/ptpython", description="Python REPL build on top of prompt_toolkit", long_description=long_description, + package_urls={ + "Changelog": "https://github.com/prompt-toolkit/ptpython/blob/master/CHANGELOG", + }, + project_urls={ + "Bug Tracker": "https://github.com/prompt-toolkit/ptpython/issues", + "Source Code": "https://github.com/prompt-toolkit/ptpython", + "Changelog": "https://github.com/prompt-toolkit/ptpython/blob/master/CHANGELOG", + }, packages=find_packages("."), package_data={"ptpython": ["py.typed"]}, install_requires=[ From d63ebc5cdb60fd57db524eaee97b099acf45dee6 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 17 Dec 2023 07:01:28 -0600 Subject: [PATCH 429/470] docs(README): Update GitHub action button --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 8ec9aca4..8616132a 100644 --- a/README.rst +++ b/README.rst @@ -288,8 +288,8 @@ Special thanks to - `wcwidth `_: Determine columns needed for a wide characters. - `prompt_toolkit `_ for the interface. -.. |Build Status| image:: https://api.travis-ci.org/prompt-toolkit/ptpython.svg?branch=master - :target: https://travis-ci.org/prompt-toolkit/ptpython# +.. |Build Status| image:: https://github.com/prompt-toolkit/ptpython/actions/workflows/test.yaml/badge.svg + :target: https://github.com/prompt-toolkit/ptpython/actions/workflows/test.yaml .. |License| image:: https://img.shields.io/github/license/prompt-toolkit/ptpython.svg :target: https://github.com/prompt-toolkit/ptpython/blob/master/LICENSE From f40e091012e9022babafe5a077bea7da154e3b39 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 17 Dec 2023 07:02:33 -0600 Subject: [PATCH 430/470] docs(README): Fix PyPI badge and link --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 8616132a..63d9aca5 100644 --- a/README.rst +++ b/README.rst @@ -294,6 +294,6 @@ Special thanks to .. |License| image:: https://img.shields.io/github/license/prompt-toolkit/ptpython.svg :target: https://github.com/prompt-toolkit/ptpython/blob/master/LICENSE -.. |PyPI| image:: https://pypip.in/version/ptpython/badge.svg - :target: https://pypi.python.org/pypi/ptpython/ +.. |PyPI| image:: https://img.shields.io/pypi/v/ptpython.svg + :target: https://pypi.org/project/ptpython/ :alt: Latest Version From 7f76e0df8697fd134e4d785343e143ba3b2f0780 Mon Sep 17 00:00:00 2001 From: Matthew Judy Date: Tue, 27 Feb 2024 18:07:25 -0500 Subject: [PATCH 431/470] Update `prompt_toolkit` from `3.0.34` to `3.0.43` Resolves https://github.com/prompt-toolkit/ptpython/issues/564 where `cannot import name 'OneStyleAndTextTuple'` is emitted when launching `ptipython`. --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 38f30282..b2fde169 100644 --- a/setup.py +++ b/setup.py @@ -29,8 +29,8 @@ "appdirs", "importlib_metadata;python_version<'3.8'", "jedi>=0.16.0", - # Use prompt_toolkit 3.0.34, because of `OneStyleAndTextTuple` import. - "prompt_toolkit>=3.0.34,<3.1.0", + # Use prompt_toolkit 3.0.43, because of `OneStyleAndTextTuple` import. + "prompt_toolkit>=3.0.43,<3.1.0", "pygments", ], python_requires=">=3.7", From 3df92f35d86f048f5c634c3c8ba853ad7bc80568 Mon Sep 17 00:00:00 2001 From: "David J. Mack" Date: Fri, 18 Nov 2022 16:32:48 +0100 Subject: [PATCH 432/470] docs: Add windows terminal profile configuration --- README.rst | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.rst b/README.rst index 63d9aca5..130e4581 100644 --- a/README.rst +++ b/README.rst @@ -255,6 +255,22 @@ Windows. Some things might not work, but it is usable: .. image :: https://github.com/jonathanslenders/ptpython/raw/master/docs/images/windows.png +Windows terminal integration +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you are using the `Windows Terminal `_ and want to +integrate ``ptpython`` as a profile, go to *Settings -> Open JSON file* and add the +following profile under *profiles.list*: + +.. code-block:: JSON + + { + "commandline": "%SystemRoot%\\System32\\cmd.exe /k ptpython", + "guid": "{f91d49a3-741b-409c-8a15-c4360649121f}", + "hidden": false, + "icon": "https://upload.wikimedia.org/wikipedia/commons/e/e6/Python_Windows_interpreter_icon_2006%E2%80%932016_Tiny.png", + "name": "ptpython@cmd" + } FAQ *** From 394fe38a2ec1206131036d901688dd695bdda439 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 16 May 2024 12:05:31 +0000 Subject: [PATCH 433/470] Limit number of completions to 5k (for performance). --- ptpython/completer.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ptpython/completer.py b/ptpython/completer.py index 91d66474..264918e8 100644 --- a/ptpython/completer.py +++ b/ptpython/completer.py @@ -6,6 +6,7 @@ import keyword import re from enum import Enum +from itertools import islice from typing import TYPE_CHECKING, Any, Callable, Iterable from prompt_toolkit.completion import ( @@ -617,7 +618,10 @@ def __init__( def get_completions( self, document: Document, complete_event: CompleteEvent ) -> Iterable[Completion]: - completions = list(self.completer.get_completions(document, complete_event)) + completions = list( + # Limit at 5k completions for performance. + islice(self.completer.get_completions(document, complete_event), 0, 5000) + ) complete_private_attributes = self.complete_private_attributes() hide_private = False From 5fb21bd51f71d220c018fca9d732df48c72c52b8 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 16 May 2024 12:07:47 +0000 Subject: [PATCH 434/470] Apply latest Ruff for formatting. --- examples/asyncio-python-embed.py | 1 + examples/asyncio-ssh-python-embed.py | 1 + examples/ptpython_config/config.py | 1 + examples/python-embed-with-custom-prompt.py | 1 + examples/python-embed.py | 4 ++-- examples/python-input.py | 4 ++-- examples/test-cases/ptpython-in-other-thread.py | 1 + ptpython/__main__.py | 1 + ptpython/contrib/asyncssh_repl.py | 1 + ptpython/entry_points/run_ptpython.py | 1 + ptpython/eventloop.py | 1 + ptpython/history_browser.py | 1 + ptpython/ipython.py | 1 + ptpython/layout.py | 1 + ptpython/python_input.py | 4 ++-- ptpython/repl.py | 1 + ptpython/signatures.py | 1 + ptpython/utils.py | 1 + pyproject.toml | 8 ++++---- 19 files changed, 25 insertions(+), 10 deletions(-) diff --git a/examples/asyncio-python-embed.py b/examples/asyncio-python-embed.py index a8fbba5a..38cc1c20 100755 --- a/examples/asyncio-python-embed.py +++ b/examples/asyncio-python-embed.py @@ -11,6 +11,7 @@ to stdout, it won't break the input line, but instead writes nicely above the prompt. """ + import asyncio from ptpython.repl import embed diff --git a/examples/asyncio-ssh-python-embed.py b/examples/asyncio-ssh-python-embed.py index be0689e7..9bbad86f 100755 --- a/examples/asyncio-ssh-python-embed.py +++ b/examples/asyncio-ssh-python-embed.py @@ -5,6 +5,7 @@ Run this example and then SSH to localhost, port 8222. """ + import asyncio import logging diff --git a/examples/ptpython_config/config.py b/examples/ptpython_config/config.py index b25850a2..bfd3914e 100644 --- a/examples/ptpython_config/config.py +++ b/examples/ptpython_config/config.py @@ -5,6 +5,7 @@ On Linux, this is: ~/.config/ptpython/config.py On macOS, this is: ~/Library/Application Support/ptpython/config.py """ + from prompt_toolkit.filters import ViInsertMode from prompt_toolkit.key_binding.key_processor import KeyPress from prompt_toolkit.keys import Keys diff --git a/examples/python-embed-with-custom-prompt.py b/examples/python-embed-with-custom-prompt.py index d54da1da..5e8c7079 100755 --- a/examples/python-embed-with-custom-prompt.py +++ b/examples/python-embed-with-custom-prompt.py @@ -2,6 +2,7 @@ """ Example of embedding a Python REPL, and setting a custom prompt. """ + from prompt_toolkit.formatted_text import HTML, AnyFormattedText from ptpython.prompt_style import PromptStyle diff --git a/examples/python-embed.py b/examples/python-embed.py index 49224ac2..a7481011 100755 --- a/examples/python-embed.py +++ b/examples/python-embed.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -""" -""" +""" """ + from ptpython.repl import embed diff --git a/examples/python-input.py b/examples/python-input.py index 567c2ee6..d586d0f5 100755 --- a/examples/python-input.py +++ b/examples/python-input.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -""" -""" +""" """ + from ptpython.python_input import PythonInput diff --git a/examples/test-cases/ptpython-in-other-thread.py b/examples/test-cases/ptpython-in-other-thread.py index 7c788464..bfe14109 100644 --- a/examples/test-cases/ptpython-in-other-thread.py +++ b/examples/test-cases/ptpython-in-other-thread.py @@ -5,6 +5,7 @@ (For testing whether it's working fine if it's not embedded in the main thread.) """ + import threading from ptpython.repl import embed diff --git a/ptpython/__main__.py b/ptpython/__main__.py index c0062613..3a2f7ddf 100644 --- a/ptpython/__main__.py +++ b/ptpython/__main__.py @@ -1,6 +1,7 @@ """ Make `python -m ptpython` an alias for running `./ptpython`. """ + from __future__ import annotations from .entry_points.run_ptpython import run diff --git a/ptpython/contrib/asyncssh_repl.py b/ptpython/contrib/asyncssh_repl.py index 2f74eb2b..a86737b6 100644 --- a/ptpython/contrib/asyncssh_repl.py +++ b/ptpython/contrib/asyncssh_repl.py @@ -6,6 +6,7 @@ should make sure not to use Python 3-only syntax, because this package should be installable in Python 2 as well! """ + from __future__ import annotations import asyncio diff --git a/ptpython/entry_points/run_ptpython.py b/ptpython/entry_points/run_ptpython.py index 1d4a5329..05df9714 100644 --- a/ptpython/entry_points/run_ptpython.py +++ b/ptpython/entry_points/run_ptpython.py @@ -22,6 +22,7 @@ PTPYTHON_CONFIG_HOME: a configuration directory to use PYTHONSTARTUP: file executed on interactive startup (no default) """ + from __future__ import annotations import argparse diff --git a/ptpython/eventloop.py b/ptpython/eventloop.py index 14ab64be..670d09bc 100644 --- a/ptpython/eventloop.py +++ b/ptpython/eventloop.py @@ -7,6 +7,7 @@ in readline. ``prompt-toolkit`` doesn't understand that input hook, but this will fix it for Tk.) """ + from __future__ import annotations import sys diff --git a/ptpython/history_browser.py b/ptpython/history_browser.py index b667be12..383cd975 100644 --- a/ptpython/history_browser.py +++ b/ptpython/history_browser.py @@ -4,6 +4,7 @@ `create_history_application` creates an `Application` instance that runs will run as a sub application of the Repl/PythonInput. """ + from __future__ import annotations from functools import partial diff --git a/ptpython/ipython.py b/ptpython/ipython.py index ad0516a3..263a981d 100644 --- a/ptpython/ipython.py +++ b/ptpython/ipython.py @@ -8,6 +8,7 @@ offer. """ + from __future__ import annotations from typing import Iterable diff --git a/ptpython/layout.py b/ptpython/layout.py index 2c1ec15f..fc00005b 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -1,6 +1,7 @@ """ Creation of the `Layout` instance for the Python input/REPL. """ + from __future__ import annotations import platform diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 54ddbef2..18421c88 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -2,6 +2,7 @@ Application for reading Python input. This can be used for creation of Python REPLs. """ + from __future__ import annotations from asyncio import get_running_loop @@ -98,8 +99,7 @@ class _SupportsLessThan(Protocol): # Taken from typeshed. _T_lt is used by "sorted", which needs anything # sortable. - def __lt__(self, __other: Any) -> bool: - ... + def __lt__(self, __other: Any) -> bool: ... _T_lt = TypeVar("_T_lt", bound="_SupportsLessThan") diff --git a/ptpython/repl.py b/ptpython/repl.py index fc9b9da1..bbbd852e 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -7,6 +7,7 @@ embed(globals(), locals(), vi_mode=False) """ + from __future__ import annotations import asyncio diff --git a/ptpython/signatures.py b/ptpython/signatures.py index d4cb98c2..b3e5c914 100644 --- a/ptpython/signatures.py +++ b/ptpython/signatures.py @@ -5,6 +5,7 @@ Either with the Jedi library, or using `inspect.signature` if Jedi fails and we can use `eval()` to evaluate the function object. """ + from __future__ import annotations import inspect diff --git a/ptpython/utils.py b/ptpython/utils.py index 28887d20..92cfc2a1 100644 --- a/ptpython/utils.py +++ b/ptpython/utils.py @@ -1,6 +1,7 @@ """ For internal use only. """ + from __future__ import annotations import re diff --git a/pyproject.toml b/pyproject.toml index 5421c454..ce420372 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.ruff] target-version = "py37" -select = [ +lint.select = [ "E", # pycodestyle errors "W", # pycodestyle warnings "F", # pyflakes @@ -12,14 +12,14 @@ select = [ "RUF100", # unused-noqa "Q", # quotes ] -ignore = [ +lint.ignore = [ "E501", # Line too long, handled by black "C901", # Too complex "E722", # bare except. ] -[tool.ruff.per-file-ignores] +[tool.ruff.lint.per-file-ignores] "examples/*" = ["T201"] # Print allowed in examples. "examples/ptpython_config/config.py" = ["F401"] # Unused imports in config. "ptpython/entry_points/run_ptipython.py" = ["T201", "F401"] # Print, import usage. @@ -30,6 +30,6 @@ ignore = [ "tests/run_tests.py" = ["F401"] # Unused imports. -[tool.ruff.isort] +[tool.ruff.lint.isort] known-first-party = ["ptpython"] known-third-party = ["prompt_toolkit", "pygments", "asyncssh"] From 3ec97d7360450f9d79a745a67e14312243227825 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 16 May 2024 12:09:19 +0000 Subject: [PATCH 435/470] Apply latest ruff fixes. --- ptpython/history_browser.py | 2 +- ptpython/ipython.py | 9 ++++----- ptpython/layout.py | 4 ++-- ptpython/prompt_style.py | 4 ++-- ptpython/python_input.py | 8 ++++---- ptpython/validator.py | 2 +- setup.py | 5 ++--- 7 files changed, 16 insertions(+), 18 deletions(-) diff --git a/ptpython/history_browser.py b/ptpython/history_browser.py index 383cd975..ae0ac03e 100644 --- a/ptpython/history_browser.py +++ b/ptpython/history_browser.py @@ -411,7 +411,7 @@ def __init__( if len(history_strings) > HISTORY_COUNT: history_lines[0] = ( - "# *** History has been truncated to %s lines ***" % HISTORY_COUNT + f"# *** History has been truncated to {HISTORY_COUNT} lines ***" ) self.history_lines = history_lines diff --git a/ptpython/ipython.py b/ptpython/ipython.py index 263a981d..0692214d 100644 --- a/ptpython/ipython.py +++ b/ptpython/ipython.py @@ -157,7 +157,7 @@ def get_completions( for m in sorted(self.magics_manager.magics["line"]): if m.startswith(text): - yield Completion("%s" % m, -len(text)) + yield Completion(f"{m}", -len(text)) class AliasCompleter(Completer): @@ -173,7 +173,7 @@ def get_completions( for a, cmd in sorted(aliases, key=lambda a: a[0]): if a.startswith(text): - yield Completion("%s" % a, -len(text), display_meta=cmd) + yield Completion(f"{a}", -len(text), display_meta=cmd) class IPythonInput(PythonInput): @@ -280,9 +280,8 @@ def initialize_extensions(shell, extensions): shell.extension_manager.load_extension(ext) except: warn( - "Error in loading extension: %s" % ext - + "\nCheck your config files in %s" - % ipy_utils.path.get_ipython_dir() + f"Error in loading extension: {ext}" + + f"\nCheck your config files in {ipy_utils.path.get_ipython_dir()}" ) shell.showtraceback() diff --git a/ptpython/layout.py b/ptpython/layout.py index fc00005b..622df594 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -132,7 +132,7 @@ def goto_next(mouse_event: MouseEvent) -> None: tokens.append(("class:sidebar" + sel, " >" if selected else " ")) tokens.append(("class:sidebar.label" + sel, "%-24s" % label, select_item)) tokens.append(("class:sidebar.status" + sel, " ", select_item)) - tokens.append(("class:sidebar.status" + sel, "%s" % status, goto_next)) + tokens.append(("class:sidebar.status" + sel, f"{status}", goto_next)) if selected: tokens.append(("[SetCursorPosition]", "")) @@ -529,7 +529,7 @@ def create_exit_confirmation( def get_text_fragments() -> StyleAndTextTuples: # Show "Do you really want to exit?" return [ - (style, "\n %s ([y]/n) " % python_input.exit_message), + (style, f"\n {python_input.exit_message} ([y]/n) "), ("[SetCursorPosition]", ""), (style, " \n"), ] diff --git a/ptpython/prompt_style.py b/ptpython/prompt_style.py index 96b738f7..465c3dbe 100644 --- a/ptpython/prompt_style.py +++ b/ptpython/prompt_style.py @@ -48,7 +48,7 @@ def __init__(self, python_input: PythonInput) -> None: def in_prompt(self) -> AnyFormattedText: return [ ("class:in", "In ["), - ("class:in.number", "%s" % self.python_input.current_statement_index), + ("class:in.number", f"{self.python_input.current_statement_index}"), ("class:in", "]: "), ] @@ -58,7 +58,7 @@ def in2_prompt(self, width: int) -> AnyFormattedText: def out_prompt(self) -> AnyFormattedText: return [ ("class:out", "Out["), - ("class:out.number", "%s" % self.python_input.current_statement_index), + ("class:out.number", f"{self.python_input.current_statement_index}"), ("class:out", "]:"), ("", " "), ] diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 18421c88..d66b5ae8 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -880,18 +880,18 @@ def get_values() -> dict[str, Callable[[], bool]]: Option( title="Min brightness", description="Minimum brightness for the color scheme (default=0.0).", - get_current_value=lambda: "%.2f" % self.min_brightness, + get_current_value=lambda: f"{self.min_brightness:.2f}", get_values=lambda: { - "%.2f" % value: partial(self._set_min_brightness, value) + f"{value:.2f}": partial(self._set_min_brightness, value) for value in brightness_values }, ), Option( title="Max brightness", description="Maximum brightness for the color scheme (default=1.0).", - get_current_value=lambda: "%.2f" % self.max_brightness, + get_current_value=lambda: f"{self.max_brightness:.2f}", get_values=lambda: { - "%.2f" % value: partial(self._set_max_brightness, value) + f"{value:.2f}": partial(self._set_max_brightness, value) for value in brightness_values }, ), diff --git a/ptpython/validator.py b/ptpython/validator.py index 91b9c284..cf2ee542 100644 --- a/ptpython/validator.py +++ b/ptpython/validator.py @@ -59,4 +59,4 @@ def validate(self, document: Document) -> None: except ValueError as e: # In Python 2, compiling "\x9" (an invalid escape sequence) raises # ValueError instead of SyntaxError. - raise ValidationError(0, "Syntax Error: %s" % e) + raise ValidationError(0, f"Syntax Error: {e}") diff --git a/setup.py b/setup.py index b2fde169..a2618a61 100644 --- a/setup.py +++ b/setup.py @@ -47,12 +47,11 @@ "console_scripts": [ "ptpython = ptpython.entry_points.run_ptpython:run", "ptipython = ptpython.entry_points.run_ptipython:run", - "ptpython%s = ptpython.entry_points.run_ptpython:run" % sys.version_info[0], + f"ptpython{sys.version_info[0]} = ptpython.entry_points.run_ptpython:run", "ptpython{}.{} = ptpython.entry_points.run_ptpython:run".format( *sys.version_info[:2] ), - "ptipython%s = ptpython.entry_points.run_ptipython:run" - % sys.version_info[0], + f"ptipython{sys.version_info[0]} = ptpython.entry_points.run_ptipython:run", "ptipython{}.{} = ptpython.entry_points.run_ptipython:run".format( *sys.version_info[:2] ), From c1a431047e88ae4b2e2b0613bf66c68095f61a4c Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 16 May 2024 12:21:32 +0000 Subject: [PATCH 436/470] Several typing fixes. --- ptpython/python_input.py | 14 ++++++-------- ptpython/repl.py | 17 +++++++++-------- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/ptpython/python_input.py b/ptpython/python_input.py index d66b5ae8..975d3d98 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -347,14 +347,6 @@ def __init__( "classic": ClassicPrompt(), } - self.get_input_prompt = lambda: self.all_prompt_styles[ - self.prompt_style - ].in_prompt() - - self.get_output_prompt = lambda: self.all_prompt_styles[ - self.prompt_style - ].out_prompt() - #: Load styles. self.code_styles: dict[str, BaseStyle] = get_all_code_styles() self.ui_styles = get_all_ui_styles() @@ -425,6 +417,12 @@ def __init__( else: self._app = None + def get_input_prompt(self) -> AnyFormattedText: + return self.all_prompt_styles[self.prompt_style].in_prompt() + + def get_output_prompt(self) -> AnyFormattedText: + return self.all_prompt_styles[self.prompt_style].out_prompt() + def _accept_handler(self, buff: Buffer) -> bool: app = get_app() app.exit(result=buff.text) diff --git a/ptpython/repl.py b/ptpython/repl.py index bbbd852e..ea2d84f0 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -19,7 +19,8 @@ import types import warnings from dis import COMPILER_FLAG_NAMES -from typing import Any, Callable, ContextManager, Iterable +from pathlib import Path +from typing import Any, Callable, ContextManager, Iterable, Sequence from prompt_toolkit.formatted_text import OneStyleAndTextTuple from prompt_toolkit.patch_stdout import patch_stdout as patch_stdout_context @@ -64,7 +65,7 @@ def _has_coroutine_flag(code: types.CodeType) -> bool: class PythonRepl(PythonInput): def __init__(self, *a, **kw) -> None: - self._startup_paths = kw.pop("startup_paths", None) + self._startup_paths: Sequence[str | Path] | None = kw.pop("startup_paths", None) super().__init__(*a, **kw) self._load_start_paths() @@ -348,7 +349,7 @@ def _store_eval_result(self, result: object) -> None: def get_compiler_flags(self) -> int: return super().get_compiler_flags() | PyCF_ALLOW_TOP_LEVEL_AWAIT - def _compile_with_flags(self, code: str, mode: str): + def _compile_with_flags(self, code: str, mode: str) -> Any: "Compile code with the right compiler flags." return compile( code, @@ -459,13 +460,13 @@ def enter_to_continue() -> None: def embed( - globals=None, - locals=None, + globals: dict[str, Any] | None = None, + locals: dict[str, Any] | None = None, configure: Callable[[PythonRepl], None] | None = None, vi_mode: bool = False, history_filename: str | None = None, title: str | None = None, - startup_paths=None, + startup_paths: Sequence[str | Path] | None = None, patch_stdout: bool = False, return_asyncio_coroutine: bool = False, ) -> None: @@ -494,10 +495,10 @@ def embed( locals = locals or globals - def get_globals(): + def get_globals() -> dict[str, Any]: return globals - def get_locals(): + def get_locals() -> dict[str, Any]: return locals # Create REPL. From 95afc939fe348558486139909b6273f1f7fa245c Mon Sep 17 00:00:00 2001 From: Elliot Ford Date: Fri, 10 May 2024 16:41:19 +0100 Subject: [PATCH 437/470] Update supported versions on README.rst --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 130e4581..06c1e02b 100644 --- a/README.rst +++ b/README.rst @@ -12,7 +12,7 @@ ptpython .. image :: https://github.com/jonathanslenders/ptpython/raw/master/docs/images/example1.png Ptpython is an advanced Python REPL. It should work on all -Python versions from 2.6 up to 3.9 and work cross platform (Linux, +Python versions from 2.6 up to 3.11 and work cross platform (Linux, BSD, OS X and Windows). Note: this version of ptpython requires at least Python 3.6. Install ptpython From 8f68b6ceccbe57d15cb864fae45a5e7b82524bdc Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Mon, 27 May 2024 20:52:04 +0000 Subject: [PATCH 438/470] Ruff compatibility: fix import order. --- ptpython/eventloop.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ptpython/eventloop.py b/ptpython/eventloop.py index 670d09bc..a6462748 100644 --- a/ptpython/eventloop.py +++ b/ptpython/eventloop.py @@ -24,9 +24,8 @@ def _inputhook_tk(inputhook_context: InputHookContext) -> None: Run the Tk eventloop until prompt-toolkit needs to process the next input. """ # Get the current TK application. - import tkinter - import _tkinter # Keep this imports inline! + import tkinter root = tkinter._default_root # type: ignore From fb9bed1e5956ac5f109fd4cb401b3fae997efcd7 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Mon, 27 May 2024 20:46:01 +0000 Subject: [PATCH 439/470] Release 3.0.27 --- CHANGELOG | 7 +++++++ setup.py | 7 +++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index d8738625..6f2bbb9a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,13 @@ CHANGELOG ========= +3.0.27: 2024-05-27 +------------------ + +- Limit number of completions to 5k (for performance). +- Several typing fixes. + + 3.0.26: 2024-02-06 ------------------ diff --git a/setup.py b/setup.py index a2618a61..84f18be2 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ setup( name="ptpython", author="Jonathan Slenders", - version="3.0.26", + version="3.0.27", url="https://github.com/prompt-toolkit/ptpython", description="Python REPL build on top of prompt_toolkit", long_description=long_description, @@ -38,8 +38,11 @@ "License :: OSI Approved :: BSD License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python", ], From f66a289544a21089f561e21f7632305ff4eed204 Mon Sep 17 00:00:00 2001 From: Valentin Valls Date: Fri, 28 Jun 2024 16:36:46 +0200 Subject: [PATCH 440/470] Clean up signatures on ctrl-c --- ptpython/python_input.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 975d3d98..b1773643 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -1116,4 +1116,5 @@ def pre_run( return result except KeyboardInterrupt: # Abort - try again. + self.signatures = [] self.default_buffer.document = Document() From 4b456890f9b06fc9ea75eef681bb9773c2172c89 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Mon, 22 Jul 2024 09:21:01 +0000 Subject: [PATCH 441/470] Add custom 'exit' function to return from REPL. - Don't terminate `sys.stdin` when `exit` is called (important for `embed()`). - Don't require 'exit' to be called with parentheses. --- ptpython/repl.py | 48 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/ptpython/repl.py b/ptpython/repl.py index ea2d84f0..6b60018e 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -20,7 +20,7 @@ import warnings from dis import COMPILER_FLAG_NAMES from pathlib import Path -from typing import Any, Callable, ContextManager, Iterable, Sequence +from typing import Any, Callable, ContextManager, Iterable, NoReturn, Sequence from prompt_toolkit.formatted_text import OneStyleAndTextTuple from prompt_toolkit.patch_stdout import patch_stdout as patch_stdout_context @@ -40,7 +40,15 @@ except ImportError: PyCF_ALLOW_TOP_LEVEL_AWAIT = 0 -__all__ = ["PythonRepl", "enable_deprecation_warnings", "run_config", "embed"] + +__all__ = [ + "PythonRepl", + "enable_deprecation_warnings", + "run_config", + "embed", + "exit", + "ReplExit", +] def _get_coroutine_flag() -> int | None: @@ -91,9 +99,16 @@ def run_and_show_expression(self, expression: str) -> None: raise except SystemExit: raise + except ReplExit: + raise except BaseException as e: self._handle_exception(e) else: + if isinstance(result, exit): + # When `exit` is evaluated without parentheses. + # Automatically trigger the `ReplExit` exception. + raise ReplExit + # Print. if result is not None: self._show_result(result) @@ -155,7 +170,10 @@ def run(self) -> None: continue # Run it; display the result (or errors if applicable). - self.run_and_show_expression(text) + try: + self.run_and_show_expression(text) + except ReplExit: + return finally: if self.terminal_title: clear_title() @@ -383,6 +401,7 @@ def get_ptpython() -> PythonInput: return self globals["get_ptpython"] = get_ptpython + globals["exit"] = exit() def _remove_from_namespace(self) -> None: """ @@ -459,6 +478,29 @@ def enter_to_continue() -> None: enter_to_continue() +class exit: + """ + Exit the ptpython REPL. + """ + + # This custom exit function ensures that the `embed` function returns from + # where we are embedded, and Python doesn't close `sys.stdin` like + # the default `exit` from `_sitebuiltins.Quitter` does. + + def __call__(self) -> NoReturn: + raise ReplExit + + def __repr__(self) -> str: + # (Same message as the built-in Python REPL.) + return "Use exit() or Ctrl-D (i.e. EOF) to exit" + + +class ReplExit(Exception): + """ + Exception raised by ptpython's exit function. + """ + + def embed( globals: dict[str, Any] | None = None, locals: dict[str, Any] | None = None, From cd54c27a6205226bdb00c5c44f045c32d9547acd Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Mon, 22 Jul 2024 09:28:07 +0000 Subject: [PATCH 442/470] Fix GitHub actions workflow. Use 'ruff check' instead of 'ruff'. --- .github/workflows/test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 9a50f3bc..c62bdc39 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -27,7 +27,7 @@ jobs: - name: Type Checker run: | mypy ptpython - ruff . + ruff check . ruff format --check . - name: Run Tests run: | From 79cb14b1982fee48b86e1b0fdee8f70d8f849d56 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Mon, 22 Jul 2024 09:36:04 +0000 Subject: [PATCH 443/470] Release 3.0.28 --- CHANGELOG | 13 +++++++++++++ setup.py | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 6f2bbb9a..999f13d6 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,19 @@ CHANGELOG ========= +3.0.28: 2024-07-22 +------------------ + +New features: +- Custom 'exit' function to return from REPL that + * doesn't terminate `sys.stdin` when `exit` is called (important for + `embed()`). + * doesn't require to be called with parentheses. + +Fixes: +- Clean up signatures on control-c. + + 3.0.27: 2024-05-27 ------------------ diff --git a/setup.py b/setup.py index 84f18be2..8e84906e 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ setup( name="ptpython", author="Jonathan Slenders", - version="3.0.27", + version="3.0.28", url="https://github.com/prompt-toolkit/ptpython", description="Python REPL build on top of prompt_toolkit", long_description=long_description, From 3e7f68ee48995de1d89e1d4c6ba255bdd1bc7ff2 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Mon, 22 Jul 2024 12:26:12 +0000 Subject: [PATCH 444/470] Improve dictionary completion performance. This improves the performance for dictionary-like objects where iterating over the keys is fast, but doing a lookup for the values is slow. This change ensures we only do value lookups when really needed. The change also caches the meta text so that we don't have to recompute it during navigation of the completion menu. --- ptpython/completer.py | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/ptpython/completer.py b/ptpython/completer.py index 264918e8..e8bab285 100644 --- a/ptpython/completer.py +++ b/ptpython/completer.py @@ -476,20 +476,34 @@ def _get_item_lookup_completions( Complete dictionary keys. """ - def meta_repr(value: object) -> Callable[[], str]: + def meta_repr(obj: object, key: object) -> Callable[[], str]: "Abbreviate meta text, make sure it fits on one line." + cached_result: str | None = None # We return a function, so that it gets computed when it's needed. # When there are many completions, that improves the performance # quite a bit (for the multi-column completion menu, we only need # to display one meta text). + # Note that we also do the lookup itself in here (`obj[key]`), + # because this part can also be slow for some mapping + # implementations. def get_value_repr() -> str: - text = self._do_repr(value) + nonlocal cached_result + if cached_result is not None: + return cached_result + + try: + value = obj[key] # type: ignore + + text = self._do_repr(value) + except BaseException: + return "-" # Take first line, if multiple lines. if "\n" in text: text = text.split("\n", 1)[0] + "..." + cached_result = text return text return get_value_repr @@ -504,24 +518,24 @@ def get_value_repr() -> str: # If this object is a dictionary, complete the keys. if isinstance(result, (dict, collections_abc.Mapping)): # Try to evaluate the key. - key_obj = key + key_obj_str = str(key) for k in [key, key + '"', key + "'"]: try: - key_obj = ast.literal_eval(k) + key_obj_str = str(ast.literal_eval(k)) except (SyntaxError, ValueError): continue else: break - for k, v in result.items(): - if str(k).startswith(str(key_obj)): + for k in result: + if str(k).startswith(key_obj_str): try: k_repr = self._do_repr(k) yield Completion( k_repr + "]", -len(key), display=f"[{k_repr}]", - display_meta=meta_repr(v), + display_meta=meta_repr(result, k), ) except ReprFailedError: pass @@ -537,7 +551,7 @@ def get_value_repr() -> str: k_repr + "]", -len(key), display=f"[{k_repr}]", - display_meta=meta_repr(result[k]), + display_meta=meta_repr(result, k), ) except KeyError: # `result[k]` lookup failed. Trying to complete From 5021832f76309755097b744f274c4e687a690b85 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Mon, 22 Jul 2024 12:42:43 +0000 Subject: [PATCH 445/470] Release 3.0.29 --- CHANGELOG | 7 +++++++ setup.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 999f13d6..bef7d07f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,13 @@ CHANGELOG ========= +3.0.29: 2024-07-22 +------------------ + +Fixes: +- Further improve performance of dictionary completions. + + 3.0.28: 2024-07-22 ------------------ diff --git a/setup.py b/setup.py index 8e84906e..aa101764 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ setup( name="ptpython", author="Jonathan Slenders", - version="3.0.28", + version="3.0.29", url="https://github.com/prompt-toolkit/ptpython", description="Python REPL build on top of prompt_toolkit", long_description=long_description, From b5d8c28535578eca504572c11a6ff893728ecac0 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 10 Apr 2025 12:53:50 +0000 Subject: [PATCH 446/470] Show exception cause/context when printing an exception. --- ptpython/printer.py | 27 ++++++++++++++++----------- ptpython/repl.py | 4 ++++ 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/ptpython/printer.py b/ptpython/printer.py index 85bd9c88..81ea16f3 100644 --- a/ptpython/printer.py +++ b/ptpython/printer.py @@ -254,8 +254,7 @@ def _apply_soft_wrapping( columns_in_buffer += width current_line.append((style, c)) - if len(current_line) > 0: - yield current_line + yield current_line def _print_paginated_formatted_text( self, lines: Iterable[StyleAndTextTuples] @@ -323,14 +322,20 @@ def show_pager() -> None: def _format_exception_output( self, e: BaseException, highlight: bool ) -> Generator[OneStyleAndTextTuple, None, None]: - # Instead of just calling ``traceback.format_exc``, we take the - # traceback and skip the bottom calls of this framework. - t, v, tb = sys.exc_info() - - # Required for pdb.post_mortem() to work. - sys.last_type, sys.last_value, sys.last_traceback = t, v, tb - - tblist = list(traceback.extract_tb(tb)) + if e.__cause__: + yield from self._format_exception_output(e.__cause__, highlight=highlight) + yield ( + "", + "\nThe above exception was the direct cause of the following exception:\n\n", + ) + elif e.__context__: + yield from self._format_exception_output(e.__context__, highlight=highlight) + yield ( + "", + "\nDuring handling of the above exception, another exception occurred:\n\n", + ) + + tblist = list(traceback.extract_tb(e.__traceback__)) for line_nr, tb_tuple in enumerate(tblist): if tb_tuple[0] == "": @@ -340,7 +345,7 @@ def _format_exception_output( tb_list = traceback.format_list(tblist) if tb_list: tb_list.insert(0, "Traceback (most recent call last):\n") - tb_list.extend(traceback.format_exception_only(t, v)) + tb_list.extend(traceback.format_exception_only(type(e), e)) tb_str = "".join(tb_list) diff --git a/ptpython/repl.py b/ptpython/repl.py index 6b60018e..9142d909 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -378,6 +378,10 @@ def _compile_with_flags(self, code: str, mode: str) -> Any: ) def _handle_exception(self, e: BaseException) -> None: + # Required for pdb.post_mortem() to work. + t, v, tb = sys.exc_info() + sys.last_type, sys.last_value, sys.last_traceback = t, v, tb + self._get_output_printer().display_exception( e, highlight=self.enable_syntax_highlighting, From 37763164fd444771c9232ed10e1021d34b7a5d20 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 10 Apr 2025 12:58:18 +0000 Subject: [PATCH 447/470] Drop Python 3.8, given it's end of life and no longer supported on GitHub CI. Also some typing fixes. --- .github/workflows/test.yaml | 6 +++--- ptpython/entry_points/run_ptpython.py | 13 ++++++------- setup.py | 5 ++--- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index c62bdc39..2311e02a 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -10,12 +10,12 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.7, 3.8, 3.9, "3.10", "3.11"] + python-version: [3.8, 3.9, "3.10", "3.11", "3.12", "3.13"] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Setup Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install Dependencies diff --git a/ptpython/entry_points/run_ptpython.py b/ptpython/entry_points/run_ptpython.py index 05df9714..d083858d 100644 --- a/ptpython/entry_points/run_ptpython.py +++ b/ptpython/entry_points/run_ptpython.py @@ -30,8 +30,9 @@ import os import pathlib import sys +from importlib import metadata from textwrap import dedent -from typing import IO +from typing import Protocol import appdirs from prompt_toolkit.formatted_text import HTML @@ -39,17 +40,15 @@ from ptpython.repl import PythonRepl, embed, enable_deprecation_warnings, run_config -try: - from importlib import metadata # type: ignore -except ImportError: - import importlib_metadata as metadata # type: ignore +__all__ = ["create_parser", "get_config_and_history_file", "run"] -__all__ = ["create_parser", "get_config_and_history_file", "run"] +class _SupportsWrite(Protocol): + def write(self, s: str, /) -> object: ... class _Parser(argparse.ArgumentParser): - def print_help(self, file: IO[str] | None = None) -> None: + def print_help(self, file: _SupportsWrite | None = None) -> None: super().print_help() print( dedent( diff --git a/setup.py b/setup.py index aa101764..bd2f962a 100644 --- a/setup.py +++ b/setup.py @@ -27,22 +27,21 @@ package_data={"ptpython": ["py.typed"]}, install_requires=[ "appdirs", - "importlib_metadata;python_version<'3.8'", "jedi>=0.16.0", # Use prompt_toolkit 3.0.43, because of `OneStyleAndTextTuple` import. "prompt_toolkit>=3.0.43,<3.1.0", "pygments", ], - python_requires=">=3.7", + python_requires=">=3.8", classifiers=[ "License :: OSI Approved :: BSD License", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python", ], From 04235d791b483af0ad36f578608d06bf4331f825 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 10 Apr 2025 13:36:23 +0000 Subject: [PATCH 448/470] Use f-strings instead of %-style formatting. --- examples/asyncio-python-embed.py | 2 +- examples/asyncio-ssh-python-embed.py | 4 ++-- ptpython/layout.py | 12 +++++------- ptpython/printer.py | 1 - ptpython/repl.py | 2 +- 5 files changed, 9 insertions(+), 12 deletions(-) diff --git a/examples/asyncio-python-embed.py b/examples/asyncio-python-embed.py index 38cc1c20..cb909731 100755 --- a/examples/asyncio-python-embed.py +++ b/examples/asyncio-python-embed.py @@ -25,7 +25,7 @@ async def print_counter() -> None: Coroutine that prints counters and saves it in a global variable. """ while True: - print("Counter: %i" % counter[0]) + print(f"Counter: {counter[0]}") counter[0] += 1 await asyncio.sleep(3) diff --git a/examples/asyncio-ssh-python-embed.py b/examples/asyncio-ssh-python-embed.py index 9bbad86f..bf79df78 100755 --- a/examples/asyncio-ssh-python-embed.py +++ b/examples/asyncio-ssh-python-embed.py @@ -44,8 +44,8 @@ async def main(port: int = 8222) -> None: def create_server() -> MySSHServer: return MySSHServer(lambda: environ) - print("Listening on :%i" % port) - print('To connect, do "ssh localhost -p %i"' % port) + print(f"Listening on: {port}") + print(f'To connect, do "ssh localhost -p {port}"') await asyncssh.create_server( create_server, "", port, server_host_keys=["/etc/ssh/ssh_host_dsa_key"] diff --git a/ptpython/layout.py b/ptpython/layout.py index 622df594..9768598e 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -108,7 +108,7 @@ def append_category(category: OptionCategory[Any]) -> None: tokens.extend( [ ("class:sidebar", " "), - ("class:sidebar.title", " %-36s" % category.title), + ("class:sidebar.title", f" {category.title:36}"), ("class:sidebar", "\n"), ] ) @@ -130,7 +130,7 @@ def goto_next(mouse_event: MouseEvent) -> None: sel = ",selected" if selected else "" tokens.append(("class:sidebar" + sel, " >" if selected else " ")) - tokens.append(("class:sidebar.label" + sel, "%-24s" % label, select_item)) + tokens.append(("class:sidebar.label" + sel, f"{label:24}", select_item)) tokens.append(("class:sidebar.status" + sel, " ", select_item)) tokens.append(("class:sidebar.status" + sel, f"{status}", goto_next)) @@ -332,7 +332,7 @@ def get_continuation( width: int, line_number: int, is_soft_wrap: bool ) -> StyleAndTextTuples: if python_input.show_line_numbers and not is_soft_wrap: - text = ("%i " % (line_number + 1)).rjust(width) + text = f"{line_number + 1} ".rjust(width) return [("class:line-number", text)] else: return to_formatted_text(get_prompt_style().in2_prompt(width)) @@ -368,8 +368,7 @@ def get_text_fragments() -> StyleAndTextTuples: append( ( TB, - "%i/%i " - % (python_buffer.working_index + 1, len(python_buffer._working_lines)), + f"{python_buffer.working_index + 1}/{len(python_buffer._working_lines)} ", ) ) @@ -492,8 +491,7 @@ def toggle_sidebar(mouse_event: MouseEvent) -> None: ("class:status-toolbar", " - "), ( "class:status-toolbar.python-version", - "%s %i.%i.%i" - % (platform.python_implementation(), version[0], version[1], version[2]), + f"{platform.python_implementation()} {version[0]}.{version[1]}.{version[2]}", ), ("class:status-toolbar", " "), ] diff --git a/ptpython/printer.py b/ptpython/printer.py index 81ea16f3..a3578de7 100644 --- a/ptpython/printer.py +++ b/ptpython/printer.py @@ -1,6 +1,5 @@ from __future__ import annotations -import sys import traceback from dataclasses import dataclass from enum import Enum diff --git a/ptpython/repl.py b/ptpython/repl.py index 9142d909..ba6717fb 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -362,7 +362,7 @@ async def eval_async(self, line: str) -> object: def _store_eval_result(self, result: object) -> None: locals: dict[str, Any] = self.get_locals() - locals["_"] = locals["_%i" % self.current_statement_index] = result + locals["_"] = locals[f"_{self.current_statement_index}"] = result def get_compiler_flags(self) -> int: return super().get_compiler_flags() | PyCF_ALLOW_TOP_LEVEL_AWAIT From ce3a9e2f5495a7ae5146942e468e3565cbe3a87c Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 10 Apr 2025 13:51:45 +0000 Subject: [PATCH 449/470] Use uv in github actions. --- .github/workflows/test.yaml | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 2311e02a..c9fb0ae8 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -10,20 +10,18 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.8, 3.9, "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v4 - - name: Setup Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + + - uses: astral-sh/setup-uv@v5 with: python-version: ${{ matrix.python-version }} - name: Install Dependencies run: | - sudo apt remove python3-pip - python -m pip install --upgrade pip - python -m pip install . ruff mypy pytest readme_renderer - pip list + uv pip install . ruff mypy pytest readme_renderer + uv pip list - name: Type Checker run: | mypy ptpython From 1f1eb1796a67699bbc2bba21129aaf1e6dab978b Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 10 Apr 2025 14:00:42 +0000 Subject: [PATCH 450/470] Reworked dummy test directory. --- .github/workflows/test.yaml | 2 +- tests/run_tests.py | 24 ------------------------ tests/test_dummy.py | 31 +++++++++++++++++++++++++++++++ 3 files changed, 32 insertions(+), 25 deletions(-) delete mode 100755 tests/run_tests.py create mode 100755 tests/test_dummy.py diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index c9fb0ae8..3f527abe 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -29,7 +29,7 @@ jobs: ruff format --check . - name: Run Tests run: | - ./tests/run_tests.py + pytest tests/ - name: Validate README.md # Ensure that the README renders correctly (required for uploading to PyPI). run: | diff --git a/tests/run_tests.py b/tests/run_tests.py deleted file mode 100755 index 0de37430..00000000 --- a/tests/run_tests.py +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env python -from __future__ import annotations - -import unittest - -import ptpython.completer -import ptpython.eventloop -import ptpython.filters -import ptpython.history_browser -import ptpython.key_bindings -import ptpython.layout -import ptpython.python_input -import ptpython.repl -import ptpython.style -import ptpython.utils -import ptpython.validator - -# For now there are no tests here. -# However this is sufficient for Travis to do at least a syntax check. -# That way we are at least sure to restrict to the Python 2.6 syntax. - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_dummy.py b/tests/test_dummy.py new file mode 100755 index 00000000..922c6a39 --- /dev/null +++ b/tests/test_dummy.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python +from __future__ import annotations + +import ptpython.completer +import ptpython.eventloop +import ptpython.filters +import ptpython.history_browser +import ptpython.key_bindings +import ptpython.layout +import ptpython.python_input +import ptpython.repl +import ptpython.style +import ptpython.utils +import ptpython.validator + +# For now there are no tests here. +# However this is sufficient to do at least a syntax check. + + +def test_dummy() -> None: + assert ptpython.completer + assert ptpython.eventloop + assert ptpython.filters + assert ptpython.history_browser + assert ptpython.key_bindings + assert ptpython.layout + assert ptpython.python_input + assert ptpython.repl + assert ptpython.style + assert ptpython.utils + assert ptpython.validator From f1dea7efe97426eec9e7218a0fdc0e17bc47aca8 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 10 Apr 2025 20:35:59 +0000 Subject: [PATCH 451/470] Use pyproject.toml instead of setup.py Cherry-picked from: https://github.com/prompt-toolkit/ptpython/pull/599 Thanks to: Branch Vincent --- pyproject.toml | 58 +++++++++++++++++++++++++++++++++++++++++++- setup.cfg | 41 ------------------------------- setup.py | 66 -------------------------------------------------- 3 files changed, 57 insertions(+), 108 deletions(-) delete mode 100644 setup.cfg delete mode 100644 setup.py diff --git a/pyproject.toml b/pyproject.toml index ce420372..3780f9d6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,55 @@ +[project] +name = "ptpython" +version = "3.0.29" +description = "Python REPL build on top of prompt_toolkit" +readme = "README.rst" +authors = [{ name = "Jonathan Slenders" }] +classifiers = [ + "License :: OSI Approved :: BSD License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python", +] +requires-python = ">=3.8" +dependencies = [ + "appdirs", + "jedi>=0.16.0", + # Use prompt_toolkit 3.0.43, because of `OneStyleAndTextTuple` import. + "prompt_toolkit>=3.0.43,<3.1.0", + "pygments", +] + + +[project.urls] +Homepage = "https://github.com/prompt-toolkit/ptpython" +Changelog = "https://github.com/prompt-toolkit/ptpython/blob/master/CHANGELOG" +"Bug Tracker" = "https://github.com/prompt-toolkit/ptpython/issues" +"Source Code" = "https://github.com/prompt-toolkit/ptpython" + + +[project.scripts] +ptpython = "ptpython.entry_points.run_ptpython:run" +ptipython = "ptpython.entry_points.run_ptipython:run" + + +[project.optional-dependencies] +ptipython = ["ipython"] # For ptipython, we need to have IPython + + +[tool.mypy] +ignore_missing_imports = true +no_implicit_optional = true +platform = "win32" +strict_equality = true +strict_optional = true + + [tool.ruff] target-version = "py37" lint.select = [ @@ -27,9 +79,13 @@ lint.ignore = [ "ptpython/ipython.py" = ["T100"] # Import usage. "ptpython/repl.py" = ["T201"] # Print usage. "ptpython/printer.py" = ["T201"] # Print usage. -"tests/run_tests.py" = ["F401"] # Unused imports. [tool.ruff.lint.isort] known-first-party = ["ptpython"] known-third-party = ["prompt_toolkit", "pygments", "asyncssh"] + + +[build-system] +requires = ["setuptools>=68"] +build-backend = "setuptools.build_meta" diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 80dfec6a..00000000 --- a/setup.cfg +++ /dev/null @@ -1,41 +0,0 @@ -[bdist_wheel] -universal=1 - -[flake8] -exclude=__init__.py -max_line_length=150 -ignore= - E114, - E116, - E117, - E121, - E122, - E123, - E125, - E126, - E127, - E128, - E131, - E171, - E203, - E211, - E221, - E227, - E231, - E241, - E251, - E301, - E402, - E501, - E701, - E702, - E704, - E731, - E741, - F401, - F403, - F405, - F811, - W503, - W504, - E722 diff --git a/setup.py b/setup.py deleted file mode 100644 index bd2f962a..00000000 --- a/setup.py +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env python -import os -import sys - -from setuptools import find_packages, setup - -with open(os.path.join(os.path.dirname(__file__), "README.rst")) as f: - long_description = f.read() - - -setup( - name="ptpython", - author="Jonathan Slenders", - version="3.0.29", - url="https://github.com/prompt-toolkit/ptpython", - description="Python REPL build on top of prompt_toolkit", - long_description=long_description, - package_urls={ - "Changelog": "https://github.com/prompt-toolkit/ptpython/blob/master/CHANGELOG", - }, - project_urls={ - "Bug Tracker": "https://github.com/prompt-toolkit/ptpython/issues", - "Source Code": "https://github.com/prompt-toolkit/ptpython", - "Changelog": "https://github.com/prompt-toolkit/ptpython/blob/master/CHANGELOG", - }, - packages=find_packages("."), - package_data={"ptpython": ["py.typed"]}, - install_requires=[ - "appdirs", - "jedi>=0.16.0", - # Use prompt_toolkit 3.0.43, because of `OneStyleAndTextTuple` import. - "prompt_toolkit>=3.0.43,<3.1.0", - "pygments", - ], - python_requires=">=3.8", - classifiers=[ - "License :: OSI Approved :: BSD License", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python", - ], - entry_points={ - "console_scripts": [ - "ptpython = ptpython.entry_points.run_ptpython:run", - "ptipython = ptpython.entry_points.run_ptipython:run", - f"ptpython{sys.version_info[0]} = ptpython.entry_points.run_ptpython:run", - "ptpython{}.{} = ptpython.entry_points.run_ptpython:run".format( - *sys.version_info[:2] - ), - f"ptipython{sys.version_info[0]} = ptpython.entry_points.run_ptipython:run", - "ptipython{}.{} = ptpython.entry_points.run_ptipython:run".format( - *sys.version_info[:2] - ), - ] - }, - extras_require={ - "ptipython": ["ipython"], # For ptipython, we need to have IPython - "all": ["black"], # Black not always possible on PyPy - }, -) From acf61459a7b203815a738cf6dc5ec20288e3ce19 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 10 Apr 2025 20:48:11 +0000 Subject: [PATCH 452/470] Use uvx in GitHub workflows. --- .github/workflows/test.yaml | 18 ++++++++---------- ptpython/history_browser.py | 4 +++- ptpython/key_bindings.py | 4 +++- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 3f527abe..74c3c7b8 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -14,23 +14,21 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v5 with: python-version: ${{ matrix.python-version }} - - name: Install Dependencies + - name: Type Checking run: | - uv pip install . ruff mypy pytest readme_renderer - uv pip list - - name: Type Checker + uvx --with . mypy ptpython + - name: Code formatting run: | - mypy ptpython - ruff check . - ruff format --check . - - name: Run Tests + uvx ruff check . + uvx ruff format --check . + - name: Unit test run: | - pytest tests/ + uvx --with . pytest tests/ - name: Validate README.md # Ensure that the README renders correctly (required for uploading to PyPI). run: | + uv pip install readme_renderer python -m readme_renderer README.rst > /dev/null diff --git a/ptpython/history_browser.py b/ptpython/history_browser.py index ae0ac03e..72bc576d 100644 --- a/ptpython/history_browser.py +++ b/ptpython/history_browser.py @@ -58,13 +58,15 @@ from .utils import if_mousedown if TYPE_CHECKING: + from typing_extensions import TypeAlias + from .python_input import PythonInput HISTORY_COUNT = 2000 __all__ = ["HistoryLayout", "PythonHistory"] -E = KeyPressEvent +E: TypeAlias = KeyPressEvent HELP_TEXT = """ This interface is meant to select multiple lines from the diff --git a/ptpython/key_bindings.py b/ptpython/key_bindings.py index d7bb575e..48c5f5ae 100644 --- a/ptpython/key_bindings.py +++ b/ptpython/key_bindings.py @@ -22,6 +22,8 @@ from .utils import document_is_multiline_python if TYPE_CHECKING: + from typing_extensions import TypeAlias + from .python_input import PythonInput __all__ = [ @@ -30,7 +32,7 @@ "load_confirm_exit_bindings", ] -E = KeyPressEvent +E: TypeAlias = KeyPressEvent @Condition From 39b1cbda27e7b579e7b470311d409924457e072b Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 10 Apr 2025 20:59:43 +0000 Subject: [PATCH 453/470] Remove mypy.ini --- mypy.ini | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 mypy.ini diff --git a/mypy.ini b/mypy.ini deleted file mode 100644 index 5a7ef2eb..00000000 --- a/mypy.ini +++ /dev/null @@ -1,6 +0,0 @@ -[mypy] -ignore_missing_imports = True -no_implicit_optional = True -platform = win32 -strict_equality = True -strict_optional = True From 1527d0527625a2c72b154a6cb937f0e4dec9a87a Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 10 Apr 2025 21:07:52 +0000 Subject: [PATCH 454/470] use src/ directory for source code. --- .github/workflows/test.yaml | 2 +- pyproject.toml | 10 +++++----- {ptpython => src/ptpython}/__init__.py | 0 {ptpython => src/ptpython}/__main__.py | 0 {ptpython => src/ptpython}/completer.py | 0 {ptpython => src/ptpython}/contrib/__init__.py | 0 {ptpython => src/ptpython}/contrib/asyncssh_repl.py | 0 {ptpython => src/ptpython}/entry_points/__init__.py | 0 .../ptpython}/entry_points/run_ptipython.py | 0 .../ptpython}/entry_points/run_ptpython.py | 0 {ptpython => src/ptpython}/eventloop.py | 0 {ptpython => src/ptpython}/filters.py | 0 {ptpython => src/ptpython}/history_browser.py | 0 {ptpython => src/ptpython}/ipython.py | 0 {ptpython => src/ptpython}/key_bindings.py | 0 {ptpython => src/ptpython}/layout.py | 0 {ptpython => src/ptpython}/lexer.py | 0 {ptpython => src/ptpython}/printer.py | 0 {ptpython => src/ptpython}/prompt_style.py | 0 {ptpython => src/ptpython}/py.typed | 0 {ptpython => src/ptpython}/python_input.py | 0 {ptpython => src/ptpython}/repl.py | 0 {ptpython => src/ptpython}/signatures.py | 0 {ptpython => src/ptpython}/style.py | 0 {ptpython => src/ptpython}/utils.py | 0 {ptpython => src/ptpython}/validator.py | 0 26 files changed, 6 insertions(+), 6 deletions(-) rename {ptpython => src/ptpython}/__init__.py (100%) rename {ptpython => src/ptpython}/__main__.py (100%) rename {ptpython => src/ptpython}/completer.py (100%) rename {ptpython => src/ptpython}/contrib/__init__.py (100%) rename {ptpython => src/ptpython}/contrib/asyncssh_repl.py (100%) rename {ptpython => src/ptpython}/entry_points/__init__.py (100%) rename {ptpython => src/ptpython}/entry_points/run_ptipython.py (100%) rename {ptpython => src/ptpython}/entry_points/run_ptpython.py (100%) rename {ptpython => src/ptpython}/eventloop.py (100%) rename {ptpython => src/ptpython}/filters.py (100%) rename {ptpython => src/ptpython}/history_browser.py (100%) rename {ptpython => src/ptpython}/ipython.py (100%) rename {ptpython => src/ptpython}/key_bindings.py (100%) rename {ptpython => src/ptpython}/layout.py (100%) rename {ptpython => src/ptpython}/lexer.py (100%) rename {ptpython => src/ptpython}/printer.py (100%) rename {ptpython => src/ptpython}/prompt_style.py (100%) rename {ptpython => src/ptpython}/py.typed (100%) rename {ptpython => src/ptpython}/python_input.py (100%) rename {ptpython => src/ptpython}/repl.py (100%) rename {ptpython => src/ptpython}/signatures.py (100%) rename {ptpython => src/ptpython}/style.py (100%) rename {ptpython => src/ptpython}/utils.py (100%) rename {ptpython => src/ptpython}/validator.py (100%) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 74c3c7b8..457a4e48 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -19,7 +19,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Type Checking run: | - uvx --with . mypy ptpython + uvx --with . mypy src/ptpython - name: Code formatting run: | uvx ruff check . diff --git a/pyproject.toml b/pyproject.toml index 3780f9d6..680d7087 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -74,11 +74,11 @@ lint.ignore = [ [tool.ruff.lint.per-file-ignores] "examples/*" = ["T201"] # Print allowed in examples. "examples/ptpython_config/config.py" = ["F401"] # Unused imports in config. -"ptpython/entry_points/run_ptipython.py" = ["T201", "F401"] # Print, import usage. -"ptpython/entry_points/run_ptpython.py" = ["T201"] # Print usage. -"ptpython/ipython.py" = ["T100"] # Import usage. -"ptpython/repl.py" = ["T201"] # Print usage. -"ptpython/printer.py" = ["T201"] # Print usage. +"src/ptpython/entry_points/run_ptipython.py" = ["T201", "F401"] # Print, import usage. +"src/ptpython/entry_points/run_ptpython.py" = ["T201"] # Print usage. +"src/ptpython/ipython.py" = ["T100"] # Import usage. +"src/ptpython/repl.py" = ["T201"] # Print usage. +"src/ptpython/printer.py" = ["T201"] # Print usage. [tool.ruff.lint.isort] diff --git a/ptpython/__init__.py b/src/ptpython/__init__.py similarity index 100% rename from ptpython/__init__.py rename to src/ptpython/__init__.py diff --git a/ptpython/__main__.py b/src/ptpython/__main__.py similarity index 100% rename from ptpython/__main__.py rename to src/ptpython/__main__.py diff --git a/ptpython/completer.py b/src/ptpython/completer.py similarity index 100% rename from ptpython/completer.py rename to src/ptpython/completer.py diff --git a/ptpython/contrib/__init__.py b/src/ptpython/contrib/__init__.py similarity index 100% rename from ptpython/contrib/__init__.py rename to src/ptpython/contrib/__init__.py diff --git a/ptpython/contrib/asyncssh_repl.py b/src/ptpython/contrib/asyncssh_repl.py similarity index 100% rename from ptpython/contrib/asyncssh_repl.py rename to src/ptpython/contrib/asyncssh_repl.py diff --git a/ptpython/entry_points/__init__.py b/src/ptpython/entry_points/__init__.py similarity index 100% rename from ptpython/entry_points/__init__.py rename to src/ptpython/entry_points/__init__.py diff --git a/ptpython/entry_points/run_ptipython.py b/src/ptpython/entry_points/run_ptipython.py similarity index 100% rename from ptpython/entry_points/run_ptipython.py rename to src/ptpython/entry_points/run_ptipython.py diff --git a/ptpython/entry_points/run_ptpython.py b/src/ptpython/entry_points/run_ptpython.py similarity index 100% rename from ptpython/entry_points/run_ptpython.py rename to src/ptpython/entry_points/run_ptpython.py diff --git a/ptpython/eventloop.py b/src/ptpython/eventloop.py similarity index 100% rename from ptpython/eventloop.py rename to src/ptpython/eventloop.py diff --git a/ptpython/filters.py b/src/ptpython/filters.py similarity index 100% rename from ptpython/filters.py rename to src/ptpython/filters.py diff --git a/ptpython/history_browser.py b/src/ptpython/history_browser.py similarity index 100% rename from ptpython/history_browser.py rename to src/ptpython/history_browser.py diff --git a/ptpython/ipython.py b/src/ptpython/ipython.py similarity index 100% rename from ptpython/ipython.py rename to src/ptpython/ipython.py diff --git a/ptpython/key_bindings.py b/src/ptpython/key_bindings.py similarity index 100% rename from ptpython/key_bindings.py rename to src/ptpython/key_bindings.py diff --git a/ptpython/layout.py b/src/ptpython/layout.py similarity index 100% rename from ptpython/layout.py rename to src/ptpython/layout.py diff --git a/ptpython/lexer.py b/src/ptpython/lexer.py similarity index 100% rename from ptpython/lexer.py rename to src/ptpython/lexer.py diff --git a/ptpython/printer.py b/src/ptpython/printer.py similarity index 100% rename from ptpython/printer.py rename to src/ptpython/printer.py diff --git a/ptpython/prompt_style.py b/src/ptpython/prompt_style.py similarity index 100% rename from ptpython/prompt_style.py rename to src/ptpython/prompt_style.py diff --git a/ptpython/py.typed b/src/ptpython/py.typed similarity index 100% rename from ptpython/py.typed rename to src/ptpython/py.typed diff --git a/ptpython/python_input.py b/src/ptpython/python_input.py similarity index 100% rename from ptpython/python_input.py rename to src/ptpython/python_input.py diff --git a/ptpython/repl.py b/src/ptpython/repl.py similarity index 100% rename from ptpython/repl.py rename to src/ptpython/repl.py diff --git a/ptpython/signatures.py b/src/ptpython/signatures.py similarity index 100% rename from ptpython/signatures.py rename to src/ptpython/signatures.py diff --git a/ptpython/style.py b/src/ptpython/style.py similarity index 100% rename from ptpython/style.py rename to src/ptpython/style.py diff --git a/ptpython/utils.py b/src/ptpython/utils.py similarity index 100% rename from ptpython/utils.py rename to src/ptpython/utils.py diff --git a/ptpython/validator.py b/src/ptpython/validator.py similarity index 100% rename from ptpython/validator.py rename to src/ptpython/validator.py From 030790f8fb8da7736cc91a76712c99f230d1ebe1 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 10 Apr 2025 22:02:03 +0000 Subject: [PATCH 455/470] Add typos to workflow. --- .github/workflows/test.yaml | 6 ++++++ pyproject.toml | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 457a4e48..6d2877b3 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -21,13 +21,19 @@ jobs: run: | uvx --with . mypy src/ptpython - name: Code formatting + if: ${{ matrix.python-version == '3.13' }} run: | uvx ruff check . uvx ruff format --check . + - name: Typos + if: ${{ matrix.python-version == '3.13' }} + run: | + uvx typos . - name: Unit test run: | uvx --with . pytest tests/ - name: Validate README.md + if: ${{ matrix.python-version == '3.13' }} # Ensure that the README renders correctly (required for uploading to PyPI). run: | uv pip install readme_renderer diff --git a/pyproject.toml b/pyproject.toml index 680d7087..72259863 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -85,6 +85,10 @@ lint.ignore = [ known-first-party = ["ptpython"] known-third-party = ["prompt_toolkit", "pygments", "asyncssh"] +[tool.typos.default] +extend-ignore-re = [ + "impotr" # Intentional typo in: ./examples/ptpython_config/config.py +] [build-system] requires = ["setuptools>=68"] From fb4949ad52ce7d603ab5bb52fba572c6dfdaad0b Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 10 Apr 2025 22:21:44 +0000 Subject: [PATCH 456/470] Typecheck examples. --- .github/workflows/test.yaml | 3 ++- examples/ssh-and-telnet-embed.py | 6 +++-- src/ptpython/repl.py | 43 ++++++++++++++++++++++++++++++-- 3 files changed, 47 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 6d2877b3..d53bfcc1 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -19,7 +19,8 @@ jobs: python-version: ${{ matrix.python-version }} - name: Type Checking run: | - uvx --with . mypy src/ptpython + uvx --with . mypy src/ptpython/ + uvx --with . mypy examples/ - name: Code formatting if: ${{ matrix.python-version == '3.13' }} run: | diff --git a/examples/ssh-and-telnet-embed.py b/examples/ssh-and-telnet-embed.py index 62fa76d9..2b293e6f 100755 --- a/examples/ssh-and-telnet-embed.py +++ b/examples/ssh-and-telnet-embed.py @@ -6,6 +6,8 @@ https://gist.github.com/vxgmichel/7685685b3e5ead04ada4a3ba75a48eef """ +from __future__ import annotations + import asyncio import pathlib @@ -15,7 +17,7 @@ PromptToolkitSSHServer, PromptToolkitSSHSession, ) -from prompt_toolkit.contrib.telnet.server import TelnetServer +from prompt_toolkit.contrib.telnet.server import TelnetConnection, TelnetServer from ptpython.repl import embed @@ -28,7 +30,7 @@ def ensure_key(filename: str = "ssh_host_key") -> str: return str(path) -async def interact(connection: PromptToolkitSSHSession) -> None: +async def interact(connection: PromptToolkitSSHSession | TelnetConnection) -> None: global_dict = {**globals(), "print": print_formatted_text} await embed(return_asyncio_coroutine=True, globals=global_dict) diff --git a/src/ptpython/repl.py b/src/ptpython/repl.py index ba6717fb..469ed694 100644 --- a/src/ptpython/repl.py +++ b/src/ptpython/repl.py @@ -20,7 +20,17 @@ import warnings from dis import COMPILER_FLAG_NAMES from pathlib import Path -from typing import Any, Callable, ContextManager, Iterable, NoReturn, Sequence +from typing import ( + Any, + Callable, + ContextManager, + Coroutine, + Iterable, + Literal, + NoReturn, + Sequence, + overload, +) from prompt_toolkit.formatted_text import OneStyleAndTextTuple from prompt_toolkit.patch_stdout import patch_stdout as patch_stdout_context @@ -505,6 +515,34 @@ class ReplExit(Exception): """ +@overload +def embed( + globals: dict[str, Any] | None = ..., + locals: dict[str, Any] | None = ..., + configure: Callable[[PythonRepl], None] | None = ..., + vi_mode: bool = ..., + history_filename: str | None = ..., + title: str | None = ..., + startup_paths: Sequence[str | Path] | None = ..., + patch_stdout: bool = ..., + return_asyncio_coroutine: Literal[False] = ..., +) -> None: ... + + +@overload +def embed( + globals: dict[str, Any] | None = ..., + locals: dict[str, Any] | None = ..., + configure: Callable[[PythonRepl], None] | None = ..., + vi_mode: bool = ..., + history_filename: str | None = ..., + title: str | None = ..., + startup_paths: Sequence[str | Path] | None = ..., + patch_stdout: bool = ..., + return_asyncio_coroutine: Literal[True] = ..., +) -> Coroutine[Any, Any, None]: ... + + def embed( globals: dict[str, Any] | None = None, locals: dict[str, Any] | None = None, @@ -515,7 +553,7 @@ def embed( startup_paths: Sequence[str | Path] | None = None, patch_stdout: bool = False, return_asyncio_coroutine: bool = False, -) -> None: +) -> None | Coroutine[Any, Any, None]: """ Call this to embed Python shell at the current point in your program. It's similar to `IPython.embed` and `bpython.embed`. :: @@ -577,3 +615,4 @@ async def coroutine() -> None: else: with patch_context: repl.run() + return None From 836431ff6775aac2c2e3aafa3295b259ebe99d0a Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 15 Apr 2025 09:24:02 +0000 Subject: [PATCH 457/470] Release 3.0.30 --- CHANGELOG | 11 +++++++++++ pyproject.toml | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index bef7d07f..7706260d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,17 @@ CHANGELOG ========= +3.0.30: 2025-04-15 +------------------ + +New features: +- Show exception cause/context when printing chained exceptions. +- Reworked project layout and use pyproject.toml instead of setup.py. + +Breaking changes: +- Drop Python 3.7 support. + + 3.0.29: 2024-07-22 ------------------ diff --git a/pyproject.toml b/pyproject.toml index 72259863..00e2d5f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ptpython" -version = "3.0.29" +version = "3.0.30" description = "Python REPL build on top of prompt_toolkit" readme = "README.rst" authors = [{ name = "Jonathan Slenders" }] From c22b59d21147d36560a2ff802c8ba1876797e208 Mon Sep 17 00:00:00 2001 From: PEMessage <1165739182@qq.com> Date: Thu, 26 Jun 2025 00:20:27 +0800 Subject: [PATCH 458/470] Fix repl.min_brightness and repl.max_brightness config not work in history --- src/ptpython/history_browser.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ptpython/history_browser.py b/src/ptpython/history_browser.py index 72bc576d..101a6c5c 100644 --- a/src/ptpython/history_browser.py +++ b/src/ptpython/history_browser.py @@ -643,6 +643,7 @@ def accept_handler(buffer: Buffer) -> bool: layout=self.history_layout.layout, full_screen=True, style=python_input._current_style, + style_transformation=python_input.style_transformation, mouse_support=Condition(lambda: python_input.enable_mouse_support), key_bindings=create_key_bindings(self, python_input, history_mapping), ) From 3767d50ff330f7847cc1695ab41de6b80c444fff Mon Sep 17 00:00:00 2001 From: Shengchen Zhang Date: Fri, 27 Jun 2025 20:47:18 +0800 Subject: [PATCH 459/470] Expose `raw` parameter from prompt-toolkit to allow escape sequences to print normally. --- src/ptpython/repl.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/ptpython/repl.py b/src/ptpython/repl.py index 469ed694..90772689 100644 --- a/src/ptpython/repl.py +++ b/src/ptpython/repl.py @@ -525,6 +525,7 @@ def embed( title: str | None = ..., startup_paths: Sequence[str | Path] | None = ..., patch_stdout: bool = ..., + patch_stdout_raw: bool = ..., return_asyncio_coroutine: Literal[False] = ..., ) -> None: ... @@ -539,6 +540,7 @@ def embed( title: str | None = ..., startup_paths: Sequence[str | Path] | None = ..., patch_stdout: bool = ..., + patch_stdout_raw: bool = ..., return_asyncio_coroutine: Literal[True] = ..., ) -> Coroutine[Any, Any, None]: ... @@ -552,6 +554,7 @@ def embed( title: str | None = None, startup_paths: Sequence[str | Path] | None = None, patch_stdout: bool = False, + patch_stdout_raw: bool = False, return_asyncio_coroutine: bool = False, ) -> None | Coroutine[Any, Any, None]: """ @@ -567,6 +570,7 @@ def embed( :param title: Title to be displayed in the terminal titlebar. (None or string.) :param patch_stdout: When true, patch `sys.stdout` so that background threads that are printing will print nicely above the prompt. + :param patch_stdout_raw: When true, patch_stdout will not escape/remove vt100 terminal escape sequences. """ # Default globals/locals if globals is None: @@ -602,7 +606,7 @@ def get_locals() -> dict[str, Any]: # Start repl. patch_context: ContextManager[None] = ( - patch_stdout_context() if patch_stdout else DummyContext() + patch_stdout_context(raw=patch_stdout_raw) if patch_stdout else DummyContext() ) if return_asyncio_coroutine: From a14c329147f498fd0ce28240a16de8620d454180 Mon Sep 17 00:00:00 2001 From: Shengchen Zhang Date: Tue, 8 Jul 2025 16:44:27 +0100 Subject: [PATCH 460/470] Update dead image links in README.rst --- README.rst | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/README.rst b/README.rst index 06c1e02b..2559a3c4 100644 --- a/README.rst +++ b/README.rst @@ -9,7 +9,7 @@ ptpython pip install ptpython -.. image :: https://github.com/jonathanslenders/ptpython/raw/master/docs/images/example1.png +.. image :: ./docs/images/example1.png Ptpython is an advanced Python REPL. It should work on all Python versions from 2.6 up to 3.11 and work cross platform (Linux, @@ -109,15 +109,15 @@ More screenshots The configuration menu: -.. image :: https://github.com/jonathanslenders/ptpython/raw/master/docs/images/ptpython-menu.png +.. image :: ./docs/images/ptpython-menu.png The history page and its help: -.. image :: https://github.com/jonathanslenders/ptpython/raw/master/docs/images/ptpython-history-help.png +.. image :: ./docs/images/ptpython-history-help.png Autocompletion: -.. image :: https://github.com/jonathanslenders/ptpython/raw/master/docs/images/file-completion.png +.. image :: ./docs/images/file-completion.png Embedding the REPL @@ -159,7 +159,7 @@ terminal, you have to check the "Use option as meta key" checkbox in your terminal settings. For iTerm2, you have to check "Left option acts as +Esc" in the options.) -.. image :: https://github.com/jonathanslenders/ptpython/raw/master/docs/images/multiline.png +.. image :: ./docs/images/multiline.png Syntax validation @@ -169,7 +169,7 @@ Before execution, ``ptpython`` will see whether the input is syntactically correct Python code. If not, it will show a warning, and move the cursor to the error. -.. image :: https://github.com/jonathanslenders/ptpython/raw/master/docs/images/validation.png +.. image :: ./docs/images/validation.png Asyncio REPL and top level await @@ -208,7 +208,7 @@ variable, if set, can also be used to explicitly override where configuration is looked for. Have a look at this example to see what is possible: -`config.py `_ +`config.py `_ Note config file support currently only works when invoking `ptpython` directly. That it, the config file will be ignored when embedding ptpython in an application. @@ -222,7 +222,7 @@ with all the power that IPython has to offer, like magic functions and shell integration. Make sure that IPython has been installed. (``pip install ipython``) -.. image :: https://github.com/jonathanslenders/ptpython/raw/master/docs/images/ipython.png +.. image :: ./docs/images/ipython.png This is also available for embedding: @@ -253,7 +253,7 @@ Windows support ``prompt_toolkit`` and ``ptpython`` works better on Linux and OS X than on Windows. Some things might not work, but it is usable: -.. image :: https://github.com/jonathanslenders/ptpython/raw/master/docs/images/windows.png +.. image :: ./docs/images/windows.png Windows terminal integration ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ From c49bf9b05a204fd8ed296698c5a6fe60b712dc1f Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Wed, 27 Aug 2025 11:30:30 +0000 Subject: [PATCH 461/470] Fix positioning of exit confirmation (compatibilitiy with latest prompt_toolkit). --- src/ptpython/layout.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ptpython/layout.py b/src/ptpython/layout.py index 9768598e..8d030dd1 100644 --- a/src/ptpython/layout.py +++ b/src/ptpython/layout.py @@ -694,7 +694,8 @@ def menu_position() -> int | None: ), Float( left=2, - bottom=1, + top=2, + height=3, content=self.exit_confirmation, ), Float( From b3959fe513d8b3fa0306ce0599347b779d736c07 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Wed, 27 Aug 2025 11:36:57 +0000 Subject: [PATCH 462/470] Upgrade ruff check to Python 3.8. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 00e2d5f8..9912c736 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,7 +51,7 @@ strict_optional = true [tool.ruff] -target-version = "py37" +target-version = "py38" lint.select = [ "E", # pycodestyle errors "W", # pycodestyle warnings From 0066c7ee392d46103b8bd2968c7a645a4c04f02d Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Wed, 27 Aug 2025 15:29:12 +0000 Subject: [PATCH 463/470] Release 3.0.31 --- CHANGELOG | 9 +++++++++ pyproject.toml | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 7706260d..8a07ede2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,15 @@ CHANGELOG ========= +3.0.31: 2025-08-27 +------------------ + +Fixes: +- Add `patch_stdout_raw` parameter to `embed()`. +- Fix repl.min_brightness and repl.max_brightness config in history. +- Fix positioning of exit confirmation (compatibilitiy with latest prompt_toolkit). + + 3.0.30: 2025-04-15 ------------------ diff --git a/pyproject.toml b/pyproject.toml index 9912c736..f1f3a853 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ptpython" -version = "3.0.30" +version = "3.0.31" description = "Python REPL build on top of prompt_toolkit" readme = "README.rst" authors = [{ name = "Jonathan Slenders" }] From d29f20d996e301db1ce12cfd77cfa50a9c128155 Mon Sep 17 00:00:00 2001 From: tapple-cisco <167885465+tapple-cisco@users.noreply.github.com> Date: Tue, 16 Sep 2025 07:01:39 -0700 Subject: [PATCH 464/470] Note in config example where the file goes on Windows --- examples/ptpython_config/config.py | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/ptpython_config/config.py b/examples/ptpython_config/config.py index bfd3914e..4efd5d97 100644 --- a/examples/ptpython_config/config.py +++ b/examples/ptpython_config/config.py @@ -4,6 +4,7 @@ Copy this file to $XDG_CONFIG_HOME/ptpython/config.py On Linux, this is: ~/.config/ptpython/config.py On macOS, this is: ~/Library/Application Support/ptpython/config.py +On Windows, this is: ~\AppData\Local\prompt_toolkit\ptpython\config.py """ from prompt_toolkit.filters import ViInsertMode From 86c6d1168a8f020f442309cd5bc3023ed6f0764d Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sat, 8 Nov 2025 22:09:54 +0000 Subject: [PATCH 465/470] Use ANSI colors for the default theme. --- src/ptpython/python_input.py | 2 +- src/ptpython/style.py | 58 ++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/src/ptpython/python_input.py b/src/ptpython/python_input.py index b1773643..7e218eb9 100644 --- a/src/ptpython/python_input.py +++ b/src/ptpython/python_input.py @@ -350,7 +350,7 @@ def __init__( #: Load styles. self.code_styles: dict[str, BaseStyle] = get_all_code_styles() self.ui_styles = get_all_ui_styles() - self._current_code_style_name: str = "default" + self._current_code_style_name: str = "default-ansi" self._current_ui_style_name: str = "default" if is_windows(): diff --git a/src/ptpython/style.py b/src/ptpython/style.py index c5a04e58..85219717 100644 --- a/src/ptpython/style.py +++ b/src/ptpython/style.py @@ -17,6 +17,7 @@ def get_all_code_styles() -> dict[str, BaseStyle]: for name in get_all_styles() } result["win32"] = Style.from_dict(win32_code_style) + result["default-ansi"] = Style.from_dict(default_ansi_code_style) return result @@ -38,6 +39,63 @@ def generate_style(python_style: BaseStyle, ui_style: BaseStyle) -> BaseStyle: return merge_styles([python_style, ui_style]) +# Use ANSI colors for the default theme. +# This is `DefaultStyle` from Pygments, modified to use ANSI colors instead of +# RGB. This adapts better to light/dark mode, because the built-in themes from +# a terminal are typically designed for whatever background is used. All the +# other Pygments themes use RGB, which is fine, because the user consciously +# chooses what works for them. + +# To convert, do: +# from prompt_toolkit.output import ColorDepth +# from prompt_toolkit.output.vt100 import _EscapeCodeCache, _get_closest_ansi_color +# print(_get_closest_ansi_color( +# *_EscapeCodeCache(ColorDepth.DEPTH_8_BIT)._color_name_to_rgb('bbbbbb')) +# ) + +default_ansi_code_style = { + "pygments.whitespace": "ansigray", # "#bbbbbb", + "pygments.comment": "italic ansibrightblack", # "italic #3d7b7b", + "pygments.comment.preproc": "noitalic ansired", # "noitalic #9c6500", + "pygments.keyword": "bold ansigreen", # "bold #008000", + "pygments.keyword.pseudo": "nobold", + "pygments.keyword.type": "nobold ansired", # "nobold #b00040", + "pygments.operator": "ansibrightblack", # "#666666", + "pygments.operator.word": "bold ansimagenta", # "bold #aa22ff", + "pygments.name.builtin": "ansigreen", # "#008000", + "pygments.name.function": "ansibrightblue", # "#0000ff", + "pygments.name.class": "bold ansibrightblue", # "bold #0000ff", + "pygments.name.namespace": "bold ansibrightblack", # "bold #0000ff", + "pygments.name.exception": "bold ansired", # "bold #cb3f38", + "pygments.name.variable": "ansiblue", # "#19177c", + "pygments.name.constant": "ansired", # "#880000", + "pygments.name.label": "ansiyellow", # "#767600", + "pygments.name.entity": "bold ansibrightblack", # "bold #717171", + "pygments.name.attribute": "ansibrightblack", # "#687822", + "pygments.name.tag": "bold ansigreen", # "bold #008000", + "pygments.name.decorator": "ansimagenta", # "#aa22ff", + "pygments.literal.string": "ansired", # "#ba2121", + "pygments.literal.string.doc": "italic", + "pygments.literal.string.interpol": "bold ansibrightblack", # "bold #a45a77", + "pygments.literal.string.escape": "bold ansired", # "bold #aa5d1f", + "pygments.literal.string.regex": "ansibrightblack", # "#a45a77", + "pygments.literal.string.symbol": "ansiblue", # "#19177c", + "pygments.literal.string.other": "ansigreen", # "#008000", + "pygments.literal.number": "ansibrightblack", # "#666666", + "pygments.generic.heading": "bold ansiblue", # "bold #000080", + "pygments.generic.subheading": "bold ansimagenta", # "bold #800080", + "pygments.generic.deleted": "ansired", # "#a00000", + "pygments.generic.inserted": "ansigreen", # "#008400", + "pygments.generic.error": "ansigreen", # "#e40000", + "pygments.generic.emph": "italic", + "pygments.generic.strong": "bold", + "pygments.generic.emphstrong": "bold italic", + "pygments.generic.prompt": "bold ansiblue", # "bold #000080", + "pygments.generic.output": "ansibrightblack", # "#717171", + "pygments.generic.traceback": "ansiblue", # "#04d", + "pygments.error": "", # "border:#ff0000", +} + # Code style for Windows consoles. They support only 16 colors, # so we choose a combination that displays nicely. win32_code_style = { From ed597e1945535e4d84a2c628ba6e470917e883dc Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Mon, 17 Nov 2025 13:59:26 +0000 Subject: [PATCH 466/470] Fix string escaping error in config example. --- examples/ptpython_config/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/ptpython_config/config.py b/examples/ptpython_config/config.py index 4efd5d97..fe8a9ae8 100644 --- a/examples/ptpython_config/config.py +++ b/examples/ptpython_config/config.py @@ -1,4 +1,4 @@ -""" +r""" Configuration example for ``ptpython``. Copy this file to $XDG_CONFIG_HOME/ptpython/config.py From 73a12553640280d01d576c080c589ec0fd099f05 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 20 Nov 2025 21:03:24 +0000 Subject: [PATCH 467/470] Release 3.0.32. --- CHANGELOG | 12 +++++++++++- pyproject.toml | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 8a07ede2..838303f6 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,13 +1,23 @@ CHANGELOG ========= +3.0.32: 2025-11-20 +------------------ + +Fixes: +- Use ANSI colors (instead of RGB colors) for the default code theme. This + improves the chances of having a better contrast with the background color, + because we now use what is configured in the terminal emulator. The theme is + called 'default-ansi' and exists alongside 'default'. + + 3.0.31: 2025-08-27 ------------------ Fixes: - Add `patch_stdout_raw` parameter to `embed()`. - Fix repl.min_brightness and repl.max_brightness config in history. -- Fix positioning of exit confirmation (compatibilitiy with latest prompt_toolkit). +- Fix positioning of exit confirmation (compatibility with latest prompt_toolkit). 3.0.30: 2025-04-15 diff --git a/pyproject.toml b/pyproject.toml index f1f3a853..cb6d1ed1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ptpython" -version = "3.0.31" +version = "3.0.32" description = "Python REPL build on top of prompt_toolkit" readme = "README.rst" authors = [{ name = "Jonathan Slenders" }] From 8fa5ddb3eb7842fd54484bac74b091c00fb804d5 Mon Sep 17 00:00:00 2001 From: Prescott Murphy Date: Mon, 22 Jul 2024 18:59:29 -0400 Subject: [PATCH 468/470] Pass style with meta completion text --- src/ptpython/completer.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/ptpython/completer.py b/src/ptpython/completer.py index e8bab285..736756ab 100644 --- a/src/ptpython/completer.py +++ b/src/ptpython/completer.py @@ -285,12 +285,15 @@ def get_completions( if jc.type == "param": suffix = "..." + style = _get_style_for_jedi_completion(jc) + display_meta = jc.type if style == "" else [(style, jc.type)] + yield Completion( jc.name_with_symbols, len(jc.complete) - len(jc.name_with_symbols), display=jc.name_with_symbols + suffix, - display_meta=jc.type, - style=_get_style_for_jedi_completion(jc), + display_meta=display_meta, + style=style, ) From bed05014c34a01c41233481dea07213ac9dca80d Mon Sep 17 00:00:00 2001 From: Prescott Murphy Date: Mon, 22 Jul 2024 19:29:59 -0400 Subject: [PATCH 469/470] Use distinct style to make feature completely opt-in --- src/ptpython/completer.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ptpython/completer.py b/src/ptpython/completer.py index 736756ab..40701cab 100644 --- a/src/ptpython/completer.py +++ b/src/ptpython/completer.py @@ -286,7 +286,9 @@ def get_completions( suffix = "..." style = _get_style_for_jedi_completion(jc) - display_meta = jc.type if style == "" else [(style, jc.type)] + display_meta = ( + jc.type if style == "" else [(f"{style}-meta", jc.type)] + ) yield Completion( jc.name_with_symbols, From dfe9dae32df17f6695aad6168ec271d02eb1232e Mon Sep 17 00:00:00 2001 From: Prescott Murphy Date: Thu, 20 Nov 2025 22:08:27 -0500 Subject: [PATCH 470/470] Add display meta styling classes to default style --- src/ptpython/style.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/ptpython/style.py b/src/ptpython/style.py index 85219717..8242df31 100644 --- a/src/ptpython/style.py +++ b/src/ptpython/style.py @@ -137,6 +137,14 @@ def generate_style(python_style: BaseStyle, ui_style: BaseStyle) -> BaseStyle: "completion.keyword": "fg:#008800", "completion.keyword fuzzymatch.inside": "fg:#008800", "completion.keyword fuzzymatch.outside": "fg:#44aa44", + # Styling for the meta completion menu that displays the type of each + # completion, e.g. param, builtin, keyword to the right of the item. + "completion.param-meta": "fg:ansiblue", + "completion.param-meta fuzzymatch.inside.character": "fg:ansiblue", + "completion.builtin-meta": "fg:ansigreen", + "completion.builtin-meta fuzzymatch.inside.character": "fg:ansigreen", + "completion.keyword-meta": "fg:ansired", + "completion.keyword-meta fuzzymatch.inside.character": "fg:ansired", # Separator between windows. (Used above docstring.) "separator": "#bbbbbb", # System toolbar