#pythonConsole.py #A part of NonVisual Desktop Access (NVDA) #This file is covered by the GNU General Public License. #See the file COPYING for more details. #Copyright (C) 2008-2013 NV Access Limited import watchdog """Provides an interactive Python console which can be run from within NVDA. To use, call L{initialize} to create a singleton instance of the console GUI. This can then be accessed externally as L{consoleUI}. """ import __builtin__ import os import code import sys import pydoc import re import itertools import rlcompleter import wx from baseObject import AutoPropertyObject import speech import queueHandler import api import gui from logHandler import log import braille class HelpCommand(object): """ Emulation of the 'help' command found in the Python interactive shell. """ _reprMessage=_("Type help(object) to get help about object.") def __repr__(self): return self._reprMessage def __call__(self,*args,**kwargs): return pydoc.help(*args,**kwargs) class ExitConsoleCommand(object): """ An object that can be used as an exit command that can close the console or print a friendly message for its repr. """ def __init__(self, exitFunc): self._exitFunc = exitFunc _reprMessage=_("Type exit() to exit the console") def __repr__(self): return self._reprMessage def __call__(self): self._exitFunc() #: The singleton Python console UI instance. consoleUI = None class Completer(rlcompleter.Completer): def _callable_postfix(self, val, word): # Just because something is callable doesn't always mean we want to call it. return word class PythonConsole(code.InteractiveConsole, AutoPropertyObject): """An interactive Python console for NVDA which directs output to supplied functions. This is necessary for a Python console with input/output other than stdin/stdout/stderr. Input is always received via the L{push} method. This console handles redirection of stdout and stderr and prevents clobbering of the gettext "_" builtin. The console's namespace is populated with useful modules and can be updated with a snapshot of NVDA's state using L{updateNamespaceSnapshotVars}. """ def __init__(self, outputFunc, setPromptFunc, exitFunc, echoFunc=None, **kwargs): self._output = outputFunc self._echo = echoFunc self._setPrompt = setPromptFunc #: The namespace available to the console. This can be updated externally. #: @type: dict # Populate with useful modules. exitCmd = ExitConsoleCommand(exitFunc) self.namespace = { "help": HelpCommand(), "exit": exitCmd, "quit": exitCmd, "sys": sys, "os": os, "wx": wx, "log": log, "api": api, "queueHandler": queueHandler, "speech": speech, "braille": braille, } #: The variables last added to the namespace containing a snapshot of NVDA's state. #: @type: dict self._namespaceSnapshotVars = None # Can't use super here because stupid code.InteractiveConsole doesn't sub-class object. Grrr! code.InteractiveConsole.__init__(self, locals=self.namespace, **kwargs) self.prompt = ">>>" def _set_prompt(self, prompt): self._prompt = prompt self._setPrompt(prompt) def _get_prompt(self): return self._prompt def write(self, data): self._output(data) def push(self, line): if self._echo: self._echo("%s %s\n" % (self.prompt, line)) # Capture stdout/stderr output as well as code interaction. stdout, stderr = sys.stdout, sys.stderr sys.stdout = sys.stderr = self # Prevent this from messing with the gettext "_" builtin. saved_ = __builtin__._ more = code.InteractiveConsole.push(self, line) sys.stdout, sys.stderr = stdout, stderr __builtin__._ = saved_ self.prompt = "..." if more else ">>>" return more def updateNamespaceSnapshotVars(self): """Update the console namespace with a snapshot of NVDA's current state. This creates/updates variables for the current focus, navigator object, etc. """ self._namespaceSnapshotVars = { "focus": api.getFocusObject(), # Copy the focus ancestor list, as it gets mutated once it is replaced in api.setFocusObject. "focusAnc": list(api.getFocusAncestors()), "fdl": api.getFocusDifferenceLevel(), "fg": api.getForegroundObject(), "nav": api.getNavigatorObject(), "review":api.getReviewPosition(), "mouse": api.getMouseObject(), "brlRegions": braille.handler.buffer.regions, } self.namespace.update(self._namespaceSnapshotVars) def removeNamespaceSnapshotVars(self): """Remove the variables from the console namespace containing the last snapshot of NVDA's state. This removes the variables added by L{updateNamespaceSnapshotVars}. """ if not self._namespaceSnapshotVars: return for key in self._namespaceSnapshotVars: try: del self.namespace[key] except KeyError: pass self._namespaceSnapshotVars = None class ConsoleUI(wx.Frame): """The NVDA Python console GUI. """ def __init__(self, parent): super(ConsoleUI, self).__init__(parent, wx.ID_ANY, _("NVDA Python Console")) self.Bind(wx.EVT_ACTIVATE, self.onActivate) self.Bind(wx.EVT_CLOSE, self.onClose) mainSizer = wx.BoxSizer(wx.VERTICAL) self.outputCtrl = wx.TextCtrl(self, wx.ID_ANY, size=(500, 500), style=wx.TE_MULTILINE | wx.TE_READONLY|wx.TE_RICH) self.outputCtrl.Bind(wx.EVT_KEY_DOWN, self.onOutputKeyDown) self.outputCtrl.Bind(wx.EVT_CHAR, self.onOutputChar) mainSizer.Add(self.outputCtrl, proportion=2, flag=wx.EXPAND) inputSizer = wx.BoxSizer(wx.HORIZONTAL) self.promptLabel = wx.StaticText(self, wx.ID_ANY) inputSizer.Add(self.promptLabel, flag=wx.EXPAND) self.inputCtrl = wx.TextCtrl(self, wx.ID_ANY, style=wx.TE_DONTWRAP | wx.TE_PROCESS_TAB) self.inputCtrl.Bind(wx.EVT_CHAR, self.onInputChar) inputSizer.Add(self.inputCtrl, proportion=1, flag=wx.EXPAND) mainSizer.Add(inputSizer, proportion=1, flag=wx.EXPAND) self.SetSizer(mainSizer) mainSizer.Fit(self) self.console = PythonConsole(outputFunc=self.output, echoFunc=self.echo, setPromptFunc=self.setPrompt, exitFunc=self.Close) self.completer = Completer(namespace=self.console.namespace) self.completionAmbiguous = False # Even the most recent line has a position in the history, so initialise with one blank line. self.inputHistory = [""] self.inputHistoryPos = 0 def onActivate(self, evt): if evt.GetActive(): self.inputCtrl.SetFocus() evt.Skip() def onClose(self, evt): self.Hide() self.console.removeNamespaceSnapshotVars() def output(self, data): self.outputCtrl.write(data) if data and not data.isspace(): queueHandler.queueFunction(queueHandler.eventQueue, speech.speakText, data) def echo(self, data): self.outputCtrl.write(data) def setPrompt(self, prompt): self.promptLabel.SetLabel(prompt) queueHandler.queueFunction(queueHandler.eventQueue, speech.speakText, prompt) def execute(self): data = self.inputCtrl.GetValue() watchdog.alive() self.console.push(data) watchdog.asleep() if data: # Only add non-blank lines to history. if len(self.inputHistory) > 1 and self.inputHistory[-2] == data: # The previous line was the same and we don't want consecutive duplicates, so trash the most recent line. del self.inputHistory[-1] else: # Update the content for the most recent line of history. self.inputHistory[-1] = data # Start with a new, blank line. self.inputHistory.append("") self.inputHistoryPos = len(self.inputHistory) - 1 self.inputCtrl.ChangeValue("") def historyMove(self, movement): newIndex = self.inputHistoryPos + movement if not (0 <= newIndex < len(self.inputHistory)): # No more lines in this direction. return False # Update the content of the history at the current position. self.inputHistory[self.inputHistoryPos] = self.inputCtrl.GetValue() self.inputHistoryPos = newIndex self.inputCtrl.ChangeValue(self.inputHistory[newIndex]) self.inputCtrl.SetInsertionPointEnd() return True RE_COMPLETE_UNIT = re.compile(r"[\w.]*$") def complete(self): try: original = self.RE_COMPLETE_UNIT.search(self.inputCtrl.GetValue()).group(0) except AttributeError: return False completions = list(self._getCompletions(original)) if self.completionAmbiguous: menu = wx.Menu() for comp in completions: item = menu.Append(wx.ID_ANY, comp) self.Bind(wx.EVT_MENU, lambda evt, completion=comp: self._insertCompletion(original, completion), item) self.PopupMenu(menu) menu.Destroy() return True self.completionAmbiguous = len(completions) > 1 completed = self._findBestCompletion(original, completions) if not completed: return False self._insertCompletion(original, completed) return not self.completionAmbiguous def _getCompletions(self, original): for state in itertools.count(): completion = self.completer.complete(original, state) if not completion: break yield completion def _findBestCompletion(self, original, completions): if not completions: return None if len(completions) == 1: return completions[0] # Find the longest completion. longestComp = None longestCompLen = 0 for comp in completions: compLen = len(comp) if compLen > longestCompLen: longestComp = comp longestCompLen = compLen # Find the longest common prefix. for prefixLen in xrange(longestCompLen, 0, -1): prefix = comp[:prefixLen] for comp in completions: if not comp.startswith(prefix): break else: # This prefix is common to all completions. if prefix == original: # We didn't actually complete anything. return None return prefix return None def _insertCompletion(self, original, completed): self.completionAmbiguous = False insert = completed[len(original):] if not insert: return self.inputCtrl.SetValue(self.inputCtrl.GetValue() + insert) queueHandler.queueFunction(queueHandler.eventQueue, speech.speakText, insert) self.inputCtrl.SetInsertionPointEnd() def onInputChar(self, evt): key = evt.GetKeyCode() if key == wx.WXK_TAB: line = self.inputCtrl.GetValue() if line and not line.isspace(): if not self.complete(): wx.Bell() return # This is something other than autocompletion, so reset autocompletion state. self.completionAmbiguous = False if key == wx.WXK_RETURN: self.execute() return elif key in (wx.WXK_UP, wx.WXK_DOWN): if self.historyMove(-1 if key == wx.WXK_UP else 1): return elif key == wx.WXK_F6: self.outputCtrl.SetFocus() return elif key == wx.WXK_ESCAPE: self.Close() return evt.Skip() def onOutputKeyDown(self, evt): key = evt.GetKeyCode() # #3763: WX 3 no longer passes escape to evt_char for richEdit fields, therefore evt_key_down is used. if key == wx.WXK_ESCAPE: self.Close() return evt.Skip() def onOutputChar(self, evt): key = evt.GetKeyCode() if key == wx.WXK_F6: self.inputCtrl.SetFocus() return evt.Skip() def initialize(): """Initialize the NVDA Python console GUI. This creates a singleton instance of the console GUI. This is accessible as L{consoleUI}. This may be manipulated externally. """ global consoleUI consoleUI = ConsoleUI(gui.mainFrame) def activate(): """Activate the console GUI. This shows the GUI and brings it to the foreground if possible. @precondition: L{initialize} has been called. """ global consoleUI consoleUI.Raise() # There is a MAXIMIZE style which can be used on the frame at construction, but it doesn't seem to work the first time it is shown, # probably because it was in the background. # Therefore, explicitly maximise it here. # This also ensures that it will be maximized whenever it is activated, even if the user restored/minimised it. consoleUI.Maximize() consoleUI.Show()