You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
345 lines
11 KiB
345 lines
11 KiB
6 years ago
|
"""
|
||
|
Utility for creating a Python repl.
|
||
|
|
||
|
::
|
||
|
|
||
|
from ptpython.repl import embed
|
||
|
embed(globals(), locals(), vi_mode=False)
|
||
|
|
||
|
"""
|
||
|
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
|
||
|
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
|
||
|
from prompt_toolkit.formatted_text import PygmentsTokens
|
||
|
|
||
|
from .python_input import PythonInput
|
||
|
from .eventloop import inputhook
|
||
|
|
||
|
import os
|
||
|
import six
|
||
|
import sys
|
||
|
import traceback
|
||
|
import warnings
|
||
|
|
||
|
__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)
|
||
|
self._load_start_paths()
|
||
|
|
||
|
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):
|
||
|
with open(path, 'rb') as f:
|
||
|
code = compile(f.read(), path, 'exec')
|
||
|
six.exec_(code, self.get_globals(), self.get_locals())
|
||
|
else:
|
||
|
output = self.app.output
|
||
|
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:
|
||
|
text = self.app.run(inputhook=inputhook)
|
||
|
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, text):
|
||
|
line = self.default_buffer.text
|
||
|
|
||
|
if line and not line.isspace():
|
||
|
try:
|
||
|
# Eval and print.
|
||
|
self._execute(line)
|
||
|
except KeyboardInterrupt as e: # KeyboardInterrupt doesn't inherit from Exception.
|
||
|
self._handle_keyboard_interrupt(e)
|
||
|
except Exception as e:
|
||
|
self._handle_exception(e)
|
||
|
|
||
|
if self.insert_blank_line_after_output:
|
||
|
self.app.output.write('\n')
|
||
|
|
||
|
self.current_statement_index += 1
|
||
|
self.signatures = []
|
||
|
|
||
|
def _execute(self, line):
|
||
|
"""
|
||
|
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:
|
||
|
sys.path.insert(0, '')
|
||
|
|
||
|
def compile_with_flags(code, mode):
|
||
|
" Compile code with the right compiler flags. "
|
||
|
return compile(code, '<stdin>', 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('!'):
|
||
|
# Run as shell command
|
||
|
os.system(line[1:])
|
||
|
else:
|
||
|
# Try eval first
|
||
|
try:
|
||
|
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
|
||
|
|
||
|
if result is not None:
|
||
|
out_prompt = 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('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)
|
||
|
|
||
|
# 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.flush()
|
||
|
|
||
|
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.
|
||
|
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):
|
||
|
if tb_tuple[0] == '<stdin>':
|
||
|
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))
|
||
|
|
||
|
# 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.
|
||
|
# (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))
|
||
|
else:
|
||
|
tokens = [(Token, tb)]
|
||
|
|
||
|
print_formatted_text(
|
||
|
PygmentsTokens(tokens), style=self._current_style,
|
||
|
style_transformation=self.style_transformation,
|
||
|
include_default_pygments_style=False)
|
||
|
|
||
|
output.write('%s\n' % e)
|
||
|
output.flush()
|
||
|
|
||
|
def _handle_keyboard_interrupt(self, e):
|
||
|
output = self.app.output
|
||
|
|
||
|
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
|
||
|
the REPL. This is recommended to call, before calling `embed`.
|
||
|
|
||
|
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__')
|
||
|
|
||
|
|
||
|
def run_config(repl, config_file='~/.ptpython/config.py'):
|
||
|
"""
|
||
|
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...')
|
||
|
|
||
|
# Check whether this file exists.
|
||
|
if not os.path.exists(config_file):
|
||
|
print('Impossible to read %r' % config_file)
|
||
|
enter_to_continue()
|
||
|
return
|
||
|
|
||
|
# Run the config file in an empty namespace.
|
||
|
try:
|
||
|
namespace = {}
|
||
|
|
||
|
with open(config_file, 'rb') as f:
|
||
|
code = compile(f.read(), config_file, 'exec')
|
||
|
six.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)
|
||
|
|
||
|
except Exception:
|
||
|
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):
|
||
|
"""
|
||
|
Call this to embed Python shell at the current point in your program.
|
||
|
It's similar to `IPython.embed` and `bpython.embed`. ::
|
||
|
|
||
|
from prompt_toolkit.contrib.repl import embed
|
||
|
embed(globals(), locals())
|
||
|
|
||
|
: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)
|
||
|
|
||
|
# Default globals/locals
|
||
|
if globals is None:
|
||
|
globals = {
|
||
|
'__name__': '__main__',
|
||
|
'__package__': None,
|
||
|
'__doc__': None,
|
||
|
'__builtins__': six.moves.builtins,
|
||
|
}
|
||
|
|
||
|
locals = locals or globals
|
||
|
|
||
|
def get_globals():
|
||
|
return 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)
|
||
|
|
||
|
if title:
|
||
|
repl.terminal_title = title
|
||
|
|
||
|
if configure:
|
||
|
configure(repl)
|
||
|
|
||
|
app = repl.app
|
||
|
|
||
|
# Start repl.
|
||
|
patch_context = app.patch_stdout_context() if patch_stdout else DummyContext()
|
||
|
|
||
|
if return_asyncio_coroutine: # XXX
|
||
|
def coroutine():
|
||
|
with patch_context:
|
||
|
for future in app.run_async():
|
||
|
yield future
|
||
|
return coroutine()
|
||
|
else:
|
||
|
with patch_context:
|
||
|
repl.run()
|