diff options
-rw-r--r-- | .github/workflows/test.yaml | 1 | ||||
-rw-r--r-- | CHANGELOG | 25 | ||||
-rw-r--r-- | examples/ptpython_config/config.py | 2 | ||||
-rw-r--r-- | ptpython/completer.py | 4 | ||||
-rw-r--r-- | ptpython/entry_points/run_ptipython.py | 2 | ||||
-rw-r--r-- | ptpython/entry_points/run_ptpython.py | 8 | ||||
-rw-r--r-- | ptpython/history_browser.py | 22 | ||||
-rw-r--r-- | ptpython/ipython.py | 17 | ||||
-rw-r--r-- | ptpython/key_bindings.py | 12 | ||||
-rw-r--r-- | ptpython/layout.py | 12 | ||||
-rw-r--r-- | ptpython/prompt_style.py | 4 | ||||
-rw-r--r-- | ptpython/python_input.py | 158 | ||||
-rw-r--r-- | ptpython/repl.py | 191 | ||||
-rw-r--r-- | setup.py | 12 |
14 files changed, 276 insertions, 194 deletions
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 00ed1b0..0368ba7 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: | @@ -1,6 +1,31 @@ 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 +------------------ + +Fixes: +- Made "black" an optional dependency. + + +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/examples/ptpython_config/config.py b/examples/ptpython_config/config.py index 8532f93..2427572 100644 --- a/examples/ptpython_config/config.py +++ b/examples/ptpython_config/config.py @@ -157,7 +157,7 @@ def configure(repl): @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. diff --git a/ptpython/completer.py b/ptpython/completer.py index 9f7e10b..285398c 100644 --- a/ptpython/completer.py +++ b/ptpython/completer.py @@ -468,7 +468,7 @@ class DictionaryCompleter(Completer): """ 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 @@ class HidePrivateCompleter(Completer): 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 650633e..21d7063 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 0b3dbdb..5ebe2b9 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: diff --git a/ptpython/history_browser.py b/ptpython/history_browser.py index 798a280..b7fe086 100644 --- a/ptpython/history_browser.py +++ b/ptpython/history_browser.py @@ -85,7 +85,7 @@ Further, remember that searching works like in Emacs class BORDER: - " Box drawing characters. " + "Box drawing characters." HORIZONTAL = "\u2501" VERTICAL = "\u2503" TOP_LEFT = "\u250f" @@ -420,7 +420,7 @@ class HistoryMapping: 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 create_key_bindings(history, python_input, history_mapping): # 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 create_key_bindings(history, python_input, history_mapping): @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 create_key_bindings(history, python_input, history_mapping): @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 @@ class PythonHistory: ) 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/ipython.py b/ptpython/ipython.py index 2e8d119..9163334 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()) diff --git a/ptpython/key_bindings.py b/ptpython/key_bindings.py index 86317f9..ae23a3d 100644 --- a/ptpython/key_bindings.py +++ b/ptpython/key_bindings.py @@ -203,7 +203,7 @@ def load_python_bindings(python_input): @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 load_sidebar_bindings(python_input): @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 load_sidebar_bindings(python_input): @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 load_sidebar_bindings(python_input): @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 6482cbd..dc6b19b 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -64,7 +64,7 @@ __all__ = ["PtPythonLayout", "CompletionVisualisation"] 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 python_sidebar(python_input: "PythonInput") -> Window: @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 meta_enter_message(python_input: "PythonInput") -> Container: @Condition def extra_condition() -> bool: - " Only show when... " + "Only show when..." b = python_input.default_buffer return ( @@ -646,7 +646,7 @@ class PtPythonLayout: 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 @@ class PtPythonLayout: ] ) - self.layout = Layout(root_container) + self.layout = Layout(self.root_container) self.sidebar = sidebar diff --git a/ptpython/prompt_style.py b/ptpython/prompt_style.py index 24e5f88..e7334af 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 @@ class PromptStyle(metaclass=ABCMeta): @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 e63cdf1..1785f52 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -4,7 +4,6 @@ This can be used for creation of Python REPLs. """ 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 @@ -174,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__( @@ -188,6 +192,7 @@ class PythonInput: 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, @@ -380,10 +385,16 @@ class PythonInput: extra_toolbars=self._extra_toolbars, ) - self.app = self._create_application(input, output) - - if vi_mode: - self.app.editing_mode = EditingMode.VI + # 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) + # 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 def _accept_handler(self, buff: Buffer) -> bool: app = get_app() @@ -393,12 +404,12 @@ class PythonInput: @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: @@ -521,7 +532,7 @@ class PythonInput: 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(): @@ -914,23 +925,19 @@ class PythonInput: else: self.editing_mode = EditingMode.EMACS - def _on_input_timeout(self, buff: Buffer, loop=None) -> None: + @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, 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 +949,47 @@ class PythonInput: document, self.get_locals(), self.get_globals() ) - self._get_signatures_thread_running = False + return signatures + + app = self.app - # 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 + async def on_timeout_task() -> None: + loop = get_event_loop() - # Set docstring in docstring buffer. - if signatures: - self.docstring_buffer.reset( - document=Document(signatures[0].docstring, cursor_position=0) + # Never run multiple get-signature threads. + if self._get_signatures_thread_running: + return + self._get_signatures_thread_running = True + + 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 = [] @@ -970,7 +998,7 @@ class PythonInput: """ Display the history. """ - app = get_app() + app = self.app app.vi_state.input_mode = InputMode.NAVIGATION history = PythonHistory(self, self.default_buffer.document) @@ -1012,43 +1040,25 @@ class PythonInput: 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 ae7b1d0..220c673 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -11,7 +11,6 @@ import asyncio import builtins import os import sys -import threading import traceback import types import warnings @@ -80,7 +79,7 @@ class PythonRepl(PythonInput): 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): @@ -91,6 +90,35 @@ class PythonRepl(PythonInput): 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: + raise + 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. @@ -102,44 +130,41 @@ class PythonRepl(PythonInput): 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 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 = [] + 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 @@ -169,24 +194,7 @@ class PythonRepl(PythonInput): 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 = [] + await self.run_and_show_expression_async(text) except KeyboardInterrupt as e: # XXX: This does not yet work properly. In some situations, @@ -231,7 +239,10 @@ class PythonRepl(PythonInput): # 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 @@ -263,9 +274,14 @@ class PythonRepl(PythonInput): 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 @@ -277,7 +293,7 @@ class PythonRepl(PythonInput): 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, "<stdin>", @@ -286,9 +302,9 @@ class PythonRepl(PythonInput): dont_inherit=True, ) - def show_result(self, result: object) -> None: + def _format_result_output(self, result: object) -> StyleAndTextTuples: """ - 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 @@ -304,7 +320,7 @@ class PythonRepl(PythonInput): except BaseException as e: # Calling repr failed. self._handle_exception(e) - return + return [] try: compile(result_repr, "", "eval") @@ -315,12 +331,15 @@ class PythonRepl(PythonInput): 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.Mode(line_length=self.app.output.get_size().columns), + ) formatted_result_repr = to_formatted_text( PygmentsTokens(list(_lex_python_result(result_repr))) @@ -363,10 +382,18 @@ class PythonRepl(PythonInput): 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() @@ -421,15 +448,7 @@ class PythonRepl(PythonInput): # 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("...") @@ -494,9 +513,7 @@ class PythonRepl(PythonInput): """ 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) -> 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() @@ -525,9 +542,15 @@ class PythonRepl(PythonInput): 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( - PygmentsTokens(tokens), + tokens, style=self._current_style, style_transformation=self.style_transformation, include_default_pygments_style=False, @@ -564,13 +587,13 @@ class PythonRepl(PythonInput): 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 @@ -590,7 +613,9 @@ 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. @@ -738,7 +763,7 @@ def create_pager_prompt( @bindings.add("<any>") def _(event: KeyPressEvent) -> None: - " Disallow inserting other text. " + "Disallow inserting other text." pass style @@ -11,7 +11,7 @@ with open(os.path.join(os.path.dirname(__file__), "README.rst")) as f: setup( name="ptpython", author="Jonathan Slenders", - version="3.0.16", + version="3.0.19", url="https://github.com/prompt-toolkit/ptpython", description="Python REPL build on top of prompt_toolkit", long_description=long_description, @@ -20,10 +20,9 @@ setup( "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", ], python_requires=">=3.6", classifiers=[ @@ -47,5 +46,8 @@ setup( % 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 + }, ) |