From 5ef1976566c07cdbe8ee28da6039fecddde4f993 2014-10-20 22:29:36 From: Thomas Kluyver Date: 2014-10-20 22:29:36 Subject: [PATCH] Merge pull request #6123 from minrk/zmq-console-echo-other Support echoing output from other clients --- diff --git a/IPython/qt/base_frontend_mixin.py b/IPython/qt/base_frontend_mixin.py index d03be9b..d317a9c 100644 --- a/IPython/qt/base_frontend_mixin.py +++ b/IPython/qt/base_frontend_mixin.py @@ -136,15 +136,23 @@ class BaseFrontendMixin(object): handler = getattr(self, '_handle_' + msg_type, None) if handler: handler(msg) - - def _is_from_this_session(self, msg): - """ Returns whether a reply from the kernel originated from a request - from this frontend. - """ - session = self._kernel_client.session.session - parent = msg['parent_header'] - if not parent: - # if the message has no parent, assume it is meant for all frontends + + def from_here(self, msg): + """Return whether a message is from this session""" + session_id = self._kernel_client.session.session + return msg['parent_header'].get("session", session_id) == session_id + + def include_output(self, msg): + """Return whether we should include a given output message""" + if self._hidden: + return False + from_here = self.from_here(msg) + if msg['msg_type'] == 'execute_input': + # only echo inputs not from here + return self.include_other_output and not from_here + + if self.include_other_output: return True else: - return parent.get('session') == session + return from_here + diff --git a/IPython/qt/console/console_widget.py b/IPython/qt/console/console_widget.py index 7a41e5a..f8794df 100644 --- a/IPython/qt/console/console_widget.py +++ b/IPython/qt/console/console_widget.py @@ -519,7 +519,15 @@ class ConsoleWidget(MetaQObjectHasTraits('NewBase', (LoggingConfigurable, QtGui. #--------------------------------------------------------------------------- # 'ConsoleWidget' public interface #--------------------------------------------------------------------------- - + + include_other_output = Bool(False, config=True, + help="""Whether to include output from clients + other than this one sharing the same kernel. + + Outputs are not displayed until enter is pressed. + """ + ) + def can_copy(self): """ Returns whether text can be copied to the clipboard. """ diff --git a/IPython/qt/console/frontend_widget.py b/IPython/qt/console/frontend_widget.py index 63e399b..75a1825 100644 --- a/IPython/qt/console/frontend_widget.py +++ b/IPython/qt/console/frontend_widget.py @@ -348,7 +348,7 @@ class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin): #--------------------------------------------------------------------------- def _handle_clear_output(self, msg): """Handle clear output messages.""" - if not self._hidden and self._is_from_this_session(msg): + if include_output(msg): wait = msg['content'].get('wait', True) if wait: self._pending_clearoutput = True @@ -509,7 +509,7 @@ class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin): """ Handle display hook output. """ self.log.debug("execute_result: %s", msg.get('content', '')) - if not self._hidden and self._is_from_this_session(msg): + if self.include_output(msg): self.flush_clearoutput() text = msg['content']['data'] self._append_plain_text(text + '\n', before_prompt=True) @@ -518,7 +518,7 @@ class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin): """ Handle stdout, stderr, and stdin. """ self.log.debug("stream: %s", msg.get('content', '')) - if not self._hidden and self._is_from_this_session(msg): + if self.include_output(msg): self.flush_clearoutput() self.append_stream(msg['content']['text']) @@ -527,7 +527,7 @@ class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin): """ self.log.info("shutdown: %s", msg.get('content', '')) restart = msg.get('content', {}).get('restart', False) - if not self._hidden and not self._is_from_this_session(msg): + if not self._hidden and not self.from_here(msg): # got shutdown reply, request came from session other than ours if restart: # someone restarted the kernel, handle it diff --git a/IPython/qt/console/ipython_widget.py b/IPython/qt/console/ipython_widget.py index 4fb10c9..0b4b100 100644 --- a/IPython/qt/console/ipython_widget.py +++ b/IPython/qt/console/ipython_widget.py @@ -219,12 +219,30 @@ class IPythonWidget(FrontendWidget): items.append(cell) last_cell = cell self._set_history(items) + + def _insert_other_input(self, cursor, content): + """Insert function for input from other frontends""" + cursor.beginEditBlock() + start = cursor.position() + n = content.get('execution_count', 0) + cursor.insertText('\n') + self._insert_html(cursor, self._make_in_prompt(n)) + cursor.insertText(content['code']) + self._highlighter.rehighlightBlock(cursor.block()) + cursor.endEditBlock() + + def _handle_execute_input(self, msg): + """Handle an execute_input message""" + self.log.debug("execute_input: %s", msg.get('content', '')) + if self.include_output(msg): + self._append_custom(self._insert_other_input, msg['content'], before_prompt=True) + def _handle_execute_result(self, msg): """ Reimplemented for IPython-style "display hook". """ self.log.debug("execute_result: %s", msg.get('content', '')) - if not self._hidden and self._is_from_this_session(msg): + if self.include_output(msg): self.flush_clearoutput() content = msg['content'] prompt_number = content.get('execution_count', 0) @@ -246,7 +264,7 @@ class IPythonWidget(FrontendWidget): # For now, we don't display data from other frontends, but we # eventually will as this allows all frontends to monitor the display # data. But we need to figure out how to handle this in the GUI. - if not self._hidden and self._is_from_this_session(msg): + if self.include_output(msg): self.flush_clearoutput() data = msg['content']['data'] metadata = msg['content']['metadata'] diff --git a/IPython/qt/console/rich_ipython_widget.py b/IPython/qt/console/rich_ipython_widget.py index 60247e7..2105019 100644 --- a/IPython/qt/console/rich_ipython_widget.py +++ b/IPython/qt/console/rich_ipython_widget.py @@ -107,7 +107,7 @@ class RichIPythonWidget(IPythonWidget): def _handle_execute_result(self, msg): """ Overridden to handle rich data types, like SVG. """ - if not self._hidden and self._is_from_this_session(msg): + if self.include_output(msg): self.flush_clearoutput() content = msg['content'] prompt_number = content.get('execution_count', 0) @@ -146,7 +146,7 @@ class RichIPythonWidget(IPythonWidget): def _handle_display_data(self, msg): """ Overridden to handle rich data types, like SVG. """ - if not self._hidden and self._is_from_this_session(msg): + if self.include_output(msg): self.flush_clearoutput() data = msg['content']['data'] metadata = msg['content']['metadata'] diff --git a/IPython/terminal/console/interactiveshell.py b/IPython/terminal/console/interactiveshell.py index 9be3ed9..be16f3c 100644 --- a/IPython/terminal/console/interactiveshell.py +++ b/IPython/terminal/console/interactiveshell.py @@ -26,7 +26,7 @@ from IPython.core import release from IPython.utils.warn import warn, error from IPython.utils import io from IPython.utils.py3compat import string_types, input -from IPython.utils.traitlets import List, Enum, Any, Instance, Unicode, Float +from IPython.utils.traitlets import List, Enum, Any, Instance, Unicode, Float, Bool from IPython.utils.tempdir import NamedFileInTemporaryDirectory from IPython.terminal.interactiveshell import TerminalInteractiveShell @@ -212,8 +212,37 @@ class ZMQTerminalInteractiveShell(TerminalInteractiveShell): print(frame, file=io.stderr) self.execution_count = int(content["execution_count"] + 1) - - + + include_other_output = Bool(False, config=True, + help="""Whether to include output from clients + other than this one sharing the same kernel. + + Outputs are not displayed until enter is pressed. + """ + ) + other_output_prefix = Unicode("[remote] ", config=True, + help="""Prefix to add to outputs coming from clients other than this one. + + Only relevant if include_other_output is True. + """ + ) + + def from_here(self, msg): + """Return whether a message is from this session""" + return msg['parent_header'].get("session", self.session_id) == self.session_id + + def include_output(self, msg): + """Return whether we should include a given output message""" + from_here = self.from_here(msg) + if msg['msg_type'] == 'execute_input': + # only echo inputs not from here + return self.include_other_output and not from_here + + if self.include_other_output: + return True + else: + return from_here + def handle_iopub(self, msg_id=''): """Process messages on the IOPub channel @@ -227,7 +256,7 @@ class ZMQTerminalInteractiveShell(TerminalInteractiveShell): msg_type = sub_msg['header']['msg_type'] parent = sub_msg["parent_header"] - if parent.get("session", self.session_id) == self.session_id: + if self.include_output(sub_msg): if msg_type == 'status': self._execution_state = sub_msg["content"]["execution_state"] elif msg_type == 'stream': @@ -249,8 +278,11 @@ class ZMQTerminalInteractiveShell(TerminalInteractiveShell): print("\r", file=io.stdout, end="") self._pending_clearoutput = False self.execution_count = int(sub_msg["content"]["execution_count"]) + if not self.from_here(sub_msg): + sys.stdout.write(self.other_output_prefix) format_dict = sub_msg["content"]["data"] self.handle_rich_data(format_dict) + # taken from DisplayHook.__call__: hook = self.displayhook hook.start_displayhook() @@ -263,10 +295,20 @@ class ZMQTerminalInteractiveShell(TerminalInteractiveShell): data = sub_msg["content"]["data"] handled = self.handle_rich_data(data) if not handled: + if not self.from_here(sub_msg): + sys.stdout.write(self.other_output_prefix) # if it was an image, we handled it by now if 'text/plain' in data: print(data['text/plain']) - + + elif msg_type == 'execute_input': + content = sub_msg['content'] + self.execution_count = content['execution_count'] + if not self.from_here(sub_msg): + sys.stdout.write(self.other_output_prefix) + sys.stdout.write(self.prompt_manager.render('in')) + sys.stdout.write(content['code']) + elif msg_type == 'clear_output': if sub_msg["content"]["wait"]: self._pending_clearoutput = True