summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/test.yaml1
-rw-r--r--CHANGELOG25
-rw-r--r--examples/ptpython_config/config.py2
-rw-r--r--ptpython/completer.py4
-rw-r--r--ptpython/entry_points/run_ptipython.py2
-rw-r--r--ptpython/entry_points/run_ptpython.py8
-rw-r--r--ptpython/history_browser.py22
-rw-r--r--ptpython/ipython.py17
-rw-r--r--ptpython/key_bindings.py12
-rw-r--r--ptpython/layout.py12
-rw-r--r--ptpython/prompt_style.py4
-rw-r--r--ptpython/python_input.py158
-rw-r--r--ptpython/repl.py191
-rw-r--r--setup.py12
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: |
diff --git a/CHANGELOG b/CHANGELOG
index 67ac0a8..6a1eb21 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -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
diff --git a/setup.py b/setup.py
index dbbe55b..faab112 100644
--- a/setup.py
+++ b/setup.py
@@ -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
+ },
)