1
0
Fork 0

Adding upstream version 3.0.19.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-02-09 18:26:28 +01:00
parent 0014608abc
commit 6e91d72088
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
14 changed files with 272 additions and 190 deletions

View file

@ -23,6 +23,7 @@ jobs:
sudo apt remove python3-pip sudo apt remove python3-pip
python -m pip install --upgrade pip python -m pip install --upgrade pip
python -m pip install . black isort mypy pytest readme_renderer python -m pip install . black isort mypy pytest readme_renderer
python -m pip install . types-dataclasses # Needed for Python 3.6
pip list pip list
- name: Type Checker - name: Type Checker
run: | run: |

View file

@ -1,6 +1,31 @@
CHANGELOG 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 3.0.16: 2020-02-11
------------------ ------------------

View file

@ -157,7 +157,7 @@ def configure(repl):
@repl.add_key_binding("j", "j", filter=ViInsertMode()) @repl.add_key_binding("j", "j", filter=ViInsertMode())
def _(event): def _(event):
" Map 'jj' to Escape. " " 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. # Custom key binding for some simple autocorrection while typing.

View file

@ -468,7 +468,7 @@ class DictionaryCompleter(Completer):
""" """
def abbr_meta(text: str) -> str: 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. # Take first line, if multiple lines.
if len(text) > 20: if len(text) > 20:
text = text[:20] + "..." text = text[:20] + "..."
@ -621,7 +621,7 @@ class HidePrivateCompleter(Completer):
class ReprFailedError(Exception): class ReprFailedError(Exception):
" Raised when the repr() call in `DictionaryCompleter` fails. " "Raised when the repr() call in `DictionaryCompleter` fails."
try: try:

View file

@ -31,7 +31,7 @@ def run(user_ns=None):
path = a.args[0] path = a.args[0]
with open(path, "rb") as f: with open(path, "rb") as f:
code = compile(f.read(), path, "exec") code = compile(f.read(), path, "exec")
exec(code, {}) exec(code, {"__name__": "__main__", "__file__": path})
else: else:
enable_deprecation_warnings() enable_deprecation_warnings()

View file

@ -179,9 +179,11 @@ def run() -> None:
path = a.args[0] path = a.args[0]
with open(path, "rb") as f: with open(path, "rb") as f:
code = compile(f.read(), path, "exec") code = compile(f.read(), path, "exec")
# NOTE: We have to pass an empty dictionary as namespace. Omitting # NOTE: We have to pass a dict as namespace. Omitting this argument
# this argument causes imports to not be found. See issue #326. # causes imports to not be found. See issue #326.
exec(code, {}) # 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. # Run interactive shell.
else: else:

View file

@ -85,7 +85,7 @@ Further, remember that searching works like in Emacs
class BORDER: class BORDER:
" Box drawing characters. " "Box drawing characters."
HORIZONTAL = "\u2501" HORIZONTAL = "\u2501"
VERTICAL = "\u2503" VERTICAL = "\u2503"
TOP_LEFT = "\u250f" TOP_LEFT = "\u250f"
@ -420,7 +420,7 @@ class HistoryMapping:
def _toggle_help(history): def _toggle_help(history):
" Display/hide help. " "Display/hide help."
help_buffer_control = history.history_layout.help_buffer_control help_buffer_control = history.history_layout.help_buffer_control
if history.app.layout.current_control == 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): def _select_other_window(history):
" Toggle focus between left/right window. " "Toggle focus between left/right window."
current_buffer = history.app.current_buffer current_buffer = history.app.current_buffer
layout = history.history_layout.layout 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. # Eager: ignore the Emacs [Ctrl-X Ctrl-X] binding.
@handle("c-w", filter=main_buffer_focussed) @handle("c-w", filter=main_buffer_focussed)
def _(event): def _(event):
" Select other window. " "Select other window."
_select_other_window(history) _select_other_window(history)
@handle("f4") @handle("f4")
def _(event): def _(event):
" Switch between Emacs/Vi mode. " "Switch between Emacs/Vi mode."
python_input.vi_mode = not python_input.vi_mode python_input.vi_mode = not python_input.vi_mode
@handle("f1") @handle("f1")
def _(event): def _(event):
" Display/hide help. " "Display/hide help."
_toggle_help(history) _toggle_help(history)
@handle("enter", filter=help_focussed) @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("c-g", filter=help_focussed)
@handle("escape", filter=help_focussed) @handle("escape", filter=help_focussed)
def _(event): def _(event):
" Leave help. " "Leave help."
event.app.layout.focus_previous() event.app.layout.focus_previous()
@handle("q", filter=main_buffer_focussed) @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-c", filter=main_buffer_focussed)
@handle("c-g", filter=main_buffer_focussed) @handle("c-g", filter=main_buffer_focussed)
def _(event): def _(event):
" Cancel and go back. " "Cancel and go back."
event.app.exit(result=None) event.app.exit(result=None)
@handle("enter", filter=main_buffer_focussed) @handle("enter", filter=main_buffer_focussed)
def _(event): def _(event):
" Accept input. " "Accept input."
event.app.exit(result=history.default_buffer.text) event.app.exit(result=history.default_buffer.text)
enable_system_bindings = Condition(lambda: python_input.enable_system_bindings) 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): def _(event):
" Suspend to background. " "Suspend to background."
event.app.suspend_to_background() event.app.suspend_to_background()
return bindings return bindings
@ -630,7 +630,7 @@ class PythonHistory:
) )
def _history_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. # Only when this buffer has the focus.
if self.app.current_buffer == self.history_buffer: if self.app.current_buffer == self.history_buffer:
line_no = self.history_buffer.document.cursor_position_row line_no = self.history_buffer.document.cursor_position_row

View file

@ -282,4 +282,21 @@ def embed(**kwargs):
kwargs["config"] = config kwargs["config"] = config
shell = InteractiveShellEmbed.instance(**kwargs) shell = InteractiveShellEmbed.instance(**kwargs)
initialize_extensions(shell, config["InteractiveShellApp"]["extensions"]) initialize_extensions(shell, config["InteractiveShellApp"]["extensions"])
run_startup_scripts(shell)
shell(header=header, stack_depth=2, compile_flags=compile_flags) 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())

View file

@ -203,7 +203,7 @@ def load_python_bindings(python_input):
@handle("c-c", filter=has_focus(python_input.default_buffer)) @handle("c-c", filter=has_focus(python_input.default_buffer))
def _(event): def _(event):
" Abort when Control-C has been pressed. " "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 return bindings
@ -222,7 +222,7 @@ def load_sidebar_bindings(python_input):
@handle("c-p", filter=sidebar_visible) @handle("c-p", filter=sidebar_visible)
@handle("k", filter=sidebar_visible) @handle("k", filter=sidebar_visible)
def _(event): def _(event):
" Go to previous option. " "Go to previous option."
python_input.selected_option_index = ( python_input.selected_option_index = (
python_input.selected_option_index - 1 python_input.selected_option_index - 1
) % python_input.option_count ) % python_input.option_count
@ -231,7 +231,7 @@ def load_sidebar_bindings(python_input):
@handle("c-n", filter=sidebar_visible) @handle("c-n", filter=sidebar_visible)
@handle("j", filter=sidebar_visible) @handle("j", filter=sidebar_visible)
def _(event): def _(event):
" Go to next option. " "Go to next option."
python_input.selected_option_index = ( python_input.selected_option_index = (
python_input.selected_option_index + 1 python_input.selected_option_index + 1
) % python_input.option_count ) % python_input.option_count
@ -240,14 +240,14 @@ def load_sidebar_bindings(python_input):
@handle("l", filter=sidebar_visible) @handle("l", filter=sidebar_visible)
@handle(" ", filter=sidebar_visible) @handle(" ", filter=sidebar_visible)
def _(event): def _(event):
" Select next value for current option. " "Select next value for current option."
option = python_input.selected_option option = python_input.selected_option
option.activate_next() option.activate_next()
@handle("left", filter=sidebar_visible) @handle("left", filter=sidebar_visible)
@handle("h", filter=sidebar_visible) @handle("h", filter=sidebar_visible)
def _(event): def _(event):
" Select previous value for current option. " "Select previous value for current option."
option = python_input.selected_option option = python_input.selected_option
option.activate_previous() option.activate_previous()
@ -257,7 +257,7 @@ def load_sidebar_bindings(python_input):
@handle("enter", filter=sidebar_visible) @handle("enter", filter=sidebar_visible)
@handle("escape", filter=sidebar_visible) @handle("escape", filter=sidebar_visible)
def _(event): def _(event):
" Hide sidebar. " "Hide sidebar."
python_input.show_sidebar = False python_input.show_sidebar = False
event.app.layout.focus_last() event.app.layout.focus_last()

View file

@ -64,7 +64,7 @@ __all__ = ["PtPythonLayout", "CompletionVisualisation"]
class CompletionVisualisation(Enum): class CompletionVisualisation(Enum):
" Visualisation method for the completions. " "Visualisation method for the completions."
NONE = "none" NONE = "none"
POP_UP = "pop-up" POP_UP = "pop-up"
MULTI_COLUMN = "multi-column" MULTI_COLUMN = "multi-column"
@ -116,7 +116,7 @@ def python_sidebar(python_input: "PythonInput") -> Window:
@if_mousedown @if_mousedown
def goto_next(mouse_event: MouseEvent) -> None: 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 python_input.selected_option_index = index
option = python_input.selected_option option = python_input.selected_option
option.activate_next() option.activate_next()
@ -472,7 +472,7 @@ def show_sidebar_button_info(python_input: "PythonInput") -> Container:
@if_mousedown @if_mousedown
def toggle_sidebar(mouse_event: MouseEvent) -> None: 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 python_input.show_sidebar = not python_input.show_sidebar
version = sys.version_info version = sys.version_info
@ -544,7 +544,7 @@ def meta_enter_message(python_input: "PythonInput") -> Container:
@Condition @Condition
def extra_condition() -> bool: def extra_condition() -> bool:
" Only show when... " "Only show when..."
b = python_input.default_buffer b = python_input.default_buffer
return ( return (
@ -646,7 +646,7 @@ class PtPythonLayout:
sidebar = python_sidebar(python_input) sidebar = python_sidebar(python_input)
self.exit_confirmation = create_exit_confirmation(python_input) self.exit_confirmation = create_exit_confirmation(python_input)
root_container = HSplit( self.root_container = HSplit(
[ [
VSplit( VSplit(
[ [
@ -759,5 +759,5 @@ class PtPythonLayout:
] ]
) )
self.layout = Layout(root_container) self.layout = Layout(self.root_container)
self.sidebar = sidebar self.sidebar = sidebar

View file

@ -16,7 +16,7 @@ class PromptStyle(metaclass=ABCMeta):
@abstractmethod @abstractmethod
def in_prompt(self) -> AnyFormattedText: def in_prompt(self) -> AnyFormattedText:
" Return the input tokens. " "Return the input tokens."
return [] return []
@abstractmethod @abstractmethod
@ -31,7 +31,7 @@ class PromptStyle(metaclass=ABCMeta):
@abstractmethod @abstractmethod
def out_prompt(self) -> AnyFormattedText: def out_prompt(self) -> AnyFormattedText:
" Return the output tokens. " "Return the output tokens."
return [] return []

View file

@ -4,7 +4,6 @@ This can be used for creation of Python REPLs.
""" """
import __future__ import __future__
import threading
from asyncio import get_event_loop from asyncio import get_event_loop
from functools import partial 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, Optional, TypeVar
@ -174,6 +173,11 @@ class PythonInput:
python_input = PythonInput(...) python_input = PythonInput(...)
python_code = python_input.app.run() 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__( def __init__(
@ -188,6 +192,7 @@ class PythonInput:
output: Optional[Output] = None, output: Optional[Output] = None,
# For internal use. # For internal use.
extra_key_bindings: Optional[KeyBindings] = None, extra_key_bindings: Optional[KeyBindings] = None,
create_app=True,
_completer: Optional[Completer] = None, _completer: Optional[Completer] = None,
_validator: Optional[Validator] = None, _validator: Optional[Validator] = None,
_lexer: Optional[Lexer] = None, _lexer: Optional[Lexer] = None,
@ -380,10 +385,16 @@ class PythonInput:
extra_toolbars=self._extra_toolbars, 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: Optional[Application] = self._create_application(input, output)
# Setting vi_mode will not work unless the prompt_toolkit
# application has been created.
if vi_mode: if vi_mode:
self.app.editing_mode = EditingMode.VI self.app.editing_mode = EditingMode.VI
else:
self._app = None
def _accept_handler(self, buff: Buffer) -> bool: def _accept_handler(self, buff: Buffer) -> bool:
app = get_app() app = get_app()
@ -393,12 +404,12 @@ class PythonInput:
@property @property
def option_count(self) -> int: 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) return sum(len(category.options) for category in self.options)
@property @property
def selected_option(self) -> Option: def selected_option(self) -> Option:
" Return the currently selected option. " "Return the currently selected option."
i = 0 i = 0
for category in self.options: for category in self.options:
for o in category.options: for o in category.options:
@ -521,7 +532,7 @@ class PythonInput:
def simple_option( def simple_option(
title: str, description: str, field_name: str, values: Optional[List] = None title: str, description: str, field_name: str, values: Optional[List] = None
) -> Option: ) -> Option:
" Create Simple on/of option. " "Create Simple on/of option."
values = values or ["off", "on"] values = values or ["off", "on"]
def get_current_value(): def get_current_value():
@ -914,23 +925,19 @@ class PythonInput:
else: else:
self.editing_mode = EditingMode.EMACS 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, When there is no input activity,
in another thread, get the signature of the current code. in another thread, get the signature of the current code.
""" """
app = self.app
# Never run multiple get-signature threads. def get_signatures_in_executor(document: Document) -> List[Signature]:
if self._get_signatures_thread_running:
return
self._get_signatures_thread_running = True
document = buff.document
loop = loop or get_event_loop()
def run():
# First, get signatures from Jedi. If we didn't found any and if # First, get signatures from Jedi. If we didn't found any and if
# "dictionary completion" (eval-based completion) is enabled, then # "dictionary completion" (eval-based completion) is enabled, then
# get signatures using eval. # get signatures using eval.
@ -942,11 +949,33 @@ class PythonInput:
document, self.get_locals(), self.get_globals() document, self.get_locals(), self.get_globals()
) )
return signatures
app = self.app
async def on_timeout_task() -> None:
loop = get_event_loop()
# 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
)
# 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 self._get_signatures_thread_running = False
# Set signatures and redraw if the text didn't change in the # Set signatures and redraw.
# meantime. Otherwise request new signatures.
if buff.text == document.text:
self.signatures = signatures self.signatures = signatures
# Set docstring in docstring buffer. # Set docstring in docstring buffer.
@ -958,10 +987,9 @@ class PythonInput:
self.docstring_buffer.reset() self.docstring_buffer.reset()
app.invalidate() app.invalidate()
else:
self._on_input_timeout(buff, loop=loop)
loop.run_in_executor(None, run) if app.is_running:
app.create_background_task(on_timeout_task())
def on_reset(self) -> None: def on_reset(self) -> None:
self.signatures = [] self.signatures = []
@ -970,7 +998,7 @@ class PythonInput:
""" """
Display the history. Display the history.
""" """
app = get_app() app = self.app
app.vi_state.input_mode = InputMode.NAVIGATION app.vi_state.input_mode = InputMode.NAVIGATION
history = PythonHistory(self, self.default_buffer.document) history = PythonHistory(self, self.default_buffer.document)
@ -1012,15 +1040,9 @@ class PythonInput:
self.app.vi_state.input_mode = InputMode.NAVIGATION self.app.vi_state.input_mode = InputMode.NAVIGATION
# Run the UI. # Run the UI.
result: str = ""
exception: Optional[BaseException] = None
def in_thread() -> None:
nonlocal result, exception
try:
while True: while True:
try: try:
result = self.app.run(pre_run=pre_run) result = self.app.run(pre_run=pre_run, in_thread=True)
if result.lstrip().startswith("\x1a"): if result.lstrip().startswith("\x1a"):
# When the input starts with Ctrl-Z, quit the REPL. # When the input starts with Ctrl-Z, quit the REPL.
@ -1033,22 +1055,10 @@ class PythonInput:
result = unindent_code(result) result = unindent_code(result)
if result and not result.isspace(): 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: if self.insert_blank_line_after_input:
self.app.output.write("\n") self.app.output.write("\n")
thread = threading.Thread(target=in_thread)
thread.start()
thread.join()
if exception is not None:
raise exception
return result return result
except KeyboardInterrupt:
# Abort - try again.
self.default_buffer.document = Document()

View file

@ -11,7 +11,6 @@ import asyncio
import builtins import builtins
import os import os
import sys import sys
import threading
import traceback import traceback
import types import types
import warnings import warnings
@ -80,7 +79,7 @@ class PythonRepl(PythonInput):
self._load_start_paths() self._load_start_paths()
def _load_start_paths(self) -> None: def _load_start_paths(self) -> None:
" Start the Read-Eval-Print Loop. " "Start the Read-Eval-Print Loop."
if self._startup_paths: if self._startup_paths:
for path in self._startup_paths: for path in self._startup_paths:
if os.path.exists(path): if os.path.exists(path):
@ -91,31 +90,15 @@ class PythonRepl(PythonInput):
output = self.app.output 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) -> None: def run_and_show_expression(self, expression):
"""
Run the REPL loop.
"""
if self.terminal_title:
set_title(self.terminal_title)
self._add_to_namespace()
try: try:
while True:
try:
# Read.
try:
text = self.read()
except EOFError:
return
# Eval. # Eval.
try: try:
result = self.eval(text) result = self.eval(expression)
except KeyboardInterrupt as e: # KeyboardInterrupt doesn't inherit from Exception. except KeyboardInterrupt: # KeyboardInterrupt doesn't inherit from Exception.
raise raise
except SystemExit: except SystemExit:
return raise
except BaseException as e: except BaseException as e:
self._handle_exception(e) self._handle_exception(e)
else: else:
@ -135,11 +118,53 @@ class PythonRepl(PythonInput):
# prevent that a Control-C keypress terminates the REPL in # prevent that a Control-C keypress terminates the REPL in
# any case.) # any case.)
self._handle_keyboard_interrupt(e) self._handle_keyboard_interrupt(e)
def run(self) -> None:
"""
Run the REPL loop.
"""
if self.terminal_title:
set_title(self.terminal_title)
self._add_to_namespace()
try:
while True:
# Pull text from the user.
try:
text = self.read()
except EOFError:
return
# Run it; display the result (or errors if applicable).
self.run_and_show_expression(text)
finally: finally:
if self.terminal_title: if self.terminal_title:
clear_title() clear_title()
self._remove_from_namespace() 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: async def run_async(self) -> None:
""" """
Run the REPL loop, but run the blocking parts in an executor, so that Run the REPL loop, but run the blocking parts in an executor, so that
@ -169,24 +194,7 @@ class PythonRepl(PythonInput):
return return
# Eval. # Eval.
try: await self.run_and_show_expression_async(text)
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: except KeyboardInterrupt as e:
# XXX: This does not yet work properly. In some situations, # 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. # above, then `sys.exc_info()` would not report the right error.
# See issue: https://github.com/prompt-toolkit/ptpython/issues/435 # See issue: https://github.com/prompt-toolkit/ptpython/issues/435
code = self._compile_with_flags(line, "exec") 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 return None
@ -263,9 +274,14 @@ class PythonRepl(PythonInput):
self._store_eval_result(result) self._store_eval_result(result)
return 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") 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 return None
@ -277,7 +293,7 @@ class PythonRepl(PythonInput):
return super().get_compiler_flags() | PyCF_ALLOW_TOP_LEVEL_AWAIT 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):
" Compile code with the right compiler flags. " "Compile code with the right compiler flags."
return compile( return compile(
code, code,
"<stdin>", "<stdin>",
@ -286,9 +302,9 @@ class PythonRepl(PythonInput):
dont_inherit=True, 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__`, Note: this can raise `KeyboardInterrupt` if either calling `__repr__`,
`__pt_repr__` or formatting the output with "Black" takes to long `__pt_repr__` or formatting the output with "Black" takes to long
@ -304,7 +320,7 @@ class PythonRepl(PythonInput):
except BaseException as e: except BaseException as e:
# Calling repr failed. # Calling repr failed.
self._handle_exception(e) self._handle_exception(e)
return return []
try: try:
compile(result_repr, "", "eval") compile(result_repr, "", "eval")
@ -315,11 +331,14 @@ class PythonRepl(PythonInput):
if self.enable_output_formatting: if self.enable_output_formatting:
# Inline import. Slightly speed up start-up time if black is # Inline import. Slightly speed up start-up time if black is
# not used. # not used.
try:
import black import black
except ImportError:
pass # no Black package in your installation
else:
result_repr = black.format_str( result_repr = black.format_str(
result_repr, 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( formatted_result_repr = to_formatted_text(
@ -363,10 +382,18 @@ class PythonRepl(PythonInput):
out_prompt + [("", fragment_list_to_text(formatted_result_repr))] 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: if self.enable_pager:
self.print_paginated_formatted_text(to_formatted_text(formatted_output)) self.print_paginated_formatted_text(formatted_text_output)
else: else:
self.print_formatted_text(to_formatted_text(formatted_output)) self.print_formatted_text(formatted_text_output)
self.app.output.flush() self.app.output.flush()
@ -421,15 +448,7 @@ class PythonRepl(PythonInput):
# Run pager prompt in another thread. # Run pager prompt in another thread.
# Same as for the input. This prevents issues with nested event # Same as for the input. This prevents issues with nested event
# loops. # loops.
pager_result = None pager_result = pager_prompt.prompt(in_thread=True)
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: if pager_result == PagerResult.ABORT:
print("...") print("...")
@ -494,9 +513,7 @@ class PythonRepl(PythonInput):
""" """
return create_pager_prompt(self._current_style, self.title) return create_pager_prompt(self._current_style, self.title)
def _handle_exception(self, e: BaseException) -> None: def _format_exception_output(self, e: BaseException) -> PygmentsTokens:
output = self.app.output
# Instead of just calling ``traceback.format_exc``, we take the # Instead of just calling ``traceback.format_exc``, we take the
# traceback and skip the bottom calls of this framework. # traceback and skip the bottom calls of this framework.
t, v, tb = sys.exc_info() t, v, tb = sys.exc_info()
@ -525,9 +542,15 @@ class PythonRepl(PythonInput):
tokens = list(_lex_python_traceback(tb_str)) tokens = list(_lex_python_traceback(tb_str))
else: else:
tokens = [(Token, tb_str)] 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( print_formatted_text(
PygmentsTokens(tokens), tokens,
style=self._current_style, style=self._current_style,
style_transformation=self.style_transformation, style_transformation=self.style_transformation,
include_default_pygments_style=False, include_default_pygments_style=False,
@ -564,13 +587,13 @@ class PythonRepl(PythonInput):
def _lex_python_traceback(tb): def _lex_python_traceback(tb):
" Return token list for traceback string. " "Return token list for traceback string."
lexer = PythonTracebackLexer() lexer = PythonTracebackLexer()
return lexer.get_tokens(tb) return lexer.get_tokens(tb)
def _lex_python_result(tb): def _lex_python_result(tb):
" Return token list for Python string. " "Return token list for Python string."
lexer = PythonLexer() lexer = PythonLexer()
# Use `get_tokens_unprocessed`, so that we get exactly the same string, # Use `get_tokens_unprocessed`, so that we get exactly the same string,
# without line endings appended. `print_formatted_text` already appends a # 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__") 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. Execute REPL config file.
@ -738,7 +763,7 @@ def create_pager_prompt(
@bindings.add("<any>") @bindings.add("<any>")
def _(event: KeyPressEvent) -> None: def _(event: KeyPressEvent) -> None:
" Disallow inserting other text. " "Disallow inserting other text."
pass pass
style style

View file

@ -11,7 +11,7 @@ with open(os.path.join(os.path.dirname(__file__), "README.rst")) as f:
setup( setup(
name="ptpython", name="ptpython",
author="Jonathan Slenders", author="Jonathan Slenders",
version="3.0.16", version="3.0.19",
url="https://github.com/prompt-toolkit/ptpython", url="https://github.com/prompt-toolkit/ptpython",
description="Python REPL build on top of prompt_toolkit", description="Python REPL build on top of prompt_toolkit",
long_description=long_description, long_description=long_description,
@ -20,10 +20,9 @@ setup(
"appdirs", "appdirs",
"importlib_metadata;python_version<'3.8'", "importlib_metadata;python_version<'3.8'",
"jedi>=0.16.0", "jedi>=0.16.0",
# Use prompt_toolkit 3.0.16, because of the `DeduplicateCompleter`. # Use prompt_toolkit 3.0.18, because of the `in_thread` option.
"prompt_toolkit>=3.0.16,<3.1.0", "prompt_toolkit>=3.0.18,<3.1.0",
"pygments", "pygments",
"black",
], ],
python_requires=">=3.6", python_requires=">=3.6",
classifiers=[ classifiers=[
@ -47,5 +46,8 @@ setup(
% sys.version_info[:2], % 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
},
) )