class PydocServer(QThread): """Pydoc server""" server_started = Signal() def __init__(self, port=7464): QThread.__init__(self) self.port = port self.server = None self.complete = False def run(self): import pydoc if PY3: # Python 3 self.callback(pydoc._start_server(pydoc._url_handler, self.port)) else: # Python 2 pydoc.serve(self.port, self.callback, self.completer) def callback(self, server): self.server = server self.server_started.emit() def completer(self): self.complete = True def quit_server(self): if PY3: # Python 3 if self.server.serving: self.server.stop() else: # Python 2 self.server.quit = 1
class SysOutput(QObject): """Handle standard I/O queue""" data_avail = Signal() def __init__(self): QObject.__init__(self) self.queue = [] self.lock = threading.Lock() def write(self, val): self.lock.acquire() self.queue.append(val) self.lock.release() self.data_avail.emit() def empty_queue(self): self.lock.acquire() s = "".join(self.queue) self.queue = [] self.lock.release() return s # We need to add this method to fix Issue 1789 def flush(self): pass
class FormComboWidget(QWidget): update_buttons = Signal() def __init__(self, datalist, comment="", parent=None): QWidget.__init__(self, parent) layout = QVBoxLayout() self.setLayout(layout) self.combobox = QComboBox() layout.addWidget(self.combobox) self.stackwidget = QStackedWidget(self) layout.addWidget(self.stackwidget) self.combobox.currentIndexChanged.connect( self.stackwidget.setCurrentIndex) self.widgetlist = [] for data, title, comment in datalist: self.combobox.addItem(title) widget = FormWidget(data, comment=comment, parent=self) self.stackwidget.addWidget(widget) self.widgetlist.append(widget) def setup(self): for widget in self.widgetlist: widget.setup() def get(self): return [ widget.get() for widget in self.widgetlist]
class FrameWebView(QFrame): """ Framed QWebView for UI consistency in Spyder. """ linkClicked = Signal(QUrl) def __init__(self, parent): QFrame.__init__(self, parent) self._webview = WebView(self) layout = QHBoxLayout() layout.addWidget(self._webview) layout.setContentsMargins(0, 0, 0, 0) self.setLayout(layout) self.setFrameStyle(QFrame.StyledPanel | QFrame.Sunken) self._webview.linkClicked.connect(self.linkClicked) def set_font(self, font, fixed_font=None): self._webview.set_font(font, fixed_font=fixed_font) def setHtml(self, html_text, base_url): self._webview.setHtml(html_text, base_url) def url(self): return self._webview.url() def load(self, url): self._webview.load(url) def page(self): return self._webview.page()
class RequestHandler(QObject): """Handle introspection request. """ introspection_complete = Signal() def __init__(self, code_info, plugins): super(RequestHandler, self).__init__() self.info = code_info self.timer = QTimer() self.timer.singleShot(LEAD_TIME_SEC * 1000, self._handle_timeout) self.waiting = True self.pending = {} self.result = None self.plugins = plugins self._start_time = time.time() self._threads = {} for plugin in plugins: self._make_async_call(plugin, code_info) def _handle_timeout(self): debug_print('got timeout: %s' % self.plugins) if self.pending: for plugin in self.plugins: if plugin.name in self.pending: self._finalize(plugin.name, self.pending[plugin.name]) return self.waiting = False def _handle_incoming(self, name): # coerce to a str in case it is a QString name = str(name) self._threads[name].wait() if self.result: return result = self._threads[name].result if name == self.plugins[0].name or not self.waiting: if result: self._finalize(name, result) else: debug_print('No valid responses acquired') self.introspection_complete.emit() else: self.pending[name] = result def _make_async_call(self, plugin, info): """Trigger an introspection job in a thread""" self._threads[str(plugin.name)] = thread = IntrospectionThread( plugin, info) thread.request_handled.connect(self._handle_incoming) thread.start() def _finalize(self, name, result): self.result = result self.waiting = False self.pending = None delta = time.time() - self._start_time debug_print('%s request from %s finished: "%s" in %.1f sec' % (self.info.name, name, str(result)[:100], delta)) self.introspection_complete.emit()
class PathComboBox(EditableComboBox): """ QComboBox handling path locations """ open_dir = Signal(str) def __init__(self, parent, adjust_to_contents=False): EditableComboBox.__init__(self, parent) if adjust_to_contents: self.setSizeAdjustPolicy(QComboBox.AdjustToContents) else: self.setSizeAdjustPolicy(QComboBox.AdjustToMinimumContentsLength) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.tips = { True: _("Press enter to validate this path"), False: _('This path is incorrect.\n' 'Enter a correct directory path,\n' 'then press enter to validate') } def is_valid(self, qstr=None): """Return True if string is valid""" if qstr is None: qstr = self.currentText() return osp.isdir(to_text_string(qstr)) def selected(self): """Action to be executed when a valid item has been selected""" EditableComboBox.selected(self) self.open_dir.emit(self.currentText())
class ColorButton(QPushButton): """ Color choosing push button """ colorChanged = Signal(QColor) def __init__(self, parent=None): QPushButton.__init__(self, parent) self.setFixedSize(20, 20) self.setIconSize(QSize(12, 12)) self.clicked.connect(self.choose_color) self._color = QColor() def choose_color(self): color = QColorDialog.getColor(self._color, self.parentWidget(), 'Select Color', QColorDialog.ShowAlphaChannel) if color.isValid(): self.set_color(color) def get_color(self): return self._color @Slot(QColor) def set_color(self, color): if color != self._color: self._color = color self.colorChanged.emit(self._color) pixmap = QPixmap(self.iconSize()) pixmap.fill(color) self.setIcon(QIcon(pixmap)) color = Property("QColor", get_color, set_color)
class FormTabWidget(QWidget): update_buttons = Signal() def __init__(self, datalist, comment="", parent=None): QWidget.__init__(self, parent) layout = QVBoxLayout() self.tabwidget = QTabWidget() layout.addWidget(self.tabwidget) self.setLayout(layout) self.widgetlist = [] for data, title, comment in datalist: if len(data[0]) == 3: widget = FormComboWidget(data, comment=comment, parent=self) else: widget = FormWidget(data, comment=comment, parent=self) index = self.tabwidget.addTab(widget, title) self.tabwidget.setTabToolTip(index, comment) self.widgetlist.append(widget) def setup(self): for widget in self.widgetlist: widget.setup() def get(self): return [widget.get() for widget in self.widgetlist]
class KeyPressFilter(QObject): """ Use with `installEventFilter` to get up/down arrow key press signal. """ UP, DOWN = [-1, 1] # Step constants sig_up_key_pressed = Signal() sig_down_key_pressed = Signal() def eventFilter(self, src, e): if e.type() == QEvent.KeyPress: if e.key() == Qt.Key_Up: self.sig_up_key_pressed.emit() elif e.key() == Qt.Key_Down: self.sig_down_key_pressed.emit() return super(KeyPressFilter, self).eventFilter(src, e)
class SpyderPluginWidget(QWidget, SpyderPluginMixin): """ Spyder base widget class Spyder's widgets either inherit this class or reimplement its interface """ sig_option_changed = Signal(str, object) def __init__(self, parent): QWidget.__init__(self, parent) SpyderPluginMixin.__init__(self, parent) def get_plugin_title(self): """ Return plugin title Note: after some thinking, it appears that using a method is more flexible here than using a class attribute """ raise NotImplementedError def get_plugin_icon(self): """ Return plugin icon (QIcon instance) Note: this is required for plugins creating a main window (see SpyderPluginMixin.create_mainwindow) and for configuration dialog widgets creation """ return get_icon('qt.png') def get_focus_widget(self): """ Return the widget to give focus to when this plugin's dockwidget is raised on top-level """ pass def closing_plugin(self, cancelable=False): """ Perform actions before parent main window is closed Return True or False whether the plugin may be closed immediately or not Note: returned value is ignored if *cancelable* is False """ raise NotImplementedError def refresh_plugin(self): """Refresh widget""" raise NotImplementedError def get_plugin_actions(self): """ Return a list of actions related to plugin Note: these actions will be enabled when plugin's dockwidget is visible and they will be disabled when it's hidden """ raise NotImplementedError def register_plugin(self): """Register plugin in Spyder's main window""" raise NotImplementedError
class ObjectComboBox(EditableComboBox): """ QComboBox handling object names """ # Signals valid = Signal(bool) def __init__(self, parent): EditableComboBox.__init__(self, parent) self.object_inspector = parent self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.tips = {True: '', False: ''} def is_valid(self, qstr=None): """Return True if string is valid""" if not self.object_inspector.source_is_console(): return True if qstr is None: qstr = self.currentText() if not re.search('^[a-zA-Z0-9_\.]*$', str(qstr), 0): return False objtxt = to_text_string(qstr) if self.object_inspector.get_option('automatic_import'): shell = self.object_inspector.internal_shell if shell is not None: return shell.is_defined(objtxt, force_import=True) shell = self.object_inspector.get_shell() if shell is not None: try: return shell.is_defined(objtxt) except socket.error: shell = self.object_inspector.get_shell() try: return shell.is_defined(objtxt) except socket.error: # Well... too bad! pass def validate_current_text(self): self.validate(self.currentText()) def validate(self, qstr, editing=True): """Reimplemented to avoid formatting actions""" valid = self.is_valid(qstr) if self.hasFocus() and valid is not None: if editing: # Combo box text is being modified: invalidate the entry self.show_tip(self.tips[valid]) self.valid.emit(False) else: # A new item has just been selected if valid: self.selected() else: self.valid.emit(False) else: self.set_default_style()
class BreakpointWidget(QWidget): """ Breakpoint widget """ VERSION = '1.0.0' clear_all_breakpoints = Signal() set_or_edit_conditional_breakpoint = Signal() clear_breakpoint = Signal(str, int) edit_goto = Signal(str, int, str) def __init__(self, parent): QWidget.__init__(self, parent) self.setWindowTitle("Breakpoints") self.dictwidget = BreakpointTableView(self, self._load_all_breakpoints()) layout = QVBoxLayout() layout.addWidget(self.dictwidget) self.setLayout(layout) self.dictwidget.clear_all_breakpoints.connect( lambda: self.clear_all_breakpoints.emit()) self.dictwidget.clear_breakpoint.connect( lambda s1, lino: self.clear_breakpoint.emit(s1, lino)) self.dictwidget.edit_goto.connect( lambda s1, lino, s2: self.edit_goto.emit(s1, lino, s2)) self.dictwidget.set_or_edit_conditional_breakpoint.connect( lambda: self.set_or_edit_conditional_breakpoint.emit()) def _load_all_breakpoints(self): bp_dict = CONF.get('run', 'breakpoints', {}) for filename in list(bp_dict.keys()): if not osp.isfile(filename): bp_dict.pop(filename) return bp_dict def get_data(self): pass def set_data(self): bp_dict = self._load_all_breakpoints() self.dictwidget.model.set_data(bp_dict) self.dictwidget.adjust_columns() self.dictwidget.sortByColumn(0, Qt.DescendingOrder)
class MacApplication(QApplication): """Subclass to be able to open external files with our Mac app""" open_external_file = Signal(str) def __init__(self, *args): QApplication.__init__(self, *args) def event(self, event): if event.type() == QEvent.FileOpen: fname = str(event.file()) self.open_external_file.emit(fname) return QApplication.event(self, event)
class BaseRunConfigDialog(QDialog): """Run configuration dialog box, base widget""" size_change = Signal(QSize) def __init__(self, parent=None): QDialog.__init__(self, parent) # Destroying the C++ object right after closing the dialog box, # otherwise it may be garbage-collected in another QThread # (e.g. the editor's analysis thread in Spyder), thus leading to # a segmentation fault on UNIX or an application crash on Windows self.setAttribute(Qt.WA_DeleteOnClose) self.setWindowIcon(get_icon("run_settings.png")) layout = QVBoxLayout() self.setLayout(layout) def add_widgets(self, *widgets_or_spacings): """Add widgets/spacing to dialog vertical layout""" layout = self.layout() for widget_or_spacing in widgets_or_spacings: if isinstance(widget_or_spacing, int): layout.addSpacing(widget_or_spacing) else: layout.addWidget(widget_or_spacing) def add_button_box(self, stdbtns): """Create dialog button box and add it to the dialog layout""" bbox = QDialogButtonBox(stdbtns) run_btn = bbox.addButton(_("Run"), QDialogButtonBox.AcceptRole) run_btn.clicked.connect(self.run_btn_clicked) bbox.accepted.connect(self.accept) bbox.rejected.connect(self.reject) btnlayout = QHBoxLayout() btnlayout.addStretch(1) btnlayout.addWidget(bbox) self.layout().addLayout(btnlayout) def resizeEvent(self, event): """ Reimplement Qt method to be able to save the widget's size from the main application """ QDialog.resizeEvent(self, event) self.size_change.emit(self.size()) def run_btn_clicked(self): """Run button was just clicked""" pass def setup(self, fname): """Setup Run Configuration dialog with filename *fname*""" raise NotImplementedError
class PlainText(QWidget): """ Read-only editor widget with find dialog """ # Signals focus_changed = Signal() def __init__(self, parent): QWidget.__init__(self, parent) self.editor = None # Read-only editor self.editor = codeeditor.CodeEditor(self) self.editor.setup_editor(linenumbers=False, language='py', scrollflagarea=False, edge_line=False) self.editor.focus_changed.connect(lambda: self.focus_changed.emit()) self.editor.setReadOnly(True) # Find/replace widget self.find_widget = FindReplace(self) self.find_widget.set_editor(self.editor) self.find_widget.hide() layout = QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(self.editor) layout.addWidget(self.find_widget) self.setLayout(layout) def set_font(self, font, color_scheme=None): """Set font""" self.editor.set_font(font, color_scheme=color_scheme) def set_color_scheme(self, color_scheme): """Set color scheme""" self.editor.set_color_scheme(color_scheme) def set_text(self, text, is_code): self.editor.set_highlight_current_line(is_code) self.editor.set_occurence_highlighting(is_code) if is_code: self.editor.set_language('py') else: self.editor.set_language(None) self.editor.set_text(text) self.editor.set_cursor_position('sof') def clear(self): self.editor.clear()
class PluginListener(QThread): """A plugin response listener. """ # Emitted when the plugin has intitialized. initialized = Signal(int) # Emitted when a request response has been received. request_handled = Signal(object) def __init__(self, sock): super(PluginListener, self).__init__() self.sock = sock self._initialized = False def run(self): while True: try: conn, _addr = self.sock.accept() except socket.error as e: badfd = errno.WSAEBADF if os.name == 'nt' else errno.EBADF extra = errno.WSAENOTSOCK if os.name == 'nt' else badfd if e.args[0] in [errno.ECONNABORTED, badfd, extra]: return # See Issue 1275 for details on why errno EINTR is # silently ignored here. eintr = errno.WSAEINTR if os.name == 'nt' else errno.EINTR if e.args[0] == eintr: continue raise if not self._initialized: server_port = read_packet(conn) if isinstance(server_port, int): self._initialized = True self.initialized.emit(server_port) else: self.request_handled.emit(read_packet(conn))
class SpyderDockWidget(QDockWidget): """Subclass to override needed methods""" plugin_closed = Signal() def __init__(self, title, parent): super(SpyderDockWidget, self).__init__(title, parent) # Needed for the installation of the event filter self.title = title self.main = parent self.dock_tabbar = None # To track dockwidget changes the filter is installed when dockwidget # visibility changes. This installs the filter on startup and also # on dockwidgets that are undocked and then docked to a new location. self.visibilityChanged.connect(self.install_tab_event_filter) def closeEvent(self, event): """ Reimplement Qt method to send a signal on close so that "Panes" main window menu can be updated correctly """ self.plugin_closed.emit() def install_tab_event_filter(self, value): """ Install an event filter to capture mouse events in the tabs of a QTabBar holding tabified dockwidgets. """ dock_tabbar = None tabbars = self.main.findChildren(QTabBar) for tabbar in tabbars: for tab in range(tabbar.count()): title = tabbar.tabText(tab) if title == self.title: dock_tabbar = tabbar break if dock_tabbar is not None: self.dock_tabbar = dock_tabbar # Install filter only once per QTabBar if getattr(self.dock_tabbar, 'filter', None) is None: self.dock_tabbar.filter = TabFilter(self.dock_tabbar, self.main) self.dock_tabbar.installEventFilter(self.dock_tabbar.filter)
class IntrospectionThread(QThread): """ A thread to perform an introspection task """ request_handled = Signal(str) def __init__(self, plugin, info): super(IntrospectionThread, self).__init__() self.plugin = plugin self.info = info self.result = None def run(self): func = getattr(self.plugin, 'get_%s' % self.info.name) self.plugin.busy = True try: self.result = func(self.info) except Exception as e: debug_print(e) self.plugin.busy = False self.request_handled.emit(self.plugin.name)
class ExtPythonShellWidget(PythonShellWidget): wait_for_ready_read = Signal() go_to_error = Signal(str) focus_changed = Signal() def __init__(self, parent, history_filename, profile=False): PythonShellWidget.__init__(self, parent, history_filename, profile) self.path = [] def set_externalshell(self, externalshell): # ExternalShellBase instance: self.externalshell = externalshell def clear_terminal(self): """Reimplement ShellBaseWidget method""" self.clear() self.execute.emit("\n") def execute_lines(self, lines): """ Execute a set of lines as multiple command lines: multiple lines of text to be executed as single commands """ for line in lines.splitlines(): stripped_line = line.strip() if stripped_line.startswith('#'): continue self.write(line + os.linesep, flush=True) self.execute_command(line) # Workaround for Issue 502 # Emmiting wait_for_ready_read was making the console hang # in Mac OS X if sys.platform.startswith("darwin"): import time time.sleep(0.025) else: self.wait_for_ready_read.emit() self.flush() #------ Code completion / Calltips def ask_monitor(self, command, settings=[]): sock = self.externalshell.introspection_socket if sock is None: return try: return communicate(sock, command, settings=settings) except socket.error: # Process was just closed pass except MemoryError: # Happens when monitor is not ready on slow machines pass def get_dir(self, objtxt): """Return dir(object)""" return self.ask_monitor("__get_dir__('%s')" % objtxt) def get_globals_keys(self): """Return shell globals() keys""" return self.ask_monitor("get_globals_keys()") def get_cdlistdir(self): """Return shell current directory list dir""" return self.ask_monitor("getcdlistdir()") def iscallable(self, objtxt): """Is object callable?""" return self.ask_monitor("__iscallable__('%s')" % objtxt) def get_arglist(self, objtxt): """Get func/method argument list""" return self.ask_monitor("__get_arglist__('%s')" % objtxt) def get__doc__(self, objtxt): """Get object __doc__""" return self.ask_monitor("__get__doc____('%s')" % objtxt) def get_doc(self, objtxt): """Get object documentation dictionary""" return self.ask_monitor("__get_doc__('%s')" % objtxt) def get_source(self, objtxt): """Get object source""" return self.ask_monitor("__get_source__('%s')" % objtxt) def is_defined(self, objtxt, force_import=False): """Return True if object is defined""" return self.ask_monitor("isdefined('%s', force_import=%s)" % (objtxt, force_import)) def get_module_completion(self, objtxt): """Return module completion list associated to object name""" return self.ask_monitor("getmodcomplist('%s', %s)" % \ (objtxt, self.path)) def get_cwd(self): """Return shell current working directory""" return self.ask_monitor("getcwd()") def set_cwd(self, dirname): """Set shell current working directory""" return self.ask_monitor("setcwd(r'%s')" % dirname) def get_env(self): """Return environment variables: os.environ""" return self.ask_monitor("getenv()") def set_env(self, env): """Set environment variables via os.environ""" return self.ask_monitor('setenv()', settings=[env]) def get_syspath(self): """Return sys.path[:]""" return self.ask_monitor("getsyspath()") def set_spyder_breakpoints(self): """Set Spyder breakpoints into debugging session""" return self.ask_monitor("set_spyder_breakpoints()")
class ExternalPythonShell(ExternalShellBase): """External Shell widget: execute Python script in a separate process""" SHELL_CLASS = ExtPythonShellWidget sig_pdb = Signal(str, int) open_file = Signal(str, int) ipython_kernel_start_error = Signal(str) create_ipython_client = Signal(str) started = Signal() sig_finished = Signal() def __init__(self, parent=None, fname=None, wdir=None, interact=False, debug=False, post_mortem=False, path=[], python_args='', ipykernel=False, arguments='', stand_alone=None, umr_enabled=True, umr_namelist=[], umr_verbose=True, pythonstartup=None, pythonexecutable=None, monitor_enabled=True, mpl_backend=None, ets_backend='qt4', qt_api=None, pyqt_api=0, ignore_sip_setapi_errors=False, merge_output_channels=False, colorize_sys_stderr=False, autorefresh_timeout=3000, autorefresh_state=True, light_background=True, menu_actions=None, show_buttons_inside=True, show_elapsed_time=True): assert qt_api in (None, 'pyqt', 'pyside') self.namespacebrowser = None # namespace browser widget! self.dialog_manager = DialogManager() self.stand_alone = stand_alone # stand alone settings (None: plugin) self.interact = interact self.is_ipykernel = ipykernel self.pythonstartup = pythonstartup self.pythonexecutable = pythonexecutable self.monitor_enabled = monitor_enabled self.mpl_backend = mpl_backend self.ets_backend = ets_backend self.qt_api = qt_api self.pyqt_api = pyqt_api self.ignore_sip_setapi_errors = ignore_sip_setapi_errors self.merge_output_channels = merge_output_channels self.colorize_sys_stderr = colorize_sys_stderr self.umr_enabled = umr_enabled self.umr_namelist = umr_namelist self.umr_verbose = umr_verbose self.autorefresh_timeout = autorefresh_timeout self.autorefresh_state = autorefresh_state self.namespacebrowser_button = None self.cwd_button = None self.env_button = None self.syspath_button = None self.terminate_button = None self.notification_thread = None ExternalShellBase.__init__(self, parent=parent, fname=fname, wdir=wdir, history_filename='history.py', light_background=light_background, menu_actions=menu_actions, show_buttons_inside=show_buttons_inside, show_elapsed_time=show_elapsed_time) if self.pythonexecutable is None: self.pythonexecutable = get_python_executable() self.python_args = None if python_args: assert is_text_string(python_args) self.python_args = python_args assert is_text_string(arguments) self.arguments = arguments self.connection_file = None if self.is_ipykernel: self.interact = False # Running our custom startup script for IPython kernels: # (see spyderlib/widgets/externalshell/start_ipython_kernel.py) self.fname = get_module_source_path( 'spyderlib.widgets.externalshell', 'start_ipython_kernel.py') self.shell.set_externalshell(self) self.toggle_globals_explorer(False) self.interact_action.setChecked(self.interact) self.debug_action.setChecked(debug) self.introspection_socket = None self.is_interpreter = fname is None if self.is_interpreter: self.terminate_button.hide() self.post_mortem_action.setChecked(post_mortem and not self.is_interpreter) # Additional python path list self.path = path self.shell.path = path def set_introspection_socket(self, introspection_socket): self.introspection_socket = introspection_socket if self.namespacebrowser is not None: settings = self.namespacebrowser.get_view_settings() communicate(introspection_socket, 'set_remote_view_settings()', settings=[settings]) def set_autorefresh_timeout(self, interval): if self.introspection_socket is not None: try: communicate(self.introspection_socket, "set_monitor_timeout(%d)" % interval) except socket.error: pass def closeEvent(self, event): self.quit_monitor() ExternalShellBase.closeEvent(self, event) def get_toolbar_buttons(self): ExternalShellBase.get_toolbar_buttons(self) if self.namespacebrowser_button is None \ and self.stand_alone is not None: self.namespacebrowser_button = create_toolbutton( self, text=_("Variables"), icon=get_icon('dictedit.png'), tip=_("Show/hide global variables explorer"), toggled=self.toggle_globals_explorer, text_beside_icon=True) if self.terminate_button is None: self.terminate_button = create_toolbutton( self, text=_("Terminate"), icon=get_icon('stop.png'), tip=_("Attempts to stop the process. The process\n" "may not exit as a result of clicking this\n" "button (it is given the chance to prompt\n" "the user for any unsaved files, etc).")) buttons = [] if self.namespacebrowser_button is not None: buttons.append(self.namespacebrowser_button) buttons += [ self.run_button, self.terminate_button, self.kill_button, self.options_button ] return buttons def get_options_menu(self): ExternalShellBase.get_options_menu(self) self.interact_action = create_action(self, _("Interact")) self.interact_action.setCheckable(True) self.debug_action = create_action(self, _("Debug")) self.debug_action.setCheckable(True) self.args_action = create_action(self, _("Arguments..."), triggered=self.get_arguments) self.post_mortem_action = create_action(self, _("Post Mortem Debug")) self.post_mortem_action.setCheckable(True) run_settings_menu = QMenu(_("Run settings"), self) add_actions(run_settings_menu, (self.interact_action, self.debug_action, self.args_action, self.post_mortem_action)) self.cwd_button = create_action( self, _("Working directory"), icon=get_std_icon('DirOpenIcon'), tip=_("Set current working directory"), triggered=self.set_current_working_directory) self.env_button = create_action(self, _("Environment variables"), icon=get_icon('environ.png'), triggered=self.show_env) self.syspath_button = create_action(self, _("Show sys.path contents"), icon=get_icon('syspath.png'), triggered=self.show_syspath) actions = [ run_settings_menu, self.show_time_action, None, self.cwd_button, self.env_button, self.syspath_button ] if self.menu_actions is not None: actions += [None] + self.menu_actions return actions def is_interpreter(self): """Return True if shellwidget is a Python interpreter""" return self.is_interpreter def get_shell_widget(self): if self.stand_alone is None: return self.shell else: self.namespacebrowser = NamespaceBrowser(self) settings = self.stand_alone self.namespacebrowser.set_shellwidget(self) self.namespacebrowser.setup(**settings) self.namespacebrowser.sig_collapse.connect( lambda: self.toggle_globals_explorer(False)) # Shell splitter self.splitter = splitter = QSplitter(Qt.Vertical, self) self.splitter.splitterMoved.connect(self.splitter_moved) splitter.addWidget(self.shell) splitter.setCollapsible(0, False) splitter.addWidget(self.namespacebrowser) splitter.setStretchFactor(0, 1) splitter.setStretchFactor(1, 0) splitter.setHandleWidth(5) splitter.setSizes([2, 1]) return splitter def get_icon(self): return get_icon('python.png') def set_buttons_runnning_state(self, state): ExternalShellBase.set_buttons_runnning_state(self, state) self.interact_action.setEnabled(not state and not self.is_interpreter) self.debug_action.setEnabled(not state and not self.is_interpreter) self.args_action.setEnabled(not state and not self.is_interpreter) self.post_mortem_action.setEnabled(not state and not self.is_interpreter) if state: if self.arguments: argstr = _("Arguments: %s") % self.arguments else: argstr = _("No argument") else: argstr = _("Arguments...") self.args_action.setText(argstr) self.terminate_button.setVisible(not self.is_interpreter and state) if not state: self.toggle_globals_explorer(False) for btn in (self.cwd_button, self.env_button, self.syspath_button): btn.setEnabled(state and self.monitor_enabled) if self.namespacebrowser_button is not None: self.namespacebrowser_button.setEnabled(state) def set_namespacebrowser(self, namespacebrowser): """ Set namespace browser *widget* Note: this method is not used in stand alone mode """ self.namespacebrowser = namespacebrowser self.configure_namespacebrowser() def configure_namespacebrowser(self): """Connect the namespace browser to the notification thread""" if self.notification_thread is not None: self.notification_thread.refresh_namespace_browser.connect( self.namespacebrowser.refresh_table) signal = self.notification_thread.sig_process_remote_view signal.connect( lambda data: self.namespacebrowser.process_remote_view(data)) def create_process(self): self.shell.clear() self.process = QProcess(self) if self.merge_output_channels: self.process.setProcessChannelMode(QProcess.MergedChannels) else: self.process.setProcessChannelMode(QProcess.SeparateChannels) self.shell.wait_for_ready_read.connect( lambda: self.process.waitForReadyRead(250)) # Working directory if self.wdir is not None: self.process.setWorkingDirectory(self.wdir) #-------------------------Python specific------------------------------ # Python arguments p_args = ['-u'] if DEBUG >= 3: p_args += ['-v'] p_args += get_python_args(self.fname, self.python_args, self.interact_action.isChecked(), self.debug_action.isChecked(), self.arguments) env = [ to_text_string(_path) for _path in self.process.systemEnvironment() ] if self.pythonstartup: env.append('PYTHONSTARTUP=%s' % self.pythonstartup) #-------------------------Python specific------------------------------- # Post mortem debugging if self.post_mortem_action.isChecked(): env.append('SPYDER_EXCEPTHOOK=True') # Set standard input/output encoding for Python consoles # (IPython handles it on its own) # See http://stackoverflow.com/q/26312400/438386, specifically # the comments of Martijn Pieters if not self.is_ipykernel: env.append('PYTHONIOENCODING=UTF-8') # Monitor if self.monitor_enabled: env.append('SPYDER_SHELL_ID=%s' % id(self)) env.append('SPYDER_AR_TIMEOUT=%d' % self.autorefresh_timeout) env.append('SPYDER_AR_STATE=%r' % self.autorefresh_state) from spyderlib.widgets.externalshell import introspection introspection_server = introspection.start_introspection_server() introspection_server.register(self) notification_server = introspection.start_notification_server() self.notification_thread = notification_server.register(self) self.notification_thread.sig_pdb.connect( lambda fname, lineno: self.sig_pdb.emit(fname, lineno)) self.notification_thread.new_ipython_kernel.connect( lambda args: self.create_ipython_client.emit(args)) self.notification_thread.open_file.connect( lambda fname, lineno: self.open_file.emit(fname, lineno)) if self.namespacebrowser is not None: self.configure_namespacebrowser() env.append('SPYDER_I_PORT=%d' % introspection_server.port) env.append('SPYDER_N_PORT=%d' % notification_server.port) # External modules options env.append('ETS_TOOLKIT=%s' % self.ets_backend) if self.mpl_backend: env.append('MATPLOTLIB_BACKEND=%s' % self.mpl_backend) if self.qt_api: env.append('QT_API=%s' % self.qt_api) env.append('COLORIZE_SYS_STDERR=%s' % self.colorize_sys_stderr) # # Socket-based alternative (see input hook in sitecustomize.py): # if self.install_qt_inputhook: # from PyQt4.QtNetwork import QLocalServer # self.local_server = QLocalServer() # self.local_server.listen(str(id(self))) if self.pyqt_api: env.append('PYQT_API=%d' % self.pyqt_api) env.append('IGNORE_SIP_SETAPI_ERRORS=%s' % self.ignore_sip_setapi_errors) # User Module Deleter if self.is_interpreter: env.append('UMR_ENABLED=%r' % self.umr_enabled) env.append('UMR_NAMELIST=%s' % ','.join(self.umr_namelist)) env.append('UMR_VERBOSE=%r' % self.umr_verbose) env.append('MATPLOTLIB_ION=True') else: if self.interact: env.append('MATPLOTLIB_ION=True') else: env.append('MATPLOTLIB_ION=False') # IPython kernel env.append('IPYTHON_KERNEL=%r' % self.is_ipykernel) # Add sitecustomize path to path list pathlist = [] scpath = osp.dirname(osp.abspath(__file__)) pathlist.append(scpath) # Adding Spyder path pathlist += self.path # Adding path list to PYTHONPATH environment variable add_pathlist_to_PYTHONPATH(env, pathlist) #-------------------------Python specific------------------------------ self.process.readyReadStandardOutput.connect(self.write_output) self.process.readyReadStandardError.connect(self.write_error) self.process.finished.connect( lambda ec, es=QProcess.ExitStatus: self.finished(ec, es)) self.sig_finished.connect(self.dialog_manager.close_all) self.terminate_button.clicked.connect(self.process.terminate) self.kill_button.clicked.connect(self.process.kill) #-------------------------Python specific------------------------------ # Fixes for our Mac app: # 1. PYTHONPATH and PYTHONHOME are set while bootstrapping the app, # but their values are messing sys.path for external interpreters # (e.g. EPD) so we need to remove them from the environment. # 2. Set PYTHONPATH again but without grabbing entries defined in the # environment (Fixes Issue 1321) # 3. Remove PYTHONOPTIMIZE from env so that we can have assert # statements working with our interpreters (See Issue 1281) if running_in_mac_app(): env.append('SPYDER_INTERPRETER=%s' % self.pythonexecutable) if MAC_APP_NAME not in self.pythonexecutable: env = [p for p in env if not (p.startswith('PYTHONPATH') or \ p.startswith('PYTHONHOME'))] # 1. add_pathlist_to_PYTHONPATH(env, pathlist, drop_env=True) # 2. env = [p for p in env if not p.startswith('PYTHONOPTIMIZE')] # 3. processEnvironment = QProcessEnvironment() for envItem in env: envName, separator, envValue = envItem.partition('=') processEnvironment.insert(envName, envValue) self.process.setProcessEnvironment(processEnvironment) self.process.start(self.pythonexecutable, p_args) #-------------------------Python specific------------------------------ running = self.process.waitForStarted(3000) self.set_running_state(running) if not running: if self.is_ipykernel: self.ipython_kernel_start_error.emit( _("The kernel failed to start!! That's all we know... " "Please close this console and open a new one.")) else: QMessageBox.critical(self, _("Error"), _("A Python console failed to start!")) else: self.shell.setFocus() self.started.emit() return self.process def finished(self, exit_code, exit_status): """Reimplement ExternalShellBase method""" if self.is_ipykernel and exit_code == 1: self.ipython_kernel_start_error.emit( self.shell.get_text_with_eol()) ExternalShellBase.finished(self, exit_code, exit_status) self.introspection_socket = None #============================================================================== # Input/Output #============================================================================== def write_error(self): if os.name == 'nt': #---This is apparently necessary only on Windows (not sure though): # emptying standard output buffer before writing error output self.process.setReadChannel(QProcess.StandardOutput) if self.process.waitForReadyRead(1): self.write_output() self.shell.write_error(self.get_stderr()) QApplication.processEvents() def send_to_process(self, text): if not self.is_running(): return if not is_text_string(text): text = to_text_string(text) if self.mpl_backend == 'Qt4Agg' and os.name == 'nt' and \ self.introspection_socket is not None: communicate(self.introspection_socket, "toggle_inputhook_flag(True)") # # Socket-based alternative (see input hook in sitecustomize.py): # while self.local_server.hasPendingConnections(): # self.local_server.nextPendingConnection().write('go!') if any([text == cmd for cmd in ['%ls', '%pwd', '%scientific']]) or \ any([text.startswith(cmd) for cmd in ['%cd ', '%clear ']]): text = 'evalsc(r"%s")\n' % text if not text.endswith('\n'): text += '\n' self.process.write(to_binary_string(text, 'utf8')) self.process.waitForBytesWritten(-1) # Eventually write prompt faster (when hitting Enter continuously) # -- necessary/working on Windows only: if os.name == 'nt': self.write_error() def keyboard_interrupt(self): if self.introspection_socket is not None: communicate(self.introspection_socket, "thread.interrupt_main()") def quit_monitor(self): if self.introspection_socket is not None: try: write_packet(self.introspection_socket, "thread.exit()") except socket.error: pass #============================================================================== # Globals explorer #============================================================================== @Slot(bool) def toggle_globals_explorer(self, state): if self.stand_alone is not None: self.splitter.setSizes([1, 1 if state else 0]) self.namespacebrowser_button.setChecked(state) if state and self.namespacebrowser is not None: self.namespacebrowser.refresh_table() def splitter_moved(self, pos, index): self.namespacebrowser_button.setChecked(self.splitter.sizes()[1]) #============================================================================== # Misc. #============================================================================== @Slot() def set_current_working_directory(self): """Set current working directory""" cwd = self.shell.get_cwd() self.redirect_stdio.emit(False) directory = getexistingdirectory(self, _("Select directory"), cwd) if directory: self.shell.set_cwd(directory) self.redirect_stdio.emit(True) @Slot() def show_env(self): """Show environment variables""" get_func = self.shell.get_env set_func = self.shell.set_env self.dialog_manager.show(RemoteEnvDialog(get_func, set_func)) @Slot() def show_syspath(self): """Show sys.path contents""" editor = DictEditor() editor.setup(self.shell.get_syspath(), title="sys.path", readonly=True, width=600, icon='syspath.png') self.dialog_manager.show(editor)
class ExternalSystemShell(ExternalShellBase): """External Shell widget: execute Python script in a separate process""" SHELL_CLASS = TerminalWidget started = Signal() def __init__(self, parent=None, wdir=None, path=[], light_background=True, menu_actions=None, show_buttons_inside=True, show_elapsed_time=True): ExternalShellBase.__init__(self, parent=parent, fname=None, wdir=wdir, history_filename='.history', light_background=light_background, menu_actions=menu_actions, show_buttons_inside=show_buttons_inside, show_elapsed_time=show_elapsed_time) # Additional python path list self.path = path # For compatibility with the other shells that can live in the external # console self.is_ipykernel = False self.connection_file = None def get_icon(self): return ima.icon('cmdprompt') def create_process(self): self.shell.clear() self.process = QProcess(self) self.process.setProcessChannelMode(QProcess.MergedChannels) # PYTHONPATH (in case we use Python in this terminal, e.g. py2exe) env = [ to_text_string(_path) for _path in self.process.systemEnvironment() ] processEnvironment = QProcessEnvironment() for envItem in env: envName, separator, envValue = envItem.partition('=') processEnvironment.insert(envName, envValue) add_pathlist_to_PYTHONPATH(env, self.path) self.process.setProcessEnvironment(processEnvironment) # Working directory if self.wdir is not None: self.process.setWorkingDirectory(self.wdir) # Shell arguments if os.name == 'nt': p_args = ['/Q'] else: p_args = ['-i'] if self.arguments: p_args.extend(shell_split(self.arguments)) self.process.readyReadStandardOutput.connect(self.write_output) self.process.finished.connect(self.finished) self.kill_button.clicked.connect(self.process.kill) if os.name == 'nt': self.process.start('cmd.exe', p_args) else: # Using bash: self.process.start('bash', p_args) self.send_to_process('PS1="\\u@\\h:\\w> "\n') running = self.process.waitForStarted() self.set_running_state(running) if not running: QMessageBox.critical(self, _("Error"), _("Process failed to start")) else: self.shell.setFocus() self.started.emit() return self.process #=============================================================================== # Input/Output #=============================================================================== def transcode(self, qba): if os.name == 'nt': return to_text_string(CP850_CODEC.toUnicode(qba.data())) else: return ExternalShellBase.transcode(self, qba) def send_to_process(self, text): if not is_text_string(text): text = to_text_string(text) if text[:-1] in ["clear", "cls", "CLS"]: self.shell.clear() self.send_to_process(os.linesep) return if not text.endswith('\n'): text += '\n' if os.name == 'nt': self.process.write(text.encode('cp850')) else: self.process.write(LOCALE_CODEC.fromUnicode(text)) self.process.waitForBytesWritten(-1) def keyboard_interrupt(self): # This does not work on Windows: # (unfortunately there is no easy way to send a Ctrl+C to cmd.exe) self.send_ctrl_to_process('c')
class IPythonClient(QWidget, SaveHistoryMixin): """ IPython client or frontend for Spyder This is a widget composed of a shell widget (i.e. RichIPythonWidget + our additions = IPythonShellWidget) and an WebView info widget to print kernel error and other messages. """ SEPARATOR = '%s##---(%s)---' % (os.linesep * 2, time.ctime()) append_to_history = Signal(str, str) def __init__(self, plugin, name, history_filename, connection_file=None, hostname=None, sshkey=None, password=None, kernel_widget_id=None, menu_actions=None): super(IPythonClient, self).__init__(plugin) SaveHistoryMixin.__init__(self) self.options_button = None # stop button and icon self.stop_button = None self.stop_icon = get_icon("stop.png") self.connection_file = connection_file self.kernel_widget_id = kernel_widget_id self.hostname = hostname self.sshkey = sshkey self.password = password self.name = name self.get_option = plugin.get_option self.shellwidget = IPythonShellWidget(config=self.shellwidget_config(), local_kernel=False) self.shellwidget.hide() self.infowidget = WebView(self) self.menu_actions = menu_actions self.history_filename = get_conf_path(history_filename) self.history = [] self.namespacebrowser = None self.set_infowidget_font() self.loading_page = self._create_loading_page() self.infowidget.setHtml(self.loading_page) vlayout = QVBoxLayout() toolbar_buttons = self.get_toolbar_buttons() hlayout = QHBoxLayout() for button in toolbar_buttons: hlayout.addWidget(button) vlayout.addLayout(hlayout) vlayout.setContentsMargins(0, 0, 0, 0) vlayout.addWidget(self.shellwidget) vlayout.addWidget(self.infowidget) self.setLayout(vlayout) self.exit_callback = lambda: plugin.close_client(client=self) #------ Public API -------------------------------------------------------- def show_shellwidget(self, give_focus=True): """Show shellwidget and configure it""" self.infowidget.hide() self.shellwidget.show() self.infowidget.setHtml(BLANK) if give_focus: self.get_control().setFocus() # Connect shellwidget to the client self.shellwidget.set_ipyclient(self) # To save history self.shellwidget.executing.connect(self.add_to_history) # For Mayavi to run correctly self.shellwidget.executing.connect(self.set_backend_for_mayavi) # To update history after execution self.shellwidget.executed.connect(self.update_history) # To update the Variable Explorer after execution self.shellwidget.executed.connect(self.auto_refresh_namespacebrowser) # To show a stop button, when executing a process self.shellwidget.executing.connect(self.enable_stop_button) # To hide a stop button after execution stopped self.shellwidget.executed.connect(self.disable_stop_button) def enable_stop_button(self): self.stop_button.setEnabled(True) def disable_stop_button(self): self.stop_button.setDisabled(True) @Slot() def stop_button_click_handler(self): self.stop_button.setDisabled(True) self.interrupt_kernel() def show_kernel_error(self, error): """Show kernel initialization errors in infowidget""" # Remove explanation about how to kill the kernel (doesn't apply to us) error = error.split('issues/2049')[-1] # Remove unneeded blank lines at the beginning eol = sourcecode.get_eol_chars(error) if eol: error = error.replace(eol, '<br>') while error.startswith('<br>'): error = error[4:] # Remove connection message if error.startswith('To connect another client') or \ error.startswith('[IPKernelApp] To connect another client'): error = error.split('<br>') error = '<br>'.join(error[2:]) # Don't break lines in hyphens # From http://stackoverflow.com/q/7691569/438386 error = error.replace('-', '‑') message = _("An error ocurred while starting the kernel") kernel_error_template = Template(KERNEL_ERROR) page = kernel_error_template.substitute(css_path=CSS_PATH, message=message, error=error) self.infowidget.setHtml(page) def show_restart_animation(self): self.shellwidget.hide() self.infowidget.setHtml(self.loading_page) self.infowidget.show() def get_name(self): """Return client name""" return ((_("Console") if self.hostname is None else self.hostname) + " " + self.name) def get_control(self): """Return the text widget (or similar) to give focus to""" # page_control is the widget used for paging page_control = self.shellwidget._page_control if page_control and page_control.isVisible(): return page_control else: return self.shellwidget._control def get_options_menu(self): """Return options menu""" restart_action = create_action(self, _("Restart kernel"), shortcut=QKeySequence("Ctrl+."), icon=get_icon('restart.png'), triggered=self.restart_kernel) restart_action.setShortcutContext(Qt.WidgetWithChildrenShortcut) # Main menu if self.menu_actions is not None: actions = [restart_action, None] + self.menu_actions else: actions = [restart_action] return actions def get_toolbar_buttons(self): """Return toolbar buttons list""" #TODO: Eventually add some buttons (Empty for now) # (see for example: spyderlib/widgets/externalshell/baseshell.py) buttons = [] # Code to add the stop button if self.stop_button is None: self.stop_button = create_toolbutton( self, text=_("Stop"), icon=self.stop_icon, tip=_("Stop the current command")) self.disable_stop_button() # set click event handler self.stop_button.clicked.connect(self.stop_button_click_handler) if self.stop_button is not None: buttons.append(self.stop_button) if self.options_button is None: options = self.get_options_menu() if options: self.options_button = create_toolbutton( self, text=_("Options"), icon=get_icon('tooloptions.png')) self.options_button.setPopupMode(QToolButton.InstantPopup) menu = QMenu(self) add_actions(menu, options) self.options_button.setMenu(menu) if self.options_button is not None: buttons.append(self.options_button) return buttons def add_actions_to_context_menu(self, menu): """Add actions to IPython widget context menu""" # See spyderlib/widgets/ipython.py for more details on this method inspect_action = create_action( self, _("Inspect current object"), QKeySequence(get_shortcut('console', 'inspect current object')), icon=get_std_icon('MessageBoxInformation'), triggered=self.inspect_object) clear_line_action = create_action(self, _("Clear line or block"), QKeySequence("Shift+Escape"), icon=get_icon('eraser.png'), triggered=self.clear_line) clear_console_action = create_action( self, _("Clear console"), QKeySequence(get_shortcut('console', 'clear shell')), icon=get_icon('clear.png'), triggered=self.clear_console) quit_action = create_action(self, _("&Quit"), icon='exit.png', triggered=self.exit_callback) add_actions(menu, (None, inspect_action, clear_line_action, clear_console_action, None, quit_action)) return menu def set_font(self, font): """Set IPython widget's font""" self.shellwidget._control.setFont(font) self.shellwidget.font = font def set_infowidget_font(self): font = get_font('inspector', 'rich_text') self.infowidget.set_font(font) def interrupt_kernel(self): """Interrupt the associanted Spyder kernel if it's running""" self.shellwidget.request_interrupt_kernel() @Slot() def restart_kernel(self): """Restart the associanted Spyder kernel""" self.shellwidget.request_restart_kernel() @Slot() def inspect_object(self): """Show how to inspect an object with our object inspector""" self.shellwidget._control.inspect_current_object() @Slot() def clear_line(self): """Clear a console line""" self.shellwidget._keyboard_quit() @Slot() def clear_console(self): """Clear the whole console""" self.shellwidget.execute("%clear") def if_kernel_dies(self, t): """ Show a message in the console if the kernel dies. t is the time in seconds between the death and showing the message. """ message = _("It seems the kernel died unexpectedly. Use " "'Restart kernel' to continue using this console.") self.shellwidget._append_plain_text(message + '\n') def update_history(self): self.history = self.shellwidget._history def set_backend_for_mayavi(self, command): calling_mayavi = False lines = command.splitlines() for l in lines: if not l.startswith('#'): if 'import mayavi' in l or 'from mayavi' in l: calling_mayavi = True break if calling_mayavi: message = _("Changing backend to Qt for Mayavi") self.shellwidget._append_plain_text(message + '\n') self.shellwidget.execute("%gui inline\n%gui qt") def interrupt_message(self): """ Print an interrupt message when the client is connected to an external kernel """ message = _("Kernel process is either remote or unspecified. " "Cannot interrupt") QMessageBox.information(self, "IPython", message) def restart_message(self): """ Print a restart message when the client is connected to an external kernel """ message = _("Kernel process is either remote or unspecified. " "Cannot restart.") QMessageBox.information(self, "IPython", message) def set_namespacebrowser(self, namespacebrowser): """Set namespace browser widget""" self.namespacebrowser = namespacebrowser def auto_refresh_namespacebrowser(self): """Refresh namespace browser""" if self.namespacebrowser: self.namespacebrowser.refresh_table() def shellwidget_config(self): """Generate a Config instance for shell widgets using our config system This lets us create each widget with its own config (as opposed to IPythonQtConsoleApp, where all widgets have the same config) """ # ---- IPython config ---- try: profile_path = osp.join(get_ipython_dir(), 'profile_default') full_ip_cfg = load_pyconfig_files(['ipython_qtconsole_config.py'], profile_path) # From the full config we only select the IPythonWidget section # because the others have no effect here. ip_cfg = Config({'IPythonWidget': full_ip_cfg.IPythonWidget}) except: ip_cfg = Config() # ---- Spyder config ---- spy_cfg = Config() # Make the pager widget a rich one (i.e a QTextEdit) spy_cfg.IPythonWidget.kind = 'rich' # Gui completion widget gui_comp_o = self.get_option('use_gui_completion') completions = {True: 'droplist', False: 'ncurses'} spy_cfg.IPythonWidget.gui_completion = completions[gui_comp_o] # Pager pager_o = self.get_option('use_pager') if pager_o: spy_cfg.IPythonWidget.paging = 'inside' else: spy_cfg.IPythonWidget.paging = 'none' # Calltips calltips_o = self.get_option('show_calltips') spy_cfg.IPythonWidget.enable_calltips = calltips_o # Buffer size buffer_size_o = self.get_option('buffer_size') spy_cfg.IPythonWidget.buffer_size = buffer_size_o # Prompts in_prompt_o = self.get_option('in_prompt') out_prompt_o = self.get_option('out_prompt') if in_prompt_o: spy_cfg.IPythonWidget.in_prompt = in_prompt_o if out_prompt_o: spy_cfg.IPythonWidget.out_prompt = out_prompt_o # Merge IPython and Spyder configs. Spyder prefs will have prevalence # over IPython ones ip_cfg._merge(spy_cfg) return ip_cfg #------ Private API ------------------------------------------------------- def _create_loading_page(self): loading_template = Template(LOADING) loading_img = get_image_path('loading_sprites.png') if os.name == 'nt': loading_img = loading_img.replace('\\', '/') message = _("Connecting to kernel...") page = loading_template.substitute(css_path=CSS_PATH, loading_img=loading_img, message=message) return page #---- Qt methods ---------------------------------------------------------- def closeEvent(self, event): """ Reimplement Qt method to stop sending the custom_restart_kernel_died signal """ kc = self.shellwidget.kernel_client if kc is not None: kc.hb_channel.pause()
class IPythonShellWidget(RichIPythonWidget): """ Spyder's IPython shell widget This class has custom control and page_control widgets, additional methods to provide missing functionality and a couple more keyboard shortcuts. """ focus_changed = Signal() new_client = Signal() def __init__(self, *args, **kw): # To override the Qt widget used by RichIPythonWidget self.custom_control = IPythonControlWidget self.custom_page_control = IPythonPageControlWidget super(IPythonShellWidget, self).__init__(*args, **kw) self.set_background_color() # --- Spyder variables --- self.ipyclient = None # --- Keyboard shortcuts --- self.shortcuts = self.create_shortcuts() # --- IPython variables --- # To send an interrupt signal to the Spyder kernel self.custom_interrupt = True # To restart the Spyder kernel in case it dies self.custom_restart = True #---- Public API ---------------------------------------------------------- def set_ipyclient(self, ipyclient): """Bind this shell widget to an IPython client one""" self.ipyclient = ipyclient self.exit_requested.connect(ipyclient.exit_callback) def long_banner(self): """Banner for IPython widgets with pylab message""" from IPython.core.usage import default_gui_banner banner = default_gui_banner pylab_o = CONF.get('ipython_console', 'pylab', True) autoload_pylab_o = CONF.get('ipython_console', 'pylab/autoload', True) mpl_installed = programs.is_module_installed('matplotlib') if mpl_installed and (pylab_o and autoload_pylab_o): pylab_message = ("\nPopulating the interactive namespace from " "numpy and matplotlib") banner = banner + pylab_message sympy_o = CONF.get('ipython_console', 'symbolic_math', True) if sympy_o: lines = """ These commands were executed: >>> from __future__ import division >>> from sympy import * >>> x, y, z, t = symbols('x y z t') >>> k, m, n = symbols('k m n', integer=True) >>> f, g, h = symbols('f g h', cls=Function) """ banner = banner + lines return banner def short_banner(self): """Short banner with Python and IPython versions""" from IPython.core.release import version py_ver = '%d.%d.%d' % (sys.version_info[0], sys.version_info[1], sys.version_info[2]) banner = 'Python %s on %s -- IPython %s' % (py_ver, sys.platform, version) return banner def clear_console(self): self.execute("%clear") def write_to_stdin(self, line): """ Send raw characters to the IPython kernel through stdin but only if the kernel is currently looking for raw input. """ if self._reading: try: self.kernel_client.stdin_channel.input(line) except AttributeError: self.kernel_client.input(line) def set_background_color(self): lightbg_o = CONF.get('ipython_console', 'light_color') if not lightbg_o: self.set_default_style(colors='linux') def create_shortcuts(self): inspect = create_shortcut(self._control.inspect_current_object, context='Console', name='Inspect current object', parent=self) clear_console = create_shortcut(self.clear_console, context='Console', name='Clear shell', parent=self) # Fixed shortcuts new_shortcut("Ctrl+T", self, lambda: self.new_client.emit()) new_shortcut(SHORTCUT_INLINE, self, lambda: self._control.enter_array_inline()) new_shortcut(SHORTCUT_TABLE, self, lambda: self._control.enter_array_table()) return [inspect, clear_console] def get_signature(self, content): """Get signature from inspect reply content""" data = content.get('data', {}) text = data.get('text/plain', '') if text: text = ANSI_OR_SPECIAL_PATTERN.sub('', text) line = self._control.get_current_line_to_cursor() name = line[:-1].split('.')[-1] argspec = getargspecfromtext(text) if argspec: # This covers cases like np.abs, whose docstring is # the same as np.absolute and because of that a proper # signature can't be obtained correctly signature = name + argspec else: signature = getsignaturefromtext(text, name) return signature else: return '' #---- IPython private methods --------------------------------------------- def _context_menu_make(self, pos): """Reimplement the IPython context menu""" menu = super(IPythonShellWidget, self)._context_menu_make(pos) return self.ipyclient.add_actions_to_context_menu(menu) def _banner_default(self): """ Reimplement banner creation to let the user decide if he wants a banner or not """ banner_o = CONF.get('ipython_console', 'show_banner', True) if banner_o: return self.long_banner() else: return self.short_banner() def _handle_object_info_reply(self, rep): """ Reimplement call tips to only show signatures, using the same style from our Editor and External Console too Note: For IPython 2- """ self.log.debug("oinfo: %s", rep.get('content', '')) cursor = self._get_cursor() info = self._request_info.get('call_tip') if info and info.id == rep['parent_header']['msg_id'] and \ info.pos == cursor.position(): content = rep['content'] if content.get('ismagic', False): call_info, doc = None, None else: call_info, doc = call_tip(content, format_call=True) if call_info is None and doc is not None: name = content['name'].split('.')[-1] argspec = getargspecfromtext(doc) if argspec: # This covers cases like np.abs, whose docstring is # the same as np.absolute and because of that a proper # signature can't be obtained correctly call_info = name + argspec else: call_info = getsignaturefromtext(doc, name) if call_info: self._control.show_calltip(_("Arguments"), call_info, signature=True, color='#2D62FF') def _handle_inspect_reply(self, rep): """ Reimplement call tips to only show signatures, using the same style from our Editor and External Console too Note: For IPython 3+ """ cursor = self._get_cursor() info = self._request_info.get('call_tip') if info and info.id == rep['parent_header']['msg_id'] and \ info.pos == cursor.position(): content = rep['content'] if content.get('status') == 'ok' and content.get('found', False): signature = self.get_signature(content) if signature: self._control.show_calltip(_("Arguments"), signature, signature=True, color='#2D62FF') #---- Qt methods ---------------------------------------------------------- def focusInEvent(self, event): """Reimplement Qt method to send focus change notification""" self.focus_changed.emit() return super(IPythonShellWidget, self).focusInEvent(event) def focusOutEvent(self, event): """Reimplement Qt method to send focus change notification""" self.focus_changed.emit() return super(IPythonShellWidget, self).focusOutEvent(event)
class NamespaceBrowser(QWidget): """Namespace browser (global variables explorer widget)""" sig_option_changed = Signal(str, object) sig_collapse = Signal() def __init__(self, parent): QWidget.__init__(self, parent) self.shellwidget = None self.is_internal_shell = None self.ipyclient = None self.is_ipykernel = None self.is_visible = True # Do not modify: light mode won't work! self.setup_in_progress = None # Remote dict editor settings self.check_all = None self.exclude_private = None self.exclude_uppercase = None self.exclude_capitalized = None self.exclude_unsupported = None self.excluded_names = None self.truncate = None self.minmax = None self.remote_editing = None self.autorefresh = None self.editor = None self.exclude_private_action = None self.exclude_uppercase_action = None self.exclude_capitalized_action = None self.exclude_unsupported_action = None self.filename = None def setup(self, check_all=None, exclude_private=None, exclude_uppercase=None, exclude_capitalized=None, exclude_unsupported=None, excluded_names=None, truncate=None, minmax=None, remote_editing=None, autorefresh=None): """Setup the namespace browser""" assert self.shellwidget is not None self.check_all = check_all self.exclude_private = exclude_private self.exclude_uppercase = exclude_uppercase self.exclude_capitalized = exclude_capitalized self.exclude_unsupported = exclude_unsupported self.excluded_names = excluded_names self.truncate = truncate self.minmax = minmax self.remote_editing = remote_editing self.autorefresh = autorefresh if self.editor is not None: self.editor.setup_menu(truncate, minmax) self.exclude_private_action.setChecked(exclude_private) self.exclude_uppercase_action.setChecked(exclude_uppercase) self.exclude_capitalized_action.setChecked(exclude_capitalized) self.exclude_unsupported_action.setChecked(exclude_unsupported) # Don't turn autorefresh on for IPython kernels # See Issue 1450 if not self.is_ipykernel: self.auto_refresh_button.setChecked(autorefresh) self.refresh_table() return # Dict editor: if self.is_internal_shell: self.editor = DictEditorTableView(self, None, truncate=truncate, minmax=minmax) else: self.editor = RemoteDictEditorTableView(self, None, truncate=truncate, minmax=minmax, remote_editing=remote_editing, get_value_func=self.get_value, set_value_func=self.set_value, new_value_func=self.set_value, remove_values_func=self.remove_values, copy_value_func=self.copy_value, is_list_func=self.is_list, get_len_func=self.get_len, is_array_func=self.is_array, is_image_func=self.is_image, is_dict_func=self.is_dict, is_data_frame_func=self.is_data_frame, is_time_series_func=self.is_time_series, get_array_shape_func=self.get_array_shape, get_array_ndim_func=self.get_array_ndim, oedit_func=self.oedit, plot_func=self.plot, imshow_func=self.imshow, show_image_func=self.show_image) self.editor.sig_option_changed.connect(self.sig_option_changed.emit) self.editor.sig_files_dropped.connect(self.import_data) # Setup layout hlayout = QHBoxLayout() vlayout = QVBoxLayout() toolbar = self.setup_toolbar(exclude_private, exclude_uppercase, exclude_capitalized, exclude_unsupported, autorefresh) vlayout.setAlignment(Qt.AlignTop) for widget in toolbar: vlayout.addWidget(widget) hlayout.addWidget(self.editor) hlayout.addLayout(vlayout) self.setLayout(hlayout) hlayout.setContentsMargins(0, 0, 0, 0) self.sig_option_changed.connect(self.option_changed) def set_shellwidget(self, shellwidget): """Bind shellwidget instance to namespace browser""" self.shellwidget = shellwidget from spyderlib.widgets import internalshell self.is_internal_shell = isinstance(self.shellwidget, internalshell.InternalShell) self.is_ipykernel = self.shellwidget.is_ipykernel if not self.is_internal_shell: shellwidget.set_namespacebrowser(self) def set_ipyclient(self, ipyclient): """Bind ipyclient instance to namespace browser""" self.ipyclient = ipyclient def setup_toolbar(self, exclude_private, exclude_uppercase, exclude_capitalized, exclude_unsupported, autorefresh): """Setup toolbar""" self.setup_in_progress = True toolbar = [] refresh_button = create_toolbutton(self, text=_('Refresh'), icon=ima.icon('reload'), triggered=self.refresh_table) self.auto_refresh_button = create_toolbutton(self, text=_('Refresh periodically'), icon=ima.icon('auto_reload'), toggled=self.toggle_auto_refresh) self.auto_refresh_button.setChecked(autorefresh) load_button = create_toolbutton(self, text=_('Import data'), icon=ima.icon('fileimport'), triggered=self.import_data) self.save_button = create_toolbutton(self, text=_("Save data"), icon=ima.icon('filesave'), triggered=lambda: self.save_data(self.filename)) self.save_button.setEnabled(False) save_as_button = create_toolbutton(self, text=_("Save data as..."), icon=ima.icon('filesaveas'), triggered=self.save_data) toolbar += [refresh_button, self.auto_refresh_button, load_button, self.save_button, save_as_button] self.exclude_private_action = create_action(self, _("Exclude private references"), tip=_("Exclude references which name starts" " with an underscore"), toggled=lambda state: self.sig_option_changed.emit('exclude_private', state)) self.exclude_private_action.setChecked(exclude_private) self.exclude_uppercase_action = create_action(self, _("Exclude all-uppercase references"), tip=_("Exclude references which name is uppercase"), toggled=lambda state: self.sig_option_changed.emit('exclude_uppercase', state)) self.exclude_uppercase_action.setChecked(exclude_uppercase) self.exclude_capitalized_action = create_action(self, _("Exclude capitalized references"), tip=_("Exclude references which name starts with an " "uppercase character"), toggled=lambda state: self.sig_option_changed.emit('exclude_capitalized', state)) self.exclude_capitalized_action.setChecked(exclude_capitalized) self.exclude_unsupported_action = create_action(self, _("Exclude unsupported data types"), tip=_("Exclude references to unsupported data types" " (i.e. which won't be handled/saved correctly)"), toggled=lambda state: self.sig_option_changed.emit('exclude_unsupported', state)) self.exclude_unsupported_action.setChecked(exclude_unsupported) options_button = create_toolbutton(self, text=_('Options'), icon=ima.icon('tooloptions')) toolbar.append(options_button) options_button.setPopupMode(QToolButton.InstantPopup) menu = QMenu(self) editor = self.editor actions = [self.exclude_private_action, self.exclude_uppercase_action, self.exclude_capitalized_action, self.exclude_unsupported_action, None, editor.truncate_action] if is_module_installed('numpy'): actions.append(editor.minmax_action) add_actions(menu, actions) options_button.setMenu(menu) self.setup_in_progress = False return toolbar def option_changed(self, option, value): """Option has changed""" setattr(self, to_text_string(option), value) if not self.is_internal_shell: settings = self.get_view_settings() communicate(self._get_sock(), 'set_remote_view_settings()', settings=[settings]) def visibility_changed(self, enable): """Notify the widget whether its container (the namespace browser plugin is visible or not""" self.is_visible = enable if enable: self.refresh_table() @Slot(bool) def toggle_auto_refresh(self, state): """Toggle auto refresh state""" self.autorefresh = state if not self.setup_in_progress and not self.is_internal_shell: communicate(self._get_sock(), "set_monitor_auto_refresh(%r)" % state) def _get_sock(self): """Return socket connection""" return self.shellwidget.introspection_socket def get_internal_shell_filter(self, mode, check_all=None): """ Return internal shell data types filter: * check_all: check all elements data types for sequences (dict, list, tuple) * mode (string): 'editable' or 'picklable' """ assert mode in list(SUPPORTED_TYPES.keys()) if check_all is None: check_all = self.check_all def wsfilter(input_dict, check_all=check_all, filters=tuple(SUPPORTED_TYPES[mode])): """Keep only objects that can be pickled""" return globalsfilter( input_dict, check_all=check_all, filters=filters, exclude_private=self.exclude_private, exclude_uppercase=self.exclude_uppercase, exclude_capitalized=self.exclude_capitalized, exclude_unsupported=self.exclude_unsupported, excluded_names=self.excluded_names) return wsfilter def get_view_settings(self): """Return dict editor view settings""" settings = {} for name in REMOTE_SETTINGS: settings[name] = getattr(self, name) return settings @Slot() def refresh_table(self): """Refresh variable table""" if self.is_visible and self.isVisible(): if self.is_internal_shell: # Internal shell wsfilter = self.get_internal_shell_filter('editable') self.editor.set_filter(wsfilter) interpreter = self.shellwidget.interpreter if interpreter is not None: self.editor.set_data(interpreter.namespace) self.editor.adjust_columns() elif self.shellwidget.is_running(): # import time; print >>STDOUT, time.ctime(time.time()), "Refreshing namespace browser" sock = self._get_sock() if sock is None: return try: communicate(sock, "refresh()") except socket.error: # Process was terminated before calling this method pass def process_remote_view(self, remote_view): """Process remote view""" if remote_view is not None: self.set_data(remote_view) #------ Remote Python process commands ------------------------------------ def get_value(self, name): value = monitor_get_global(self._get_sock(), name) if value is None: if communicate(self._get_sock(), '%s is not None' % name): import pickle msg = to_text_string(_("Object <b>%s</b> is not picklable") % name) raise pickle.PicklingError(msg) return value def set_value(self, name, value): monitor_set_global(self._get_sock(), name, value) self.refresh_table() def remove_values(self, names): for name in names: monitor_del_global(self._get_sock(), name) self.refresh_table() def copy_value(self, orig_name, new_name): monitor_copy_global(self._get_sock(), orig_name, new_name) self.refresh_table() def is_list(self, name): """Return True if variable is a list or a tuple""" return communicate(self._get_sock(), 'isinstance(%s, (tuple, list))' % name) def is_dict(self, name): """Return True if variable is a dictionary""" return communicate(self._get_sock(), 'isinstance(%s, dict)' % name) def get_len(self, name): """Return sequence length""" return communicate(self._get_sock(), "len(%s)" % name) def is_array(self, name): """Return True if variable is a NumPy array""" return communicate(self._get_sock(), 'is_array("%s")' % name) def is_image(self, name): """Return True if variable is a PIL.Image image""" return communicate(self._get_sock(), 'is_image("%s")' % name) def is_data_frame(self, name): """Return True if variable is a data_frame""" return communicate(self._get_sock(), "isinstance(globals()['%s'], DataFrame)" % name) def is_time_series(self, name): """Return True if variable is a data_frame""" return communicate(self._get_sock(), "isinstance(globals()['%s'], TimeSeries)" % name) def get_array_shape(self, name): """Return array's shape""" return communicate(self._get_sock(), "%s.shape" % name) def get_array_ndim(self, name): """Return array's ndim""" return communicate(self._get_sock(), "%s.ndim" % name) def plot(self, name, funcname): command = "import spyderlib.pyplot; "\ "__fig__ = spyderlib.pyplot.figure(); "\ "__items__ = getattr(spyderlib.pyplot, '%s')(%s); "\ "spyderlib.pyplot.show(); "\ "del __fig__, __items__;" % (funcname, name) if self.is_ipykernel: self.ipyclient.shellwidget.execute("%%varexp --%s %s" % (funcname, name)) else: self.shellwidget.send_to_process(command) def imshow(self, name): command = "import spyderlib.pyplot; " \ "__fig__ = spyderlib.pyplot.figure(); " \ "__items__ = spyderlib.pyplot.imshow(%s); " \ "spyderlib.pyplot.show(); del __fig__, __items__;" % name if self.is_ipykernel: self.ipyclient.shellwidget.execute("%%varexp --imshow %s" % name) else: self.shellwidget.send_to_process(command) def show_image(self, name): command = "%s.show()" % name if self.is_ipykernel: self.ipyclient.shellwidget.execute(command) else: self.shellwidget.send_to_process(command) def oedit(self, name): command = "from spyderlib.widgets.objecteditor import oedit; " \ "oedit('%s', modal=False, namespace=locals());" % name self.shellwidget.send_to_process(command) #------ Set, load and save data ------------------------------------------- def set_data(self, data): """Set data""" if data != self.editor.model.get_data(): self.editor.set_data(data) self.editor.adjust_columns() def collapse(self): """Collapse""" self.sig_collapse.emit() @Slot(list) def import_data(self, filenames=None): """Import data from text file""" title = _("Import data") if filenames is None: if self.filename is None: basedir = getcwd() else: basedir = osp.dirname(self.filename) filenames, _selfilter = getopenfilenames(self, title, basedir, iofunctions.load_filters) if not filenames: return elif is_text_string(filenames): filenames = [filenames] for filename in filenames: self.filename = to_text_string(filename) ext = osp.splitext(self.filename)[1].lower() if ext not in iofunctions.load_funcs: buttons = QMessageBox.Yes | QMessageBox.Cancel answer = QMessageBox.question(self, title, _("<b>Unsupported file extension '%s'</b><br><br>" "Would you like to import it anyway " "(by selecting a known file format)?" ) % ext, buttons) if answer == QMessageBox.Cancel: return formats = list(iofunctions.load_extensions.keys()) item, ok = QInputDialog.getItem(self, title, _('Open file as:'), formats, 0, False) if ok: ext = iofunctions.load_extensions[to_text_string(item)] else: return load_func = iofunctions.load_funcs[ext] # 'import_wizard' (self.setup_io) if is_text_string(load_func): # Import data with import wizard error_message = None try: text, _encoding = encoding.read(self.filename) if self.is_internal_shell: self.editor.import_from_string(text) else: base_name = osp.basename(self.filename) editor = ImportWizard(self, text, title=base_name, varname=fix_reference_name(base_name)) if editor.exec_(): var_name, clip_data = editor.get_data() monitor_set_global(self._get_sock(), var_name, clip_data) except Exception as error: error_message = str(error) else: QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) QApplication.processEvents() if self.is_internal_shell: namespace, error_message = load_func(self.filename) interpreter = self.shellwidget.interpreter for key in list(namespace.keys()): new_key = fix_reference_name(key, blacklist=list(interpreter.namespace.keys())) if new_key != key: namespace[new_key] = namespace.pop(key) if error_message is None: interpreter.namespace.update(namespace) else: error_message = monitor_load_globals(self._get_sock(), self.filename, ext) QApplication.restoreOverrideCursor() QApplication.processEvents() if error_message is not None: QMessageBox.critical(self, title, _("<b>Unable to load '%s'</b>" "<br><br>Error message:<br>%s" ) % (self.filename, error_message)) self.refresh_table() @Slot() def save_data(self, filename=None): """Save data""" if filename is None: filename = self.filename if filename is None: filename = getcwd() filename, _selfilter = getsavefilename(self, _("Save data"), filename, iofunctions.save_filters) if filename: self.filename = filename else: return False QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) QApplication.processEvents() if self.is_internal_shell: wsfilter = self.get_internal_shell_filter('picklable', check_all=True) namespace = wsfilter(self.shellwidget.interpreter.namespace).copy() error_message = iofunctions.save(namespace, filename) else: settings = self.get_view_settings() error_message = monitor_save_globals(self._get_sock(), settings, filename) QApplication.restoreOverrideCursor() QApplication.processEvents() if error_message is not None: QMessageBox.critical(self, _("Save data"), _("<b>Unable to save current workspace</b>" "<br><br>Error message:<br>%s") % error_message) self.save_button.setEnabled(self.filename is not None)
class Profiler(ProfilerWidget, SpyderPluginMixin): """Profiler (after python's profile and pstats)""" CONF_SECTION = 'profiler' CONFIGWIDGET_CLASS = ProfilerConfigPage edit_goto = Signal(str, int, str) def __init__(self, parent=None): ProfilerWidget.__init__(self, parent=parent, max_entries=self.get_option('max_entries', 50)) SpyderPluginMixin.__init__(self, parent) # Initialize plugin self.initialize_plugin() #------ SpyderPluginWidget API --------------------------------------------- def get_plugin_title(self): """Return widget title""" return _("Profiler") def get_plugin_icon(self): """Return widget icon""" return get_icon('profiler.png') def get_focus_widget(self): """ Return the widget to give focus to when this plugin's dockwidget is raised on top-level """ return self.datatree def get_plugin_actions(self): """Return a list of actions related to plugin""" return [] def on_first_registration(self): """Action to be performed on first plugin registration""" self.main.tabify_plugins(self.main.inspector, self) self.dockwidget.hide() def register_plugin(self): """Register plugin in Spyder's main window""" self.edit_goto.connect(self.main.editor.load) self.redirect_stdio.connect(self.main.redirect_internalshell_stdio) self.main.add_dockwidget(self) profiler_act = create_action(self, _("Profile"), icon=get_icon('profiler.png'), triggered=self.run_profiler) profiler_act.setEnabled(is_profiler_installed()) self.register_shortcut(profiler_act, context="Profiler", name="Run profiler") self.main.run_menu_actions += [profiler_act] self.main.editor.pythonfile_dependent_actions += [profiler_act] def refresh_plugin(self): """Refresh profiler widget""" #self.remove_obsolete_items() # FIXME: not implemented yet def closing_plugin(self, cancelable=False): """Perform actions before parent main window is closed""" return True def apply_plugin_settings(self, options): """Apply configuration file's plugin settings""" # The history depth option will be applied at # next Spyder startup, which is soon enough pass #------ Public API --------------------------------------------------------- def run_profiler(self): """Run profiler""" self.analyze(self.main.editor.get_current_filename()) def analyze(self, filename): """Reimplement analyze method""" if self.dockwidget and not self.ismaximized: self.dockwidget.setVisible(True) self.dockwidget.setFocus() self.dockwidget.raise_() pythonpath = self.main.get_spyder_pythonpath() runconf = runconfig.get_run_configuration(filename) wdir, args = None, None if runconf is not None: if runconf.wdir_enabled: wdir = runconf.wdir if runconf.args_enabled: args = runconf.args ProfilerWidget.analyze(self, filename, wdir=wdir, args=args, pythonpath=pythonpath)
class ConfigPage(QWidget): """Base class for configuration page in Preferences""" # Signals apply_button_enabled = Signal(bool) show_this_page = Signal() def __init__(self, parent, apply_callback=None): QWidget.__init__(self, parent) self.apply_callback = apply_callback self.is_modified = False def initialize(self): """ Initialize configuration page: * setup GUI widgets * load settings and change widgets accordingly """ self.setup_page() self.load_from_conf() def get_name(self): """Return configuration page name""" raise NotImplementedError def get_icon(self): """Return configuration page icon (24x24)""" raise NotImplementedError def setup_page(self): """Setup configuration page widget""" raise NotImplementedError def set_modified(self, state): self.is_modified = state self.apply_button_enabled.emit(state) def is_valid(self): """Return True if all widget contents are valid""" raise NotImplementedError def apply_changes(self): """Apply changes callback""" if self.is_modified: self.save_to_conf() if self.apply_callback is not None: self.apply_callback() # Since the language cannot be retrieved by CONF and the language # is needed before loading CONF, this is an extra method needed to # ensure that when changes are applied, they are copied to a # specific file storing the language value. This only applies to # the main section config. if self.CONF_SECTION == u'main': self._save_lang() for restart_option in self.restart_options: if restart_option in self.changed_options: self.prompt_restart_required() break # Ensure a single popup is displayed self.set_modified(False) def load_from_conf(self): """Load settings from configuration file""" raise NotImplementedError def save_to_conf(self): """Save settings to configuration file""" raise NotImplementedError
class ConfigDialog(QDialog): """Spyder configuration ('Preferences') dialog box""" # Signals check_settings = Signal() size_change = Signal(QSize) def __init__(self, parent=None): QDialog.__init__(self, parent) self.main = parent # Widgets self.pages_widget = QStackedWidget() self.contents_widget = QListWidget() self.button_reset = QPushButton(_('Reset to defaults')) bbox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Apply | QDialogButtonBox.Cancel) self.apply_btn = bbox.button(QDialogButtonBox.Apply) # Widgets setup # Destroying the C++ object right after closing the dialog box, # otherwise it may be garbage-collected in another QThread # (e.g. the editor's analysis thread in Spyder), thus leading to # a segmentation fault on UNIX or an application crash on Windows self.setAttribute(Qt.WA_DeleteOnClose) self.setWindowTitle(_('Preferences')) self.setWindowIcon(ima.icon('configure')) self.contents_widget.setMovement(QListView.Static) self.contents_widget.setSpacing(1) self.contents_widget.setCurrentRow(0) # Layout hsplitter = QSplitter() hsplitter.addWidget(self.contents_widget) hsplitter.addWidget(self.pages_widget) btnlayout = QHBoxLayout() btnlayout.addWidget(self.button_reset) btnlayout.addStretch(1) btnlayout.addWidget(bbox) vlayout = QVBoxLayout() vlayout.addWidget(hsplitter) vlayout.addLayout(btnlayout) self.setLayout(vlayout) # Signals and slots self.button_reset.clicked.connect(self.main.reset_spyder) self.pages_widget.currentChanged.connect(self.current_page_changed) self.contents_widget.currentRowChanged.connect( self.pages_widget.setCurrentIndex) bbox.accepted.connect(self.accept) bbox.rejected.connect(self.reject) bbox.clicked.connect(self.button_clicked) # Ensures that the config is present on spyder first run CONF.set('main', 'interface_language', load_lang_conf()) def get_current_index(self): """Return current page index""" return self.contents_widget.currentRow() def set_current_index(self, index): """Set current page index""" self.contents_widget.setCurrentRow(index) def get_page(self, index=None): """Return page widget""" if index is None: widget = self.pages_widget.currentWidget() else: widget = self.pages_widget.widget(index) return widget.widget() @Slot() def accept(self): """Reimplement Qt method""" for index in range(self.pages_widget.count()): configpage = self.get_page(index) if not configpage.is_valid(): return configpage.apply_changes() QDialog.accept(self) def button_clicked(self, button): if button is self.apply_btn: # Apply button was clicked configpage = self.get_page() if not configpage.is_valid(): return configpage.apply_changes() def current_page_changed(self, index): widget = self.get_page(index) self.apply_btn.setVisible(widget.apply_callback is not None) self.apply_btn.setEnabled(widget.is_modified) def add_page(self, widget): self.check_settings.connect(widget.check_settings) widget.show_this_page.connect(lambda row=self.contents_widget.count(): self.contents_widget.setCurrentRow(row)) widget.apply_button_enabled.connect(self.apply_btn.setEnabled) scrollarea = QScrollArea(self) scrollarea.setWidgetResizable(True) scrollarea.setWidget(widget) self.pages_widget.addWidget(scrollarea) item = QListWidgetItem(self.contents_widget) item.setIcon(widget.get_icon()) item.setText(widget.get_name()) item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) item.setSizeHint(QSize(0, 25)) def check_all_settings(self): """This method is called to check all configuration page settings after configuration dialog has been shown""" self.check_settings.emit() def resizeEvent(self, event): """ Reimplement Qt method to be able to save the widget's size from the main application """ QDialog.resizeEvent(self, event) self.size_change.emit(self.size())
class HistoryLog(SpyderPluginWidget): """ History log widget """ CONF_SECTION = 'historylog' CONFIGWIDGET_CLASS = HistoryConfigPage focus_changed = Signal() def __init__(self, parent): self.tabwidget = None self.menu_actions = None self.dockviewer = None self.wrap_action = None self.editors = [] self.filenames = [] self.icons = [] if PYQT5: SpyderPluginWidget.__init__(self, parent, main=parent) else: SpyderPluginWidget.__init__(self, parent) # Initialize plugin self.initialize_plugin() layout = QVBoxLayout() self.tabwidget = Tabs(self, self.menu_actions) self.tabwidget.currentChanged.connect(self.refresh_plugin) self.tabwidget.move_data.connect(self.move_tab) if sys.platform == 'darwin': tab_container = QWidget() tab_container.setObjectName('tab-container') tab_layout = QHBoxLayout(tab_container) tab_layout.setContentsMargins(0, 0, 0, 0) tab_layout.addWidget(self.tabwidget) layout.addWidget(tab_container) else: layout.addWidget(self.tabwidget) # Menu as corner widget options_button = create_toolbutton(self, text=_('Options'), icon=ima.icon('tooloptions')) options_button.setPopupMode(QToolButton.InstantPopup) menu = QMenu(self) add_actions(menu, self.menu_actions) options_button.setMenu(menu) self.tabwidget.setCornerWidget(options_button) # Find/replace widget self.find_widget = FindReplace(self) self.find_widget.hide() self.register_widget_shortcuts("Editor", self.find_widget) layout.addWidget(self.find_widget) self.setLayout(layout) #------ SpyderPluginWidget API --------------------------------------------- def get_plugin_title(self): """Return widget title""" return _('History log') def get_plugin_icon(self): """Return widget icon""" return ima.icon('history') def get_focus_widget(self): """ Return the widget to give focus to when this plugin's dockwidget is raised on top-level """ return self.tabwidget.currentWidget() def closing_plugin(self, cancelable=False): """Perform actions before parent main window is closed""" return True def refresh_plugin(self): """Refresh tabwidget""" if self.tabwidget.count(): editor = self.tabwidget.currentWidget() else: editor = None self.find_widget.set_editor(editor) def get_plugin_actions(self): """Return a list of actions related to plugin""" history_action = create_action(self, _("History..."), None, ima.icon('history'), _("Set history maximum entries"), triggered=self.change_history_depth) self.wrap_action = create_action(self, _("Wrap lines"), toggled=self.toggle_wrap_mode) self.wrap_action.setChecked(self.get_option('wrap')) self.menu_actions = [history_action, self.wrap_action] return self.menu_actions def on_first_registration(self): """Action to be performed on first plugin registration""" self.main.tabify_plugins(self.main.extconsole, self) def register_plugin(self): """Register plugin in Spyder's main window""" self.focus_changed.connect(self.main.plugin_focus_changed) self.main.add_dockwidget(self) # self.main.console.set_historylog(self) self.main.console.shell.refresh.connect(self.refresh_plugin) def update_font(self): """Update font from Preferences""" color_scheme = self.get_color_scheme() font = self.get_plugin_font() for editor in self.editors: editor.set_font(font, color_scheme) def apply_plugin_settings(self, options): """Apply configuration file's plugin settings""" color_scheme_n = 'color_scheme_name' color_scheme_o = self.get_color_scheme() font_n = 'plugin_font' font_o = self.get_plugin_font() wrap_n = 'wrap' wrap_o = self.get_option(wrap_n) self.wrap_action.setChecked(wrap_o) for editor in self.editors: if font_n in options: scs = color_scheme_o if color_scheme_n in options else None editor.set_font(font_o, scs) elif color_scheme_n in options: editor.set_color_scheme(color_scheme_o) if wrap_n in options: editor.toggle_wrap_mode(wrap_o) #------ Private API -------------------------------------------------------- def move_tab(self, index_from, index_to): """ Move tab (tabs themselves have already been moved by the tabwidget) """ filename = self.filenames.pop(index_from) editor = self.editors.pop(index_from) icon = self.icons.pop(index_from) self.filenames.insert(index_to, filename) self.editors.insert(index_to, editor) self.icons.insert(index_to, icon) #------ Public API --------------------------------------------------------- def add_history(self, filename): """ Add new history tab Slot for add_history signal emitted by shell instance """ filename = encoding.to_unicode_from_fs(filename) if filename in self.filenames: return editor = codeeditor.CodeEditor(self) if osp.splitext(filename)[1] == '.py': language = 'py' icon = ima.icon('python') else: language = 'bat' icon = ima.icon('cmdprompt') editor.setup_editor(linenumbers=False, language=language, scrollflagarea=False) editor.focus_changed.connect(lambda: self.focus_changed.emit()) editor.setReadOnly(True) color_scheme = self.get_color_scheme() editor.set_font(self.get_plugin_font(), color_scheme) editor.toggle_wrap_mode(self.get_option('wrap')) text, _ = encoding.read(filename) editor.set_text(text) editor.set_cursor_position('eof') self.editors.append(editor) self.filenames.append(filename) self.icons.append(icon) index = self.tabwidget.addTab(editor, osp.basename(filename)) self.find_widget.set_editor(editor) self.tabwidget.setTabToolTip(index, filename) self.tabwidget.setTabIcon(index, icon) self.tabwidget.setCurrentIndex(index) def append_to_history(self, filename, command): """ Append an entry to history filename Slot for append_to_history signal emitted by shell instance """ if not is_text_string(filename): # filename is a QString filename = to_text_string(filename.toUtf8(), 'utf-8') command = to_text_string(command) index = self.filenames.index(filename) self.editors[index].append(command) if self.get_option('go_to_eof'): self.editors[index].set_cursor_position('eof') self.tabwidget.setCurrentIndex(index) @Slot() def change_history_depth(self): "Change history max entries" "" depth, valid = QInputDialog.getInteger(self, _('History'), _('Maximum entries'), self.get_option('max_entries'), 10, 10000) if valid: self.set_option('max_entries', depth) @Slot(bool) def toggle_wrap_mode(self, checked): """Toggle wrap mode""" if self.tabwidget is None: return for editor in self.editors: editor.toggle_wrap_mode(checked) self.set_option('wrap', checked)
class VariableExplorer(QWidget, SpyderPluginMixin): """ Variable Explorer Plugin """ CONF_SECTION = 'variable_explorer' CONFIGWIDGET_CLASS = VariableExplorerConfigPage sig_option_changed = Signal(str, object) def __init__(self, parent): QWidget.__init__(self, parent) SpyderPluginMixin.__init__(self, parent) # Widgets self.stack = QStackedWidget(self) self.shellwidgets = {} # Layout layout = QVBoxLayout() layout.addWidget(self.stack) self.setLayout(layout) # Initialize plugin self.initialize_plugin() @staticmethod def get_settings(): """ Return Variable Explorer settings dictionary (i.e. namespace browser settings according to Spyder's configuration file) """ settings = {} # CONF.load_from_ini() # necessary only when called from another process for name in REMOTE_SETTINGS: settings[name] = CONF.get(VariableExplorer.CONF_SECTION, name) return settings # ----- Stack accesors ---------------------------------------------------- def set_current_widget(self, nsb): self.stack.setCurrentWidget(nsb) def current_widget(self): return self.stack.currentWidget() def count(self): return self.stack.count() def remove_widget(self, nsb): self.stack.removeWidget(nsb) def add_widget(self, nsb): self.stack.addWidget(nsb) # ----- Public API -------------------------------------------------------- def add_shellwidget(self, shellwidget): shellwidget_id = id(shellwidget) # Add shell only once: this method may be called two times in a row # by the External console plugin (dev. convenience) from spyderlib.widgets.externalshell import systemshell if isinstance(shellwidget, systemshell.ExternalSystemShell): return if shellwidget_id not in self.shellwidgets: nsb = NamespaceBrowser(self) nsb.set_shellwidget(shellwidget) nsb.setup(**VariableExplorer.get_settings()) nsb.sig_option_changed.connect(self.sig_option_changed.emit) self.add_widget(nsb) self.shellwidgets[shellwidget_id] = nsb self.set_shellwidget_from_id(shellwidget_id) return nsb def remove_shellwidget(self, shellwidget_id): # If shellwidget_id is not in self.shellwidgets, it simply means # that shell was not a Python-based console (it was a terminal) if shellwidget_id in self.shellwidgets: nsb = self.shellwidgets.pop(shellwidget_id) self.remove_widget(nsb) nsb.close() def set_shellwidget_from_id(self, shellwidget_id): if shellwidget_id in self.shellwidgets: nsb = self.shellwidgets[shellwidget_id] self.set_current_widget(nsb) if self.isvisible: nsb.visibility_changed(True) def import_data(self, fname): """Import data in current namespace""" if self.count(): nsb = self.current_widget() nsb.refresh_table() nsb.import_data(filename=fname) if self.dockwidget and not self.ismaximized: self.dockwidget.setVisible(True) self.dockwidget.raise_() #------ SpyderPluginMixin API --------------------------------------------- def visibility_changed(self, enable): """DockWidget visibility has changed""" SpyderPluginMixin.visibility_changed(self, enable) for nsb in list(self.shellwidgets.values()): nsb.visibility_changed(enable and nsb is self.current_widget()) #------ SpyderPluginWidget API --------------------------------------------- def get_plugin_title(self): """Return widget title""" return _('Variable explorer') def get_plugin_icon(self): """Return plugin icon""" return ima.icon('dictedit') def get_focus_widget(self): """ Return the widget to give focus to when this plugin's dockwidget is raised on top-level """ return self.current_widget() def closing_plugin(self, cancelable=False): """Perform actions before parent main window is closed""" return True def refresh_plugin(self): """Refresh widget""" pass def get_plugin_actions(self): """Return a list of actions related to plugin""" return [] def register_plugin(self): """Register plugin in Spyder's main window""" self.main.extconsole.set_variableexplorer(self) self.main.add_dockwidget(self) def apply_plugin_settings(self, options): """Apply configuration file's plugin settings""" for nsb in list(self.shellwidgets.values()): nsb.setup(**VariableExplorer.get_settings()) ar_timeout = self.get_option('autorefresh/timeout') for shellwidget in self.main.extconsole.shellwidgets: shellwidget.set_autorefresh_timeout(ar_timeout)
class ProjectExplorer(ProjectExplorerWidget, SpyderPluginMixin): """Project explorer plugin""" CONF_SECTION = 'project_explorer' open_terminal = Signal(str) open_interpreter = Signal(str) pythonpath_changed = Signal() sig_projects_were_closed = Signal() create_module = Signal(str) edit = Signal(str) removed = Signal(str) removed_tree = Signal(str) renamed = Signal(str, str) redirect_stdio = Signal(bool) def __init__(self, parent=None): ProjectExplorerWidget.__init__( self, parent=parent, name_filters=self.get_option('name_filters'), show_all=self.get_option('show_all', False), show_hscrollbar=self.get_option('show_hscrollbar')) SpyderPluginMixin.__init__(self, parent) # Initialize plugin self.initialize_plugin() self.treewidget.header().hide() self.set_font(self.get_plugin_font()) self.load_config() #------ SpyderPluginWidget API --------------------------------------------- def get_plugin_title(self): """Return widget title""" return _("Project explorer") def get_focus_widget(self): """ Return the widget to give focus to when this plugin's dockwidget is raised on top-level """ return self.treewidget def get_plugin_actions(self): """Return a list of actions related to plugin""" new_project_act = create_action(self, text=_('New project...'), icon=ima.icon('project_expanded'), triggered=self.create_new_project) font_action = create_action(self, _("&Font..."), None, ima.icon('font'), _("Set font style"), triggered=self.change_font) self.treewidget.common_actions += (None, font_action) self.main.file_menu_actions.insert(1, new_project_act) return [] def register_plugin(self): """Register plugin in Spyder's main window""" self.main.pythonpath_changed() self.main.restore_scrollbar_position.connect( self.restore_scrollbar_position) self.pythonpath_changed.connect(self.main.pythonpath_changed) self.sig_projects_were_closed.connect(self.projects_were_closed) self.create_module.connect(self.main.editor.new) self.edit.connect(self.main.editor.load) self.removed.connect(self.main.editor.removed) self.removed_tree.connect(self.main.editor.removed_tree) self.renamed.connect(self.main.editor.renamed) self.main.editor.set_projectexplorer(self) self.main.add_dockwidget(self) self.sig_open_file.connect(self.main.open_file) def refresh_plugin(self): """Refresh project explorer widget""" pass def closing_plugin(self, cancelable=False): """Perform actions before parent main window is closed""" self.save_config() self.closing_widget() return True #------ Public API --------------------------------------------------------- @Slot() def create_new_project(self): """Create new project""" if self.dockwidget.isHidden(): self.dockwidget.show() self.dockwidget.raise_() if not self.treewidget.new_project(): # Notify dockwidget to schedule a repaint self.dockwidget.update() def projects_were_closed(self): """Project were just closed: checking if related files are opened in the editor and closing them""" for fname in self.main.editor.get_filenames(): if self.treewidget.workspace.is_file_in_closed_project(fname): self.main.editor.close_file_from_name(fname) @Slot() def change_font(self): """Change font""" font, valid = QFontDialog.getFont(self.get_plugin_font(), self, _("Select a new font")) if valid: self.set_font(font) self.set_plugin_font(font) def set_font(self, font): """Set project explorer widget font""" self.treewidget.setFont(font) def save_config(self): """Save configuration: opened projects & tree widget state""" self.set_option('workspace', self.get_workspace()) self.set_option('expanded_state', self.treewidget.get_expanded_state()) self.set_option('scrollbar_position', self.treewidget.get_scrollbar_position()) def load_config(self): """Load configuration: opened projects & tree widget state""" self.set_workspace(self.get_option('workspace', None)) expanded_state = self.get_option('expanded_state', None) # Sometimes the expanded state option may be truncated in .ini file # (for an unknown reason), in this case it would be converted to a # string by 'userconfig': if is_text_string(expanded_state): expanded_state = None if expanded_state is not None: self.treewidget.set_expanded_state(expanded_state) def restore_scrollbar_position(self): """Restoring scrollbar position after main window is visible""" scrollbar_pos = self.get_option('scrollbar_position', None) if scrollbar_pos is not None: self.treewidget.set_scrollbar_position(scrollbar_pos)