You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

505 lines
20 KiB
Python

# TODO that would make IPython integration better
# - show output other times then when enter was pressed
# - support proper exit to allow IPython to cleanup (e.g. temp files created with %edit)
# - support Ctrl-D (Ctrl-Z on Windows)
# - use IPython (numbered) prompts in PyDev
# - better integration of IPython and PyDev completions
# - some of the semantics on handling the code completion are not correct:
# eg: Start a line with % and then type c should give %cd as a completion by it doesn't
# however type %c and request completions and %cd is given as an option
# eg: Completing a magic when user typed it without the leading % causes the % to be inserted
# to the left of what should be the first colon.
"""Interface to TerminalInteractiveShell for PyDev Interactive Console frontend
for IPython 0.11 to 1.0+.
"""
from __future__ import print_function
import os
import sys
import codeop
import traceback
from IPython.core.error import UsageError
from IPython.core.completer import IPCompleter
from IPython.core.interactiveshell import InteractiveShell, InteractiveShellABC
from IPython.core.usage import default_banner_parts
from IPython.utils.strdispatch import StrDispatch
import IPython.core.release as IPythonRelease
from IPython.terminal.interactiveshell import TerminalInteractiveShell
try:
from traitlets import CBool, Unicode
except ImportError:
from IPython.utils.traitlets import CBool, Unicode
from IPython.core import release
from _pydev_bundle.pydev_imports import xmlrpclib
default_pydev_banner_parts = default_banner_parts
default_pydev_banner = "".join(default_pydev_banner_parts)
def show_in_pager(self, strng, *args, **kwargs):
"""Run a string through pager"""
# On PyDev we just output the string, there are scroll bars in the console
# to handle "paging". This is the same behaviour as when TERM==dump (see
# page.py)
# for compatibility with mime-bundle form:
if isinstance(strng, dict):
strng = strng.get("text/plain", strng)
print(strng)
def create_editor_hook(pydev_host, pydev_client_port):
def call_editor(filename, line=0, wait=True):
"""Open an editor in PyDev"""
if line is None:
line = 0
# Make sure to send an absolution path because unlike most editor hooks
# we don't launch a process. This is more like what happens in the zmqshell
filename = os.path.abspath(filename)
# import sys
# sys.__stderr__.write('Calling editor at: %s:%s\n' % (pydev_host, pydev_client_port))
# Tell PyDev to open the editor
server = xmlrpclib.Server("http://%s:%s" % (pydev_host, pydev_client_port))
server.IPythonEditor(filename, str(line))
if wait:
input("Press Enter when done editing:")
return call_editor
class PyDevIPCompleter(IPCompleter):
def __init__(self, *args, **kwargs):
"""Create a Completer that reuses the advanced completion support of PyDev
in addition to the completion support provided by IPython"""
IPCompleter.__init__(self, *args, **kwargs)
# Use PyDev for python matches, see getCompletions below
if self.python_matches in self.matchers:
# `self.python_matches` matches attributes or global python names
self.matchers.remove(self.python_matches)
class PyDevIPCompleter6(IPCompleter):
def __init__(self, *args, **kwargs):
"""Create a Completer that reuses the advanced completion support of PyDev
in addition to the completion support provided by IPython"""
IPCompleter.__init__(self, *args, **kwargs)
@property
def matchers(self):
"""All active matcher routines for completion"""
# To remove python_matches we now have to override it as it's now a property in the superclass.
return [
self.file_matches,
self.magic_matches,
self.python_func_kw_matches,
self.dict_key_matches,
]
@matchers.setter
def matchers(self, value):
# To stop the init in IPCompleter raising an AttributeError we now have to specify a setter as it's now a property in the superclass.
return
class PyDevTerminalInteractiveShell(TerminalInteractiveShell):
banner1 = Unicode(default_pydev_banner, config=True, help="""The part of the banner to be printed before the profile""")
# TODO term_title: (can PyDev's title be changed???, see terminal.py for where to inject code, in particular set_term_title as used by %cd)
# for now, just disable term_title
term_title = CBool(False)
# Note in version 0.11 there is no guard in the IPython code about displaying a
# warning, so with 0.11 you get:
# WARNING: Readline services not available or not loaded.
# WARNING: The auto-indent feature requires the readline library
# Disable readline, readline type code is all handled by PyDev (on Java side)
readline_use = CBool(False)
# autoindent has no meaning in PyDev (PyDev always handles that on the Java side),
# and attempting to enable it will print a warning in the absence of readline.
autoindent = CBool(False)
# Force console to not give warning about color scheme choice and default to NoColor.
# TODO It would be nice to enable colors in PyDev but:
# - The PyDev Console (Eclipse Console) does not support the full range of colors, so the
# effect isn't as nice anyway at the command line
# - If done, the color scheme should default to LightBG, but actually be dependent on
# any settings the user has (such as if a dark theme is in use, then Linux is probably
# a better theme).
colors_force = CBool(True)
colors = Unicode("NoColor")
# Since IPython 5 the terminal interface is not compatible with Emacs `inferior-shell` and
# the `simple_prompt` flag is needed
simple_prompt = CBool(True)
# In the PyDev Console, GUI control is done via hookable XML-RPC server
@staticmethod
def enable_gui(gui=None, app=None):
"""Switch amongst GUI input hooks by name."""
# Deferred import
from pydev_ipython.inputhook import enable_gui as real_enable_gui
try:
return real_enable_gui(gui, app)
except ValueError as e:
raise UsageError("%s" % e)
# -------------------------------------------------------------------------
# Things related to hooks
# -------------------------------------------------------------------------
def init_history(self):
# Disable history so that we don't have an additional thread for that
# (and we don't use the history anyways).
self.config.HistoryManager.enabled = False
super(PyDevTerminalInteractiveShell, self).init_history()
def init_hooks(self):
super(PyDevTerminalInteractiveShell, self).init_hooks()
self.set_hook("show_in_pager", show_in_pager)
# -------------------------------------------------------------------------
# Things related to exceptions
# -------------------------------------------------------------------------
def showtraceback(self, exc_tuple=None, *args, **kwargs):
# IPython does a lot of clever stuff with Exceptions. However mostly
# it is related to IPython running in a terminal instead of an IDE.
# (e.g. it prints out snippets of code around the stack trace)
# PyDev does a lot of clever stuff too, so leave exception handling
# with default print_exc that PyDev can parse and do its clever stuff
# with (e.g. it puts links back to the original source code)
try:
if exc_tuple is None:
etype, value, tb = sys.exc_info()
else:
etype, value, tb = exc_tuple
except ValueError:
return
if tb is not None:
traceback.print_exception(etype, value, tb)
# -------------------------------------------------------------------------
# Things related to text completion
# -------------------------------------------------------------------------
# The way to construct an IPCompleter changed in most versions,
# so we have a custom, per version implementation of the construction
def _new_completer_100(self):
completer = PyDevIPCompleter(
shell=self,
namespace=self.user_ns,
global_namespace=self.user_global_ns,
alias_table=self.alias_manager.alias_table,
use_readline=self.has_readline,
parent=self,
)
return completer
def _new_completer_234(self):
# correct for IPython versions 2.x, 3.x, 4.x
completer = PyDevIPCompleter(
shell=self,
namespace=self.user_ns,
global_namespace=self.user_global_ns,
use_readline=self.has_readline,
parent=self,
)
return completer
def _new_completer_500(self):
completer = PyDevIPCompleter(
shell=self, namespace=self.user_ns, global_namespace=self.user_global_ns, use_readline=False, parent=self
)
return completer
def _new_completer_600(self):
completer = PyDevIPCompleter6(
shell=self, namespace=self.user_ns, global_namespace=self.user_global_ns, use_readline=False, parent=self
)
return completer
def add_completer_hooks(self):
from IPython.core.completerlib import module_completer, magic_run_completer, cd_completer
try:
from IPython.core.completerlib import reset_completer
except ImportError:
# reset_completer was added for rel-0.13
reset_completer = None
self.configurables.append(self.Completer)
# Add custom completers to the basic ones built into IPCompleter
sdisp = self.strdispatchers.get("complete_command", StrDispatch())
self.strdispatchers["complete_command"] = sdisp
self.Completer.custom_completers = sdisp
self.set_hook("complete_command", module_completer, str_key="import")
self.set_hook("complete_command", module_completer, str_key="from")
self.set_hook("complete_command", magic_run_completer, str_key="%run")
self.set_hook("complete_command", cd_completer, str_key="%cd")
if reset_completer:
self.set_hook("complete_command", reset_completer, str_key="%reset")
def init_completer(self):
"""Initialize the completion machinery.
This creates a completer that provides the completions that are
IPython specific. We use this to supplement PyDev's core code
completions.
"""
# PyDev uses its own completer and custom hooks so that it uses
# most completions from PyDev's core completer which provides
# extra information.
# See getCompletions for where the two sets of results are merged
if IPythonRelease._version_major >= 6:
self.Completer = self._new_completer_600()
elif IPythonRelease._version_major >= 5:
self.Completer = self._new_completer_500()
elif IPythonRelease._version_major >= 2:
self.Completer = self._new_completer_234()
elif IPythonRelease._version_major >= 1:
self.Completer = self._new_completer_100()
if hasattr(self.Completer, "use_jedi"):
self.Completer.use_jedi = False
self.add_completer_hooks()
if IPythonRelease._version_major <= 3:
# Only configure readline if we truly are using readline. IPython can
# do tab-completion over the network, in GUIs, etc, where readline
# itself may be absent
if self.has_readline:
self.set_readline_completer()
# -------------------------------------------------------------------------
# Things related to aliases
# -------------------------------------------------------------------------
def init_alias(self):
# InteractiveShell defines alias's we want, but TerminalInteractiveShell defines
# ones we don't. So don't use super and instead go right to InteractiveShell
InteractiveShell.init_alias(self)
# -------------------------------------------------------------------------
# Things related to exiting
# -------------------------------------------------------------------------
def ask_exit(self):
"""Ask the shell to exit. Can be overiden and used as a callback."""
# TODO PyDev's console does not have support from the Python side to exit
# the console. If user forces the exit (with sys.exit()) then the console
# simply reports errors. e.g.:
# >>> import sys
# >>> sys.exit()
# Failed to create input stream: Connection refused
# >>>
# Console already exited with value: 0 while waiting for an answer.
# Error stream:
# Output stream:
# >>>
#
# Alternatively if you use the non-IPython shell this is what happens
# >>> exit()
# <type 'exceptions.SystemExit'>:None
# >>>
# <type 'exceptions.SystemExit'>:None
# >>>
#
super(PyDevTerminalInteractiveShell, self).ask_exit()
print("To exit the PyDev Console, terminate the console within IDE.")
# -------------------------------------------------------------------------
# Things related to magics
# -------------------------------------------------------------------------
def init_magics(self):
super(PyDevTerminalInteractiveShell, self).init_magics()
# TODO Any additional magics for PyDev?
InteractiveShellABC.register(PyDevTerminalInteractiveShell) # @UndefinedVariable
# =======================================================================================================================
# _PyDevFrontEnd
# =======================================================================================================================
class _PyDevFrontEnd:
version = release.__version__
def __init__(self):
# Create and initialize our IPython instance.
if hasattr(PyDevTerminalInteractiveShell, "_instance") and PyDevTerminalInteractiveShell._instance is not None:
self.ipython = PyDevTerminalInteractiveShell._instance
else:
self.ipython = PyDevTerminalInteractiveShell.instance()
self._curr_exec_line = 0
self._curr_exec_lines = []
def show_banner(self):
self.ipython.show_banner()
def update(self, globals, locals):
ns = self.ipython.user_ns
for key, value in list(ns.items()):
if key not in locals:
locals[key] = value
self.ipython.user_global_ns.clear()
self.ipython.user_global_ns.update(globals)
self.ipython.user_ns = locals
if hasattr(self.ipython, "history_manager") and hasattr(self.ipython.history_manager, "save_thread"):
self.ipython.history_manager.save_thread.pydev_do_not_trace = True # don't trace ipython history saving thread
def complete(self, string):
try:
if string:
return self.ipython.complete(None, line=string, cursor_pos=string.__len__())
else:
return self.ipython.complete(string, string, 0)
except:
# Silence completer exceptions
pass
def is_complete(self, string):
# Based on IPython 0.10.1
if string in ("", "\n"):
# Prefiltering, eg through ipython0, may return an empty
# string although some operations have been accomplished. We
# thus want to consider an empty string as a complete
# statement.
return True
else:
try:
# Add line returns here, to make sure that the statement is
# complete (except if '\' was used).
# This should probably be done in a different place (like
# maybe 'prefilter_input' method? For now, this works.
clean_string = string.rstrip("\n")
if not clean_string.endswith("\\"):
clean_string += "\n\n"
is_complete = codeop.compile_command(clean_string, "<string>", "exec")
except Exception:
# XXX: Hack: return True so that the
# code gets executed and the error captured.
is_complete = True
return is_complete
def getCompletions(self, text, act_tok):
# Get completions from IPython and from PyDev and merge the results
# IPython only gives context free list of completions, while PyDev
# gives detailed information about completions.
try:
TYPE_IPYTHON = "11"
TYPE_IPYTHON_MAGIC = "12"
_line, ipython_completions = self.complete(text)
from _pydev_bundle._pydev_completer import Completer
completer = Completer(self.get_namespace(), None)
ret = completer.complete(act_tok)
append = ret.append
ip = self.ipython
pydev_completions = set([f[0] for f in ret])
for ipython_completion in ipython_completions:
# PyCharm was not expecting completions with '%'...
# Could be fixed in the backend, but it's probably better
# fixing it at PyCharm.
# if ipython_completion.startswith('%'):
# ipython_completion = ipython_completion[1:]
if ipython_completion not in pydev_completions:
pydev_completions.add(ipython_completion)
inf = ip.object_inspect(ipython_completion)
if inf["type_name"] == "Magic function":
pydev_type = TYPE_IPYTHON_MAGIC
else:
pydev_type = TYPE_IPYTHON
pydev_doc = inf["docstring"]
if pydev_doc is None:
pydev_doc = ""
append((ipython_completion, pydev_doc, "", pydev_type))
return ret
except:
import traceback
traceback.print_exc()
return []
def get_namespace(self):
return self.ipython.user_ns
def clear_buffer(self):
del self._curr_exec_lines[:]
def add_exec(self, line):
if self._curr_exec_lines:
self._curr_exec_lines.append(line)
buf = "\n".join(self._curr_exec_lines)
if self.is_complete(buf):
self._curr_exec_line += 1
self.ipython.run_cell(buf)
del self._curr_exec_lines[:]
return False # execute complete (no more)
return True # needs more
else:
if not self.is_complete(line):
# Did not execute
self._curr_exec_lines.append(line)
return True # needs more
else:
self._curr_exec_line += 1
self.ipython.run_cell(line, store_history=True)
# hist = self.ipython.history_manager.output_hist_reprs
# rep = hist.get(self._curr_exec_line, None)
# if rep is not None:
# print(rep)
return False # execute complete (no more)
def is_automagic(self):
return self.ipython.automagic
def get_greeting_msg(self):
return "PyDev console: using IPython %s\n" % self.version
class _PyDevFrontEndContainer:
_instance = None
_last_host_port = None
def get_pydev_frontend(pydev_host, pydev_client_port):
if _PyDevFrontEndContainer._instance is None:
_PyDevFrontEndContainer._instance = _PyDevFrontEnd()
if _PyDevFrontEndContainer._last_host_port != (pydev_host, pydev_client_port):
_PyDevFrontEndContainer._last_host_port = pydev_host, pydev_client_port
# Back channel to PyDev to open editors (in the future other
# info may go back this way. This is the same channel that is
# used to get stdin, see StdIn in pydev_console_utils)
_PyDevFrontEndContainer._instance.ipython.hooks["editor"] = create_editor_hook(pydev_host, pydev_client_port)
# Note: setting the callback directly because setting it with set_hook would actually create a chain instead
# of ovewriting at each new call).
# _PyDevFrontEndContainer._instance.ipython.set_hook('editor', create_editor_hook(pydev_host, pydev_client_port))
return _PyDevFrontEndContainer._instance