class BasePlugin(BasePluginMixin): """ Basic functionality for Spyder plugins. WARNING: Don't override any methods or attributes present here! """ # Use this signal to display a message in the status bar. # str: The message you want to display # int: Amount of time to display the message sig_show_status_message = Signal(str, int) # Use this signal to inform another plugin that a configuration # value has changed. sig_option_changed = Signal(str, object) def __init__(self, parent=None): super(BasePlugin, self).__init__(parent) # This is the plugin parent, which corresponds to the main # window. self.main = parent # Filesystem path to the root directory that contains the # plugin self.PLUGIN_PATH = self._get_plugin_path() # Connect signals to slots. self.sig_show_status_message.connect(self.show_status_message) self.sig_option_changed.connect(self.set_option) @Slot(str) @Slot(str, int) def show_status_message(self, message, timeout=0): """ Show message in main window's status bar. Parameters ---------- message: str Message to display in the status bar. timeout: int Amount of time to display the message. """ super(BasePlugin, self)._show_status_message(message, timeout) @Slot(str, object) def set_option(self, option, value, section=None, recursive_notification=True): """ Set an option in Spyder configuration file. Parameters ---------- option: str Name of the option (e.g. 'case_sensitive') value: bool, int, str, tuple, list, dict Value to save in configuration file, passed as a Python object. Notes ----- * Use sig_option_changed to call this method from widgets of the same or another plugin. * CONF_SECTION needs to be defined for this to work. """ super(BasePlugin, self)._set_option( option, value, section=section, recursive_notification=recursive_notification) def get_option(self, option, default=NoDefault, section=None): """ Get an option from Spyder configuration file. Parameters ---------- option: str Name of the option to get its value from. Returns ------- bool, int, str, tuple, list, dict Value associated with `option`. """ return super(BasePlugin, self)._get_option(option, default, section=section) def remove_option(self, option, section=None): """ Remove an option from the Spyder configuration file. Parameters ---------- option: Union[str, Tuple[str, ...]] A string or a Tuple of strings containing an option name to remove. section: Optional[str] Name of the section where the option belongs to. """ return super(BasePlugin, self)._remove_option(option, section=section) def starting_long_process(self, message): """ Show a message in main window's status bar and changes the mouse to Qt.WaitCursor when starting a long process. Parameters ---------- message: str Message to show in the status bar when the long process starts. """ super(BasePlugin, self)._starting_long_process(message) def ending_long_process(self, message=""): """ Clear main window's status bar after a long process and restore mouse to the OS deault. Parameters ---------- message: str Message to show in the status bar when the long process finishes. """ super(BasePlugin, self)._ending_long_process(message)
class Connection(QObject, Serializable, ConnectionBase): connection_completed = Signal(QObject) connection_made_incomplete = Signal(QObject) updated = Signal(QObject) def __init__(self, port_a: Port, port_b: Port = None, *, style: StyleCollection, converter: TypeConverter = None): super().__init__() self._uid = str(uuid.uuid4()) if port_a is None: raise ValueError('port_a is required') elif port_a is port_b: raise ValueError('Cannot connect a port to itself') if port_a.port_type == PortType.input: in_port = port_a out_port = port_b else: in_port = port_b out_port = port_a if in_port is not None and out_port is not None: if in_port.port_type == out_port.port_type: raise ValueError('Cannot connect two ports of the same type') self._ports = {PortType.input: in_port, PortType.output: out_port} if in_port and out_port: self._required_port = PortType.none elif in_port: self._required_port = PortType.output else: self._required_port = PortType.input self._last_hovered_node = None self._converter = converter self._style = style self._connection_geometry = ConnectionGeometry(style) self._graphics_object = None def _cleanup(self): if self.is_complete: self.connection_made_incomplete.emit(self) self.propagate_empty_data() self.last_hovered_node = None for port_type, port in self.valid_ports.items(): if port.node.graphics_object is not None: port.node.graphics_object.update() self._ports[port] = None if self._graphics_object is not None: self._graphics_object._cleanup() self._graphics_object = None def __del__(self): try: self._cleanup() except Exception: ... @property def style(self) -> StyleCollection: return self._style def __getstate__(self) -> dict: """ save Returns ------- value : dict """ in_port, out_port = self.ports if not in_port and not out_port: return {} connection_json = dict( in_id=in_port.node.id, in_index=in_port.index, out_id=out_port.node.id, out_index=out_port.index, ) if self._converter: def get_type_json(type: PortType): node_type = self.data_type(type) return dict(id=node_type.id, name=node_type.name) connection_json["converter"] = { "in": get_type_json(PortType.input), "out": get_type_json(PortType.output), } return connection_json @property def id(self) -> str: """ Unique identifier (uuid) Returns ------- uuid : str """ return self._uid @property def required_port(self) -> PortType: """ Required port Returns ------- value : PortType """ return self._required_port @required_port.setter def required_port(self, dragging: PortType): """ Remembers the end being dragged. Invalidates Node address. Grabs mouse. Parameters ---------- dragging : PortType """ self._required_port = dragging try: port = self.valid_ports[dragging] except KeyError: ... else: port.remove_connection(self) @property def graphics_object(self) -> ConnectionGraphicsObject: """ Get the connection graphics object Returns ---------- graphics : ConnectionGraphicsObject """ return self._graphics_object @graphics_object.setter def graphics_object(self, graphics: ConnectionGraphicsObject): self._graphics_object = graphics # this function is only called when the ConnectionGraphicsObject is # newly created. At self moment both end coordinates are (0, 0) in # Connection G.O. coordinates. The position of the whole Connection GO # in scene coordinate system is also (0, 0). By moving the whole # object to the Node Port position we position both connection ends # correctly. if self.required_port != PortType.none: attached_port = opposite_port(self.required_port) attached_port_index = self.get_port_index(attached_port) node = self.get_node(attached_port) node_scene_transform = node.graphics_object.sceneTransform() pos = node.geometry.port_scene_position(attached_port, attached_port_index, node_scene_transform) self._graphics_object.setPos(pos) self._graphics_object.move() def connect_to(self, port: Port): """ Assigns a node to the required port. Parameters ---------- port : Port """ if self._ports[port.port_type] is not None: raise ValueError('Port already specified') was_incomplete = not self.is_complete self._ports[port.port_type] = port self.updated.emit(self) self.required_port = PortType.none if self.is_complete and was_incomplete: self.connection_completed.emit(self) def remove_from_nodes(self): for port in self._ports.values(): if port is not None: port.remove_connection(self) @property def geometry(self) -> ConnectionGeometry: """ Connection geometry Returns ------- value : ConnectionGeometry """ return self._connection_geometry def get_node(self, port_type: PortType) -> Node: """ Get node Parameters ---------- port_type : PortType Returns ------- value : Node """ port = self._ports[port_type] if port is not None: return port.node @property def nodes(self): # TODO namedtuple; TODO order return (self.get_node(PortType.input), self.get_node(PortType.output)) @property def ports(self): # TODO namedtuple; TODO order return (self._ports[PortType.input], self._ports[PortType.output]) def get_port_index(self, port_type: PortType) -> int: """ Get port index Parameters ---------- port_type : PortType Returns ------- index : int """ return self._ports[port_type].index def clear_node(self, port_type: PortType): """ Clear node Parameters ---------- port_type : PortType """ if self.is_complete: self.connection_made_incomplete.emit(self) port = self._ports[port_type] self._ports[port_type] = None port.remove_connection(self) @property def valid_ports(self): return { port_type: port for port_type, port in self._ports.items() if port is not None } def data_type(self, port_type: PortType) -> NodeDataType: """ Data type Parameters ---------- port_type : PortType Returns ------- value : NodeDataType """ ports = self.valid_ports if not ports: raise ValueError('No ports set') try: return ports[port_type].data_type except KeyError: valid_type, = ports return ports[valid_type].data_type @property def type_converter(self) -> TypeConverter: """ Set type converter Returns ------- converter : TypeConverter """ return self._converter @type_converter.setter def type_converter(self, converter: TypeConverter): self._converter = converter @property def is_complete(self) -> bool: """ Connection is complete - in/out nodes are set Returns ------- value : bool """ return all(self._ports.values()) def propagate_data(self, node_data: NodeData): """ Propagate the given data from the output port -> input port. Parameters ---------- node_data : NodeData """ in_port, out_port = self.ports if not in_port: return if self._converter: node_data = self._converter(node_data) in_port.node.propagate_data(node_data, in_port) @property def input_node(self) -> Node: 'Input node' return self._ports[PortType.input].node @property def output(self) -> Node: 'Output node' return self._ports[PortType.output].node def propagate_empty_data(self): self.propagate_data(None) @property def last_hovered_node(self) -> Node: """ Last hovered node Returns ------- value : Node """ return self._last_hovered_node @last_hovered_node.setter def last_hovered_node(self, node: Node): """ Set last hovered node Parameters ---------- node : Node """ if node is None and self._last_hovered_node: self._last_hovered_node.reset_reaction_to_connection() self._last_hovered_node = node def interact_with_node(self, node: Node): """ Interact with node Parameters ---------- node : Node """ self.last_hovered_node = node @property def requires_port(self) -> bool: """ Requires port Returns ------- value : bool """ return self._required_port != PortType.none
class SearchThread(QThread): """Find in files search thread""" sig_finished = Signal(bool) sig_current_file = Signal(str) sig_current_folder = Signal(str) sig_file_match = Signal(tuple, int) sig_out_print = Signal(object) def __init__(self, parent): QThread.__init__(self, parent) self.mutex = QMutex() self.stopped = None self.results = None self.pathlist = None self.total_matches = None self.error_flag = None self.rootpath = None self.python_path = None self.hg_manifest = None self.exclude = None self.texts = None self.text_re = None self.completed = None self.case_sensitive = True self.get_pythonpath_callback = None self.results = {} self.total_matches = 0 self.is_file = False def initialize(self, path, is_file, exclude, texts, text_re, case_sensitive): self.rootpath = path self.python_path = False self.hg_manifest = False if exclude: self.exclude = re.compile(exclude) self.texts = texts self.text_re = text_re self.is_file = is_file self.stopped = False self.completed = False self.case_sensitive = case_sensitive def run(self): try: self.filenames = [] if self.is_file: self.find_string_in_file(self.rootpath) else: self.find_files_in_path(self.rootpath) except Exception: # Important note: we have to handle unexpected exceptions by # ourselves because they won't be catched by the main thread # (known QThread limitation/bug) traceback.print_exc() self.error_flag = _("Unexpected error: see internal console") self.stop() self.sig_finished.emit(self.completed) def stop(self): with QMutexLocker(self.mutex): self.stopped = True def find_files_in_path(self, path): if self.pathlist is None: self.pathlist = [] self.pathlist.append(path) for path, dirs, files in os.walk(path): with QMutexLocker(self.mutex): if self.stopped: return False try: for d in dirs[:]: with QMutexLocker(self.mutex): if self.stopped: return False dirname = os.path.join(path, d) if self.exclude \ and re.search(self.exclude, dirname + os.sep): dirs.remove(d) for f in files: with QMutexLocker(self.mutex): if self.stopped: return False filename = os.path.join(path, f) if self.exclude and re.search(self.exclude, filename): continue if is_text_file(filename): self.find_string_in_file(filename) except re.error: self.error_flag = _("invalid regular expression") return False return True def find_string_in_file(self, fname): self.error_flag = False self.sig_current_file.emit(fname) try: for lineno, line in enumerate(open(fname, 'rb')): for text, enc in self.texts: with QMutexLocker(self.mutex): if self.stopped: return False line_search = line if not self.case_sensitive: line_search = line_search.lower() if self.text_re: found = re.search(text, line_search) if found is not None: break else: found = line_search.find(text) if found > -1: break try: line_dec = line.decode(enc) except UnicodeDecodeError: line_dec = line if not self.case_sensitive: line = line.lower() if self.text_re: for match in re.finditer(text, line): with QMutexLocker(self.mutex): if self.stopped: return False self.total_matches += 1 self.sig_file_match.emit( (osp.abspath(fname), lineno + 1, match.start(), match.end(), line_dec), self.total_matches) else: found = line.find(text) while found > -1: with QMutexLocker(self.mutex): if self.stopped: return False self.total_matches += 1 self.sig_file_match.emit( (osp.abspath(fname), lineno + 1, found, found + len(text), line_dec), self.total_matches) for text, enc in self.texts: found = line.find(text, found + 1) if found > -1: break except IOError as xxx_todo_changeme: (_errno, _strerror) = xxx_todo_changeme.args self.error_flag = _("permission denied errors were encountered") self.completed = True def get_results(self): return self.results, self.pathlist, self.total_matches, self.error_flag
class WorkerUpdates(QObject): """ Worker that checks for releases using the Github API without blocking the TRex user interface, in case of connections issues. """ sig_ready = Signal() def __init__(self, parent, startup): QObject.__init__(self) self._parent = parent self.error = None self.latest_release = None self.startup = startup def check_update_available(self, version, releases): """Checks if there is an update available. It takes as parameters the current version of TRex and a list of valid cleaned releases in chronological order (what github api returns by default). Example: ['2.3.4', '2.3.3' ...] """ if is_stable_version(version): # Remove non stable versions from the list releases = [r for r in releases if is_stable_version(r)] latest_release = releases[0] if version.endswith('dev'): return (False, latest_release) return (check_version(version, latest_release, '<'), latest_release) def start(self): """Main method of the WorkerUpdates worker""" self.url = 'https://api.github.com/repos/novaya/trex/releases' self.update_available = False self.latest_release = __version__ error_msg = None try: if hasattr(ssl, '_create_unverified_context'): # Fix for issue # 2685 [Works only with Python >=2.7.9] # More info: https://www.python.org/dev/peps/pep-0476/#opting-out context = ssl._create_unverified_context() page = urlopen(self.url, context=context) else: page = urlopen(self.url) try: data = page.read() # Needed step for python3 compatibility if not isinstance(data, str): data = data.decode() data = json.loads(data) releases = [item['tag_name'].replace('v', '') for item in data] version = __version__ result = self.check_update_available(version, releases) self.update_available, self.latest_release = result except Exception: error_msg = _('Unable to retrieve information.') except HTTPError: error_msg = _('Unable to retrieve information.') except URLError: error_msg = _('Unable to connect to the internet. <br><br>Make ' 'sure the connection is working properly.') except Exception: error_msg = _('Unable to check for updates.') # Don't show dialog when starting up trex and an error occur if not (self.startup and error_msg is not None): self.error = error_msg self.sig_ready.emit()
class FoldingPanel(Panel): """ Displays the document outline and lets the user collapse/expand blocks. The data represented by the panel come from the text block user state and is set by the SyntaxHighlighter mode. The panel does not expose any function that you can use directly. To interact with the fold tree, you need to modify text block fold level or trigger state using :class:`spyder.api.utils.TextBlockHelper` or :mod:`spyder.api.folding` """ #: signal emitted when a fold trigger state has changed, parameters are #: the concerned text block and the new state (collapsed or not). trigger_state_changed = Signal(QTextBlock, bool) collapse_all_triggered = Signal() expand_all_triggered = Signal() @property def native_icons(self): """ Defines whether the panel will use native indicator icons or use custom ones. If you want to use custom indicator icons, you must first set this flag to False. """ return self.native_icons @native_icons.setter def native_icons(self, value): self._native_icons = value # propagate changes to every clone if self.editor: for clone in self.editor.clones: try: clone.modes.get(self.__class__).native_icons = value except KeyError: # this should never happen since we're working with clones pass @property def indicators_icons(self): """ Gets/sets the icons for the fold indicators. The list of indicators is interpreted as follow:: (COLLAPSED_OFF, COLLAPSED_ON, EXPANDED_OFF, EXPANDED_ON) To use this property you must first set `native_icons` to False. :returns: tuple(str, str, str, str) """ return self._indicators_icons @indicators_icons.setter def indicators_icons(self, value): if len(value) != 4: raise ValueError('The list of custom indicators must contains 4 ' 'strings') self._indicators_icons = value if self.editor: # propagate changes to every clone for clone in self.editor.clones: try: clone.modes.get(self.__class__).indicators_icons = value except KeyError: # this should never happen since we're working with clones pass @property def highlight_caret_scope(self): """ True to highlight the caret scope automatically. (Similar to the ``Highlight blocks in Qt Creator``. Default is False. """ return self._highlight_caret @highlight_caret_scope.setter def highlight_caret_scope(self, value): if value != self._highlight_caret: self._highlight_caret = value if self.editor: if value: self._block_nbr = -1 self.editor.cursorPositionChanged.connect( self._highlight_caret_scope) else: self._block_nbr = -1 self.editor.cursorPositionChanged.disconnect( self._highlight_caret_scope) for clone in self.editor.clones: try: clone.modes.get( self.__class__).highlight_caret_scope = value except KeyError: # this should never happen since we're working with # clones pass def __init__(self, highlight_caret_scope=False): Panel.__init__(self) self._native_icons = False self._indicators_icons = ('folding.arrow_right_off', 'folding.arrow_right_on', 'folding.arrow_down_off', 'folding.arrow_down_on') self._block_nbr = -1 self._highlight_caret = False self.highlight_caret_scope = highlight_caret_scope self._indic_size = 16 #: the list of deco used to highlight the current fold region ( #: surrounding regions are darker) self._scope_decos = [] #: the list of folded blocs decorations self._block_decos = [] self.setMouseTracking(True) self.scrollable = True self._mouse_over_line = None self._current_scope = None self._prev_cursor = None self.context_menu = None self.action_collapse = None self.action_expand = None self.action_collapse_all = None self.action_expand_all = None self._original_background = None self._highlight_runner = DelayJobRunner(delay=250) def sizeHint(self): """Returns the widget size hint (based on the editor font size) """ fm = QFontMetricsF(self.editor.font()) size_hint = QSize(fm.height(), fm.height()) if size_hint.width() > 16: size_hint.setWidth(16) return size_hint def paintEvent(self, event): # Paints the fold indicators and the possible fold region background # on the folding panel. super(FoldingPanel, self).paintEvent(event) painter = QPainter(self) # Draw background over the selected non collapsed fold region if self._mouse_over_line is not None: block = self.editor.document().findBlockByNumber( self._mouse_over_line) try: self._draw_fold_region_background(block, painter) except ValueError: pass # Draw fold triggers for top_position, line_number, block in self.editor.visible_blocks: if TextBlockHelper.is_fold_trigger(block): collapsed = TextBlockHelper.is_collapsed(block) mouse_over = self._mouse_over_line == line_number self._draw_fold_indicator(top_position, mouse_over, collapsed, painter) if collapsed: # check if the block already has a decoration, it might # have been folded by the parent editor/document in the # case of cloned editor for deco in self._block_decos: if deco.block == block: # no need to add a deco, just go to the next block break else: self._add_fold_decoration(block, FoldScope(block)) else: for deco in self._block_decos: # check if the block decoration has been removed, it # might have been unfolded by the parent # editor/document in the case of cloned editor if deco.block == block: # remove it and self._block_decos.remove(deco) self.editor.decorations.remove(deco) del deco break def _draw_fold_region_background(self, block, painter): """ Draw the fold region when the mouse is over and non collapsed indicator. :param top: Top position :param block: Current block. :param painter: QPainter """ r = FoldScope(block) th = TextHelper(self.editor) start, end = r.get_range(ignore_blank_lines=True) if start > 0: top = th.line_pos_from_number(start) else: top = 0 bottom = th.line_pos_from_number(end + 1) h = bottom - top if h == 0: h = self.sizeHint().height() w = self.sizeHint().width() self._draw_rect(QRectF(0, top, w, h), painter) def _draw_rect(self, rect, painter): """ Draw the background rectangle using the current style primitive color. :param rect: The fold zone rect to draw :param painter: The widget's painter. """ c = self.editor.sideareas_color grad = QLinearGradient(rect.topLeft(), rect.topRight()) if sys.platform == 'darwin': grad.setColorAt(0, c.lighter(100)) grad.setColorAt(1, c.lighter(110)) outline = c.darker(110) else: grad.setColorAt(0, c.lighter(110)) grad.setColorAt(1, c.lighter(130)) outline = c.darker(100) painter.fillRect(rect, grad) painter.setPen(QPen(outline)) painter.drawLine(rect.topLeft() + QPointF(1, 0), rect.topRight() - QPointF(1, 0)) painter.drawLine(rect.bottomLeft() + QPointF(1, 0), rect.bottomRight() - QPointF(1, 0)) painter.drawLine(rect.topRight() + QPointF(0, 1), rect.bottomRight() - QPointF(0, 1)) painter.drawLine(rect.topLeft() + QPointF(0, 1), rect.bottomLeft() - QPointF(0, 1)) def _draw_fold_indicator(self, top, mouse_over, collapsed, painter): """ Draw the fold indicator/trigger (arrow). :param top: Top position :param mouse_over: Whether the mouse is over the indicator :param collapsed: Whether the trigger is collapsed or not. :param painter: QPainter """ rect = QRect(0, top, self.sizeHint().width(), self.sizeHint().height()) if self._native_icons: opt = QStyleOptionViewItem() opt.rect = rect opt.state = (QStyle.State_Active | QStyle.State_Item | QStyle.State_Children) if not collapsed: opt.state |= QStyle.State_Open if mouse_over: opt.state |= (QStyle.State_MouseOver | QStyle.State_Enabled | QStyle.State_Selected) opt.palette.setBrush(QPalette.Window, self.palette().highlight()) opt.rect.translate(-2, 0) self.style().drawPrimitive(QStyle.PE_IndicatorBranch, opt, painter, self) else: index = 0 if not collapsed: index = 2 if mouse_over: index += 1 ima.icon(self._indicators_icons[index]).paint(painter, rect) @staticmethod def find_parent_scope(block): """Find parent scope, if the block is not a fold trigger.""" original = block if not TextBlockHelper.is_fold_trigger(block): # search level of next non blank line while block.text().strip() == '' and block.isValid(): block = block.next() ref_lvl = TextBlockHelper.get_fold_lvl(block) - 1 block = original while (block.blockNumber() and (not TextBlockHelper.is_fold_trigger(block) or TextBlockHelper.get_fold_lvl(block) > ref_lvl)): block = block.previous() return block def _clear_scope_decos(self): """Clear scope decorations (on the editor)""" for deco in self._scope_decos: self.editor.decorations.remove(deco) self._scope_decos[:] = [] def _get_scope_highlight_color(self): """ Gets the base scope highlight color (derivated from the editor background) For lighter themes will be a darker color, and for darker ones will be a lighter color """ color = self.editor.sideareas_color if color.lightness() < 128: color = drift_color(color, 130) else: color = drift_color(color, 105) return color def _decorate_block(self, start, end): """ Create a decoration and add it to the editor. Args: start (int) start line of the decoration end (int) end line of the decoration """ color = self._get_scope_highlight_color() draw_order = DRAW_ORDERS.get('codefolding') d = TextDecoration(self.editor.document(), start_line=start, end_line=end + 1, draw_order=draw_order) d.set_background(color) d.set_full_width(True, clear=False) self.editor.decorations.add(d) self._scope_decos.append(d) def _highlight_block(self, block): """ Highlights the current fold scope. :param block: Block that starts the current fold scope. """ scope = FoldScope(block) if (self._current_scope is None or self._current_scope.get_range() != scope.get_range()): self._current_scope = scope self._clear_scope_decos() # highlight current scope with darker or lighter color start, end = scope.get_range() if not TextBlockHelper.is_collapsed(block): self._decorate_block(start, end) def mouseMoveEvent(self, event): """ Detect mouser over indicator and highlight the current scope in the editor (up and down decoration arround the foldable text when the mouse is over an indicator). :param event: event """ super(FoldingPanel, self).mouseMoveEvent(event) th = TextHelper(self.editor) line = th.line_nbr_from_position(event.pos().y()) if line >= 0: block = FoldScope.find_parent_scope( self.editor.document().findBlockByNumber(line - 1)) if TextBlockHelper.is_fold_trigger(block): if self._mouse_over_line is None: # mouse enter fold scope QApplication.setOverrideCursor( QCursor(Qt.PointingHandCursor)) if self._mouse_over_line != block.blockNumber() and \ self._mouse_over_line is not None: # fold scope changed, a previous block was highlighter so # we quickly update our highlighting self._mouse_over_line = block.blockNumber() self._highlight_block(block) else: # same fold scope, request highlight self._mouse_over_line = block.blockNumber() self._highlight_runner.request_job(self._highlight_block, block) self._highight_block = block else: # no fold scope to highlight, cancel any pending requests self._highlight_runner.cancel_requests() self._mouse_over_line = None QApplication.restoreOverrideCursor() self.repaint() def leaveEvent(self, event): """ Removes scope decorations and background from the editor and the panel if highlight_caret_scope, else simply update the scope decorations to match the caret scope. """ super(FoldingPanel, self).leaveEvent(event) QApplication.restoreOverrideCursor() self._highlight_runner.cancel_requests() if not self.highlight_caret_scope: self._clear_scope_decos() self._mouse_over_line = None self._current_scope = None else: self._block_nbr = -1 self._highlight_caret_scope() self.editor.repaint() def _add_fold_decoration(self, block, region): """ Add fold decorations (boxes arround a folded block in the editor widget). """ draw_order = DRAW_ORDERS.get('codefolding') deco = TextDecoration(block, draw_order=draw_order) deco.signals.clicked.connect(self._on_fold_deco_clicked) deco.tooltip = region.text(max_lines=25) deco.block = block deco.select_line() deco.set_outline(drift_color(self._get_scope_highlight_color(), 110)) deco.set_background(self._get_scope_highlight_color()) deco.set_foreground(QColor('#808080')) self._block_decos.append(deco) self.editor.decorations.add(deco) def toggle_fold_trigger(self, block): """ Toggle a fold trigger block (expand or collapse it). :param block: The QTextBlock to expand/collapse """ if not TextBlockHelper.is_fold_trigger(block): return region = FoldScope(block) if region.collapsed: region.unfold() if self._mouse_over_line is not None: self._decorate_block(*region.get_range()) else: region.fold() self._clear_scope_decos() self._refresh_editor_and_scrollbars() self.trigger_state_changed.emit(region._trigger, region.collapsed) def mousePressEvent(self, event): """Folds/unfolds the pressed indicator if any.""" if self._mouse_over_line is not None: block = self.editor.document().findBlockByNumber( self._mouse_over_line) self.toggle_fold_trigger(block) def _on_fold_deco_clicked(self, deco): """Unfold a folded block that has just been clicked by the user""" self.toggle_fold_trigger(deco.block) def on_state_changed(self, state): """ On state changed we (dis)connect to the cursorPositionChanged signal """ if state: self.editor.key_pressed.connect(self._on_key_pressed) if self._highlight_caret: self.editor.cursorPositionChanged.connect( self._highlight_caret_scope) self._block_nbr = -1 self.editor.new_text_set.connect(self._clear_block_deco) else: self.editor.key_pressed.disconnect(self._on_key_pressed) if self._highlight_caret: self.editor.cursorPositionChanged.disconnect( self._highlight_caret_scope) self._block_nbr = -1 self.editor.new_text_set.disconnect(self._clear_block_deco) def _on_key_pressed(self, event): """ Override key press to select the current scope if the user wants to deleted a folded scope (without selecting it). """ delete_request = event.key() in [Qt.Key_Backspace, Qt.Key_Delete] if event.text() or delete_request: cursor = self.editor.textCursor() if cursor.hasSelection(): # change selection to encompass the whole scope. positions_to_check = cursor.selectionStart( ), cursor.selectionEnd() else: positions_to_check = (cursor.position(), ) for pos in positions_to_check: block = self.editor.document().findBlock(pos) th = TextBlockHelper() if th.is_fold_trigger(block) and th.is_collapsed(block): self.toggle_fold_trigger(block) if delete_request and cursor.hasSelection(): scope = FoldScope(self.find_parent_scope(block)) tc = TextHelper( self.editor).select_lines(*scope.get_range()) if tc.selectionStart() > cursor.selectionStart(): start = cursor.selectionStart() else: start = tc.selectionStart() if tc.selectionEnd() < cursor.selectionEnd(): end = cursor.selectionEnd() else: end = tc.selectionEnd() tc.setPosition(start) tc.setPosition(end, tc.KeepAnchor) self.editor.setTextCursor(tc) @staticmethod def _show_previous_blank_lines(block): """ Show the block previous blank lines """ # set previous blank lines visibles pblock = block.previous() while (pblock.text().strip() == '' and pblock.blockNumber() >= 0): pblock.setVisible(True) pblock = pblock.previous() def refresh_decorations(self, force=False): """ Refresh decorations colors. This function is called by the syntax highlighter when the style changed so that we may update our decorations colors according to the new style. """ cursor = self.editor.textCursor() if (self._prev_cursor is None or force or self._prev_cursor.blockNumber() != cursor.blockNumber()): for deco in self._block_decos: self.editor.decorations.remove(deco) for deco in self._block_decos: deco.set_outline( drift_color(self._get_scope_highlight_color(), 110)) deco.set_background(self._get_scope_highlight_color()) self.editor.decorations.add(deco) self._prev_cursor = cursor def _refresh_editor_and_scrollbars(self): """ Refrehes editor content and scollbars. We generate a fake resize event to refresh scroll bar. We have the same problem as described here: http://www.qtcentre.org/threads/44803 and we apply the same solution (don't worry, there is no visual effect, the editor does not grow up at all, even with a value = 500) """ TextHelper(self.editor).mark_whole_doc_dirty() self.editor.repaint() s = self.editor.size() s.setWidth(s.width() + 1) self.editor.resizeEvent(QResizeEvent(self.editor.size(), s)) def collapse_all(self): """ Collapses all triggers and makes all blocks with fold level > 0 invisible. """ self._clear_block_deco() block = self.editor.document().firstBlock() last = self.editor.document().lastBlock() while block.isValid(): lvl = TextBlockHelper.get_fold_lvl(block) trigger = TextBlockHelper.is_fold_trigger(block) if trigger: if lvl == 0: self._show_previous_blank_lines(block) TextBlockHelper.set_collapsed(block, True) block.setVisible(lvl == 0) if block == last and block.text().strip() == '': block.setVisible(True) self._show_previous_blank_lines(block) block = block.next() self._refresh_editor_and_scrollbars() tc = self.editor.textCursor() tc.movePosition(tc.Start) self.editor.setTextCursor(tc) self.collapse_all_triggered.emit() def _clear_block_deco(self): """Clear the folded block decorations.""" for deco in self._block_decos: self.editor.decorations.remove(deco) self._block_decos[:] = [] def expand_all(self): """Expands all fold triggers.""" block = self.editor.document().firstBlock() while block.isValid(): TextBlockHelper.set_collapsed(block, False) block.setVisible(True) block = block.next() self._clear_block_deco() self._refresh_editor_and_scrollbars() self.expand_all_triggered.emit() def _on_action_toggle(self): """Toggle the current fold trigger.""" block = FoldScope.find_parent_scope(self.editor.textCursor().block()) self.toggle_fold_trigger(block) def _on_action_collapse_all_triggered(self): """Closes all top levels fold triggers recursively.""" self.collapse_all() def _on_action_expand_all_triggered(self): """ Expands all fold triggers. :return: """ self.expand_all() def _highlight_caret_scope(self): """ Highlight the scope of the current caret position. This get called only if :attr:` spyder.widgets.panels.FoldingPanel.highlight_care_scope` is True. """ cursor = self.editor.textCursor() block_nbr = cursor.blockNumber() if self._block_nbr != block_nbr: block = FoldScope.find_parent_scope( self.editor.textCursor().block()) try: s = FoldScope(block) except ValueError: self._clear_scope_decos() else: self._mouse_over_line = block.blockNumber() if TextBlockHelper.is_fold_trigger(block): self._highlight_block(block) self._block_nbr = block_nbr def clone_settings(self, original): self.native_icons = original.native_icons self.indicators_icons = original.indicators_icons self.highlight_caret_scope = original.highlight_caret_scope self.custom_fold_region_background = \ original.custom_fold_region_background
class TCPClient(QObject): """ PyQt5 object initializing a TCP socket client. Can be used by any module but is a builtin functionnality of all actuators and detectors of PyMoDAQ The module should init TCPClient, move it in a thread and communicate with it using a custom signal connected to TCPClient.queue_command slot. The module should also connect TCPClient.cmd_signal to one of its methods inorder to get info/data back from the client The client itself communicate with a TCP server, it is best to use a server object subclassing the TCPServer class defined within this python module """ cmd_signal = Signal( ThreadCommand ) # signal to connect with a module slot in order to start communication back params = [] def __init__(self, ipaddress="192.168.1.62", port=6341, params_state=None, client_type="GRABBER"): """Create a socket client particularly fit to be used with PyMoDAQ's TCPServer Parameters ---------- ipaddress: (str) the IP address of the server port: (int) the port where to communicate with the server params_state: (dict) state of the Parameter settings of the module instantiating this client and wishing to export its settings to the server. Obtained from param.saveState() where param is an instance of Parameter object, see pyqtgraph.parametertree::Parameter client_type: (str) should be one of the accepted client_type by the TCPServer instance (within pymodaq it is either 'GRABBER' or 'ACTUATOR' """ super().__init__() self.ipaddress = ipaddress self.port = port self._socket = None self.connected = False self.settings = Parameter.create(name='Settings', type='group', children=self.params) if params_state is not None: if isinstance(params_state, dict): self.settings.restoreState(params_state) elif isinstance(params_state, Parameter): self.settings.restoreState(params_state.saveState()) self.client_type = client_type # "GRABBER" or "ACTUATOR" @property def socket(self): return self._socket @socket.setter def socket(self, sock): self._socket = sock def close(self): if self.socket is not None: self.socket.close() def send_data(self, data_list): # first send 'Done' and then send the length of the list if self.socket is not None and isinstance(data_list, list): self.socket.send_string('Done') self.socket.send_list(data_list) def send_infos_xml(self, infos): if self.socket is not None: self.socket.send_string('Infos') self.socket.send_string(infos) def send_info_string(self, info_to_display, value_as_string): if self.socket is not None: self.socket.send_string('Info') # the command self.socket.send_string( info_to_display) # the actual info to display as a string if not isinstance(value_as_string, str): value_as_string = str(value_as_string) self.socket.send_string(value_as_string) @Slot(ThreadCommand) def queue_command(self, command=ThreadCommand()): """ when this TCPClient object is within a thread, the corresponding module communicate with it with signal and slots from module to client: module_signal to queue_command slot from client to module: self.cmd_signal to a module slot """ if command.command == "ini_connection": status = self.init_connection() elif command.command == "quit": try: self.socket.close() except Exception as e: pass finally: self.cmd_signal.emit(ThreadCommand('disconnected')) elif command.command == 'update_connection': self.ipaddress = command.attributes['ipaddress'] self.port = command.attributes['port'] elif command.command == 'data_ready': self.data_ready(command.attributes) elif command.command == 'send_info': if self.socket is not None: path = command.attributes['path'] param = command.attributes['param'] self.socket.send_string('Info_xml') self.socket.send_list(path) # send value data = pymodaq.daq_utils.parameter.ioxml.parameter_to_xml_string( param) self.socket.send_string(data) elif command.command == 'position_is': if self.socket is not None: self.socket.send_string('position_is') self.socket.send_scalar(command.attributes[0]) elif command.command == 'move_done': if self.socket is not None: self.socket.send_string('move_done') self.socket.send_scalar(command.attributes[0]) elif command.command == 'x_axis': if self.socket is not None: self.socket.send_string('x_axis') x_axis = dict(label='', units='') if isinstance(command.attributes[0], np.ndarray): x_axis['data'] = command.attributes[0] elif isinstance(command.attributes[0], dict): x_axis.update(command.attributes[0].copy()) self.socket.send_array(x_axis['data']) self.socket.send_string(x_axis['label']) self.socket.send_string(x_axis['units']) elif command.command == 'y_axis': if self.socket is not None: self.socket.send_string('y_axis') y_axis = dict(label='', units='') if isinstance(command.attributes[0], np.ndarray): y_axis['data'] = command.attributes[0] elif isinstance(command.attributes[0], dict): y_axis.update(command.attributes[0].copy()) self.socket.send_array(y_axis['data']) self.socket.send_string(y_axis['label']) self.socket.send_string(y_axis['units']) else: raise IOError('Unknown TCP client command') def init_connection(self, extra_commands=[]): # %% try: # create an INET, STREAMing socket self.socket = Socket( socket.socket(socket.AF_INET, socket.SOCK_STREAM)) # now connect to the web server on port 80 - the normal http port self.socket.connect((self.ipaddress, self.port)) self.cmd_signal.emit(ThreadCommand('connected')) self.socket.send_string(self.client_type) self.send_infos_xml( pymodaq.daq_utils.parameter.ioxml.parameter_to_xml_string( self.settings)) for command in extra_commands: if isinstance(command, ThreadCommand): self.cmd_signal.emit(command) self.connected = True # %% while True: try: ready_to_read, ready_to_write, in_error = \ select.select([self.socket.socket], [self.socket.socket], [self.socket.socket], 0) if len(ready_to_read) != 0: message = self.socket.get_string() # print(message) self.get_data(message) if len(in_error) != 0: self.connected = False self.cmd_signal.emit(ThreadCommand('disconnected')) QtWidgets.QApplication.processEvents() except Exception as e: try: self.cmd_signal.emit( ThreadCommand('Update_Status', [getLineInfo() + str(e), 'log'])) self.socket.send_string('Quit') self.socket.close() except Exception: # pragma: no cover pass finally: break except ConnectionRefusedError as e: self.connected = False self.cmd_signal.emit(ThreadCommand('disconnected')) self.cmd_signal.emit( ThreadCommand('Update_Status', [getLineInfo() + str(e), 'log'])) def get_data(self, message): """ Parameters ---------- message Returns ------- """ if self.socket is not None: messg = ThreadCommand(message) if message == 'set_info': path = self.socket.get_list() param_xml = self.socket.get_string() messg.attributes = [path, param_xml] elif message == 'move_abs': position = self.socket.get_scalar() messg.attributes = [position] elif message == 'move_rel': position = self.socket.get_scalar() messg.attributes = [position] self.cmd_signal.emit(messg) @Slot(list) def data_ready(self, datas): self.send_data( datas[0]['data'] ) # datas from viewer 0 and get 'data' key (within the ordereddict list of datas
class ProfilerDataTree(QTreeWidget): """ Convenience tree widget (with built-in model) to store and view profiler data. The quantities calculated by the profiler are as follows (from profile.Profile): [0] = The number of times this function was called, not counting direct or indirect recursion, [1] = Number of times this function appears on the stack, minus one [2] = Total time spent internal to this function [3] = Cumulative time that this function was present on the stack. In non-recursive functions, this is the total execution time from start to finish of each invocation of a function, including time spent in all subfunctions. [4] = A dictionary indicating for each function name, the number of times it was called by us. """ sig_edit_goto = Signal(str, int, str) SEP = r"<[=]>" # separator between filename and linenumber # (must be improbable as a filename to avoid splitting the filename itself) def __init__(self, parent=None): QTreeWidget.__init__(self, parent) self.header_list = [ _('Function/Module'), _('Total Time'), _('Diff'), _('Local Time'), _('Diff'), _('Calls'), _('Diff'), _('File:line') ] self.icon_list = { 'module': ima.icon('python'), 'function': ima.icon('function'), 'builtin': ima.icon('python'), 'constructor': ima.icon('class') } self.profdata = None # To be filled by self.load_data() self.stats = None # To be filled by self.load_data() self.item_depth = None self.item_list = None self.items_to_be_shown = None self.current_view_depth = None self.compare_file = None self.setColumnCount(len(self.header_list)) self.setHeaderLabels(self.header_list) self.initialize_view() self.itemActivated.connect(self.item_activated) self.itemExpanded.connect(self.item_expanded) def set_item_data(self, item, filename, line_number): """Set tree item user data: filename (string) and line_number (int)""" set_item_user_text(item, '%s%s%d' % (filename, self.SEP, line_number)) def get_item_data(self, item): """Get tree item user data: (filename, line_number)""" filename, line_number_str = get_item_user_text(item).split(self.SEP) return filename, int(line_number_str) def initialize_view(self): """Clean the tree and view parameters""" self.clear() self.item_depth = 0 # To be use for collapsing/expanding one level self.item_list = [] # To be use for collapsing/expanding one level self.items_to_be_shown = {} self.current_view_depth = 0 def load_data(self, profdatafile): """Load profiler data saved by profile/cProfile module""" import pstats try: stats_indi = [ pstats.Stats(profdatafile), ] except (OSError, IOError): return self.profdata = stats_indi[0] if self.compare_file is not None: try: stats_indi.append(pstats.Stats(self.compare_file)) except (OSError, IOError) as e: QMessageBox.critical( self, _("Error"), _("Error when trying to load profiler results")) debug_print("Error when calling pstats, {}".format(e)) self.compare_file = None map(lambda x: x.calc_callees(), stats_indi) self.profdata.calc_callees() self.stats1 = stats_indi self.stats = stats_indi[0].stats def compare(self, filename): self.hide_diff_cols(False) self.compare_file = filename def hide_diff_cols(self, hide): for i in (2, 4, 6): self.setColumnHidden(i, hide) def save_data(self, filename): """""" self.stats1[0].dump_stats(filename) def find_root(self): """Find a function without a caller""" self.profdata.sort_stats("cumulative") for func in self.profdata.fcn_list: if ('~', 0) != func[0:2] and not func[2].startswith( '<built-in method exec>'): # This skips the profiler function at the top of the list # it does only occur in Python 3 return func def find_callees(self, parent): """Find all functions called by (parent) function.""" # FIXME: This implementation is very inneficient, because it # traverses all the data to find children nodes (callees) return self.profdata.all_callees[parent] def show_tree(self): """Populate the tree with profiler data and display it.""" self.initialize_view() # Clear before re-populating self.setItemsExpandable(True) self.setSortingEnabled(False) rootkey = self.find_root() # This root contains profiler overhead if rootkey: self.populate_tree(self, self.find_callees(rootkey)) self.resizeColumnToContents(0) self.setSortingEnabled(True) self.sortItems(1, Qt.AscendingOrder) # FIXME: hardcoded index self.change_view(1) def function_info(self, functionKey): """Returns processed information about the function's name and file.""" node_type = 'function' filename, line_number, function_name = functionKey if function_name == '<module>': modulePath, moduleName = osp.split(filename) node_type = 'module' if moduleName == '__init__.py': modulePath, moduleName = osp.split(modulePath) function_name = '<' + moduleName + '>' if not filename or filename == '~': file_and_line = '(built-in)' node_type = 'builtin' else: if function_name == '__init__': node_type = 'constructor' file_and_line = '%s : %d' % (filename, line_number) return filename, line_number, function_name, file_and_line, node_type @staticmethod def format_measure(measure): """Get format and units for data coming from profiler task.""" # Convert to a positive value. measure = abs(measure) # For number of calls if isinstance(measure, int): return to_text_string(measure) # For time measurements if 1.e-9 < measure <= 1.e-6: measure = u"{0:.2f} ns".format(measure / 1.e-9) elif 1.e-6 < measure <= 1.e-3: measure = u"{0:.2f} us".format(measure / 1.e-6) elif 1.e-3 < measure <= 1: measure = u"{0:.2f} ms".format(measure / 1.e-3) elif 1 < measure <= 60: measure = u"{0:.2f} sec".format(measure) elif 60 < measure <= 3600: m, s = divmod(measure, 3600) if s > 60: m, s = divmod(measure, 60) s = to_text_string(s).split(".")[-1] measure = u"{0:.0f}.{1:.2s} min".format(m, s) else: h, m = divmod(measure, 3600) if m > 60: m /= 60 measure = u"{0:.0f}h:{1:.0f}min".format(h, m) return measure def color_string(self, x): """Return a string formatted delta for the values in x. Args: x: 2-item list of integers (representing number of calls) or 2-item list of floats (representing seconds of runtime). Returns: A list with [formatted x[0], [color, formatted delta]], where color reflects whether x[1] is lower, greater, or the same as x[0]. """ diff_str = "" color = "black" if len(x) == 2 and self.compare_file is not None: difference = x[0] - x[1] if difference: color, sign = ('green', '-') if difference < 0 else ('red', '+') diff_str = '{}{}'.format(sign, self.format_measure(difference)) return [self.format_measure(x[0]), [diff_str, color]] def format_output(self, child_key): """ Formats the data. self.stats1 contains a list of one or two pstat.Stats() instances, with the first being the current run and the second, the saved run, if it exists. Each Stats instance is a dictionary mapping a function to 5 data points - cumulative calls, number of calls, total time, cumulative time, and callers. format_output() converts the number of calls, total time, and cumulative time to a string format for the child_key parameter. """ data = [x.stats.get(child_key, [0, 0, 0, 0, {}]) for x in self.stats1] return (map(self.color_string, islice(zip(*data), 1, 4))) def populate_tree(self, parentItem, children_list): """Recursive method to create each item (and associated data) in the tree.""" for child_key in children_list: self.item_depth += 1 (filename, line_number, function_name, file_and_line, node_type) = self.function_info(child_key) ((total_calls, total_calls_dif), (loc_time, loc_time_dif), (cum_time, cum_time_dif)) = self.format_output(child_key) child_item = TreeWidgetItem(parentItem) self.item_list.append(child_item) self.set_item_data(child_item, filename, line_number) # FIXME: indexes to data should be defined by a dictionary on init child_item.setToolTip(0, _('Function or module name')) child_item.setData(0, Qt.DisplayRole, function_name) child_item.setIcon(0, self.icon_list[node_type]) child_item.setToolTip(1, _('Time in function '\ '(including sub-functions)')) child_item.setData(1, Qt.DisplayRole, cum_time) child_item.setTextAlignment(1, Qt.AlignRight) child_item.setData(2, Qt.DisplayRole, cum_time_dif[0]) child_item.setForeground(2, QColor(cum_time_dif[1])) child_item.setTextAlignment(2, Qt.AlignLeft) child_item.setToolTip(3, _('Local time in function '\ '(not in sub-functions)')) child_item.setData(3, Qt.DisplayRole, loc_time) child_item.setTextAlignment(3, Qt.AlignRight) child_item.setData(4, Qt.DisplayRole, loc_time_dif[0]) child_item.setForeground(4, QColor(loc_time_dif[1])) child_item.setTextAlignment(4, Qt.AlignLeft) child_item.setToolTip(5, _('Total number of calls '\ '(including recursion)')) child_item.setData(5, Qt.DisplayRole, total_calls) child_item.setTextAlignment(5, Qt.AlignRight) child_item.setData(6, Qt.DisplayRole, total_calls_dif[0]) child_item.setForeground(6, QColor(total_calls_dif[1])) child_item.setTextAlignment(6, Qt.AlignLeft) child_item.setToolTip(7, _('File:line '\ 'where function is defined')) child_item.setData(7, Qt.DisplayRole, file_and_line) #child_item.setExpanded(True) if self.is_recursive(child_item): child_item.setData(7, Qt.DisplayRole, '(%s)' % _('recursion')) child_item.setDisabled(True) else: callees = self.find_callees(child_key) if self.item_depth < 3: self.populate_tree(child_item, callees) elif callees: child_item.setChildIndicatorPolicy( child_item.ShowIndicator) self.items_to_be_shown[id(child_item)] = callees self.item_depth -= 1 def item_activated(self, item): filename, line_number = self.get_item_data(item) self.sig_edit_goto.emit(filename, line_number, '') def item_expanded(self, item): if item.childCount() == 0 and id(item) in self.items_to_be_shown: callees = self.items_to_be_shown[id(item)] self.populate_tree(item, callees) def is_recursive(self, child_item): """Returns True is a function is a descendant of itself.""" ancestor = child_item.parent() # FIXME: indexes to data should be defined by a dictionary on init while ancestor: if (child_item.data(0, Qt.DisplayRole) == ancestor.data( 0, Qt.DisplayRole) and child_item.data(7, Qt.DisplayRole) == ancestor.data( 7, Qt.DisplayRole)): return True else: ancestor = ancestor.parent() return False def get_top_level_items(self): """Iterate over top level items""" return [ self.topLevelItem(_i) for _i in range(self.topLevelItemCount()) ] def get_items(self, maxlevel): """Return all items with a level <= `maxlevel`""" itemlist = [] def add_to_itemlist(item, maxlevel, level=1): level += 1 for index in range(item.childCount()): citem = item.child(index) itemlist.append(citem) if level <= maxlevel: add_to_itemlist(citem, maxlevel, level) for tlitem in self.get_top_level_items(): itemlist.append(tlitem) if maxlevel > 0: add_to_itemlist(tlitem, maxlevel=maxlevel) return itemlist def change_view(self, change_in_depth): """Change the view depth by expand or collapsing all same-level nodes""" self.current_view_depth += change_in_depth if self.current_view_depth < 0: self.current_view_depth = 0 self.collapseAll() if self.current_view_depth > 0: for item in self.get_items(maxlevel=self.current_view_depth - 1): item.setExpanded(True)
class FitPropertyBrowser(FitPropertyBrowserBase): closing = Signal() def __init__(self, canvas, parent=None): super(FitPropertyBrowser, self).__init__(parent) self.init() self.canvas = canvas self.tool = None self.fit_result_lines = [] self.startXChanged.connect(self.move_start_x) self.endXChanged.connect(self.move_end_x) self.fittingDone.connect(self.fitting_done) def closeEvent(self, event): self.closing.emit() BaseBrowser.closeEvent(self, event) def show(self): allowed_spectra = {} pattern = re.compile('(.+?): spec (\d+)') for label in [lin.get_label() for lin in self.get_lines()]: a_match = re.match(pattern, label) name, spec = a_match.group(1), int(a_match.group(2)) spec_list = allowed_spectra.get(name, []) spec_list.append(spec) allowed_spectra[name] = spec_list for name, spec_list in allowed_spectra.items(): self.addAllowedSpectra(name, spec_list) self.tool = FitInteractiveTool(self.canvas) self.tool.fit_start_x_moved.connect(self.setStartX) self.tool.fit_end_x_moved.connect(self.setEndX) self.setXRange(self.tool.fit_start_x.x, self.tool.fit_end_x.x) super(FitPropertyBrowser, self).show() def hide(self): if self.tool is not None: self.tool.fit_start_x_moved.disconnect() self.tool.fit_end_x_moved.disconnect() self.tool.disconnect() super(FitPropertyBrowser, self).hide() def move_start_x(self, xd): if self.tool is not None: self.tool.move_start_x(xd) def move_end_x(self, xd): if self.tool is not None: self.tool.move_end_x(xd) def clear_fit_result_lines(self): for lin in self.fit_result_lines: try: lin.remove() except ValueError: # workspace replacement could invalidate these references pass self.fit_result_lines = [] def get_lines(self): return self.canvas.figure.get_axes()[0].get_lines() def fitting_done(self, name): from mantidqt.plotting.functions import plot name += '_Workspace' ws = mtd[name] self.clear_fit_result_lines() plot([ws], wksp_indices=[1, 2], fig=self.canvas.figure, overplot=True) name += ':' for lin in self.get_lines(): if lin.get_label().startswith(name): self.fit_result_lines.append(lin)
class ConsoleWidget(PluginMainWidget): DEFAULT_OPTIONS = { 'codecompletion/auto': True, 'commands': [], 'external_editor/gotoline': '', 'external_editor/path': '', 'max_line_count': 300, 'message': 'Internal console\n\n', 'multithreaded': False, 'namespace': None, 'profile': False, 'show_internal_errors': True, 'wrap': True, # From appearance 'color_theme': 'spyder/dark', } # --- Signals # This signal emits a parsed error traceback text so we can then # request opening the file that traceback comes from in the Editor. sig_edit_goto_requested = Signal(str, int, str) # TODO: I do not think we use this? sig_focus_changed = Signal() # Emit this when the interpreter buffer is flushed sig_refreshed = Signal() # Request to show a status message on the main window sig_show_status_requested = Signal(str) # Request the main application to quit. sig_quit_requested = Signal() sig_help_requested = Signal(dict) """ This signal is emitted to request help on a given object `name`. Parameters ---------- help_data: dict Example `{'name': str, 'ignore_unknown': bool}`. """ def __init__(self, name, plugin, parent=None, options=DEFAULT_OPTIONS): super().__init__(name, plugin, parent, options) logger.info("Initializing...") # Traceback MessageBox self.error_traceback = '' self.dismiss_error = False # Widgets self.dialog_manager = DialogManager() self.error_dlg = None self.shell = InternalShell( # TODO: Move to use SpyderWidgetMixin? parent=parent, namespace=self.get_option('namespace'), commands=self.get_option('commands'), message=self.get_option('message'), max_line_count=self.get_option('max_line_count'), profile=self.get_option('profile'), multithreaded=self.get_option('multithreaded'), ) self.find_widget = FindReplace(self) # Setup self.setAcceptDrops(True) self.find_widget.set_editor(self.shell) self.find_widget.hide() self.shell.toggle_wrap_mode(self.get_option('wrap')) # Layout layout = QVBoxLayout() layout.addWidget(self.shell) layout.addWidget(self.find_widget) self.setLayout(layout) # Signals self.shell.sig_help_requested.connect(self.sig_help_requested) self.shell.sig_exception_occurred.connect(self.handle_exception) self.shell.sig_focus_changed.connect(self.sig_focus_changed) self.shell.sig_go_to_error_requested.connect(self.go_to_error) self.shell.sig_redirect_stdio_requested.connect( self.sig_redirect_stdio_requested) self.shell.sig_refreshed.connect(self.sig_refreshed) self.shell.sig_show_status_requested.connect( lambda msg: self.sig_show_status_message.emit(msg, 0)) # --- PluginMainWidget API # ------------------------------------------------------------------------ def get_title(self): return _('Internal console') def setup(self, options): # TODO: Move this to the shell quit_action = self.create_action( ConsoleWidgetActions.Quit, text=_("&Quit"), tip=_("Quit"), icon=self.create_icon('exit'), triggered=self.sig_quit_requested, context=Qt.ApplicationShortcut, ) run_action = self.create_action( ConsoleWidgetActions.Run, text=_("&Run..."), tip=_("Run a Python script"), icon=self.create_icon('run_small'), triggered=self.run_script, ) environ_action = self.create_action( ConsoleWidgetActions.Environment, text=_("Environment variables..."), tip=_("Show and edit environment variables (for current " "session)"), icon=self.create_icon('environ'), triggered=self.show_env, ) syspath_action = self.create_action( ConsoleWidgetActions.SysPath, text=_("Show sys.path contents..."), tip=_("Show (read-only) sys.path"), icon=self.create_icon('syspath'), triggered=self.show_syspath, ) buffer_action = self.create_action( ConsoleWidgetActions.MaxLineCount, text=_("Buffer..."), tip=_("Set maximum line count"), triggered=self.change_max_line_count, ) exteditor_action = self.create_action( ConsoleWidgetActions.ExternalEditor, text=_("External editor path..."), tip=_("Set external editor executable path"), triggered=self.change_exteditor, ) wrap_action = self.create_action( ConsoleWidgetActions.ToggleWrap, text=_("Wrap lines"), toggled=lambda val: self.set_option('wrap', val), initial=self.get_option('wrap'), ) codecompletion_action = self.create_action( ConsoleWidgetActions.ToggleCodeCompletion, text=_("Automatic code completion"), toggled=lambda val: self.set_option('codecompletion/auto', val), initial=self.get_option('codecompletion/auto'), ) # Submenu internal_settings_menu = self.create_menu( ConsoleWidgetMenus.InternalSettings, _('Internal console settings'), icon=self.create_icon('tooloptions'), ) for item in [ buffer_action, wrap_action, codecompletion_action, exteditor_action ]: self.add_item_to_menu( item, menu=internal_settings_menu, section=ConsoleWidgetInternalSettingsSubMenuSections.Main, ) # Options menu options_menu = self.get_options_menu() for item in [ run_action, environ_action, syspath_action, internal_settings_menu ]: self.add_item_to_menu( item, menu=options_menu, section=ConsoleWidgetOptionsMenuSections.Run, ) self.add_item_to_menu( quit_action, menu=options_menu, section=ConsoleWidgetOptionsMenuSections.Quit, ) self.shell.set_external_editor(self.get_option('external_editor/path'), '') def on_option_update(self, option, value): if option == 'max_line_count': self.shell.setMaximumBlockCount(value) elif option == 'wrap': self.shell.toggle_wrap_mode(value) elif option == 'codecompletion/auto': self.shell.set_codecompletion_auto(value) elif option == 'external_editor/path': self.shell.set_external_editor(value, '') def update_actions(self): pass def get_focus_widget(self): return self.shell # --- Qt overrides # ------------------------------------------------------------------------ def dragEnterEvent(self, event): """ Reimplement Qt method. Inform Qt about the types of data that the widget accepts. """ source = event.mimeData() if source.hasUrls(): if mimedata2url(source): event.acceptProposedAction() else: event.ignore() elif source.hasText(): event.acceptProposedAction() def dropEvent(self, event): """ Reimplement Qt method. Unpack dropped data and handle it. """ source = event.mimeData() if source.hasUrls(): pathlist = mimedata2url(source) self.shell.drop_pathlist(pathlist) elif source.hasText(): lines = to_text_string(source.text()) self.shell.set_cursor_position('eof') self.shell.execute_lines(lines) event.acceptProposedAction() # --- Public API # ------------------------------------------------------------------------ def start_interpreter(self, namespace): """ Start internal console interpreter. """ self.shell.start_interpreter(namespace) def set_historylog(self, historylog): """ Bind historylog instance to this console. Not used anymore since v2.0. """ historylog.add_history(self.shell.history_filename) self.shell.append_to_history.connect(historylog.append_to_history) def set_help(self, help_plugin): """ Bind help instance to this console. """ self.shell.help = help_plugin @Slot(dict) def handle_exception(self, error_data, sender=None, internal_plugins=None): """ Exception ocurred in the internal console. Show a QDialog or the internal console to warn the user. Handle any exception that occurs during Spyder usage. Parameters ---------- error_data: dict The dictionary containing error data. The expected keys are: >>> error_data= { "text": str, "is_traceback": bool, "repo": str, "title": str, "label": str, "steps": str, } sender: spyder.api.plugins.SpyderPluginV2, optional The sender plugin. Default is None. Notes ----- The `is_traceback` key indicates if `text` contains plain text or a Python error traceback. The `title` and `repo` keys indicate how the error data should customize the report dialog and Github error submission. The `label` and `steps` keys allow customizing the content of the error dialog. """ text = error_data.get("text", None) is_traceback = error_data.get("is_traceback", False) title = error_data.get("title", "") label = error_data.get("label", "") steps = error_data.get("steps", "") # Skip errors without traceback (and no text) or dismiss if ((not text and not is_traceback and self.error_dlg is None) or self.dismiss_error): return if internal_plugins is None: internal_plugins = find_internal_plugins() if internal_plugins: internal_plugin_names = [] for __, val in internal_plugins.items(): name = getattr(val, 'NAME', getattr(val, 'CONF_SECTION')) internal_plugin_names.append(name) sender_name = getattr(val, 'NAME', getattr(val, 'CONF_SECTION')) is_internal_plugin = sender_name in internal_plugin_names else: is_internal_plugin = False repo = "spyder-ide/spyder" if sender is not None and not is_internal_plugin: repo = error_data.get("repo", None) try: plugin_name = sender.NAME except Exception: plugin_name = sender.CONF_SECTION if repo is None: raise Exception( 'External plugin "{}" does not define "repo" key in ' 'the "error_data" dictionary!'.format(plugin_name)) if self.get_option('show_internal_errors'): if self.error_dlg is None: self.error_dlg = SpyderErrorDialog(self) self.error_dlg.set_color_scheme(self.get_option('color_theme')) self.error_dlg.close_btn.clicked.connect(self.close_error_dlg) self.error_dlg.rejected.connect(self.remove_error_dlg) self.error_dlg.details.go_to_error.connect(self.go_to_error) # Set the report repository self.error_dlg.set_github_repo_org(repo) if title: self.error_dlg.set_title(title) self.error_dlg.title.setEnabled(False) if label: self.error_dlg.main_label.setText(label) self.error_dlg.submit_btn.setEnabled(True) if steps: self.error_dlg.steps_text.setText(steps) self.error_dlg.set_require_minimum_length(False) self.error_dlg.append_traceback(text) self.error_dlg.show() elif DEV or get_debug_level(): self.change_visibility(True, True) def close_error_dlg(self): """ Close error dialog. """ if self.error_dlg.dismiss_box.isChecked(): self.dismiss_error = True self.error_dlg.reject() def remove_error_dlg(self): """ Remove error dialog. """ self.error_dlg = None @Slot() def show_env(self): """ Show environment variables. """ self.dialog_manager.show(EnvDialog(parent=self)) def get_sys_path(self): """ Return the `sys.path`. """ return sys.path @Slot() def show_syspath(self): """ Show `sys.path`. """ editor = CollectionsEditor(parent=self) editor.setup( sys.path, title="sys.path", readonly=True, icon=self.create_icon('syspath'), ) self.dialog_manager.show(editor) @Slot() def run_script(self, filename=None, silent=False, set_focus=False, args=None): """ Run a Python script. """ if filename is None: self.shell.interpreter.restore_stds() filename, _selfilter = getopenfilename( self, _("Run Python script"), getcwd_or_home(), _("Python scripts") + " (*.py ; *.pyw ; *.ipy)", ) self.shell.interpreter.redirect_stds() if filename: os.chdir(osp.dirname(filename)) filename = osp.basename(filename) else: return logger.debug("Running script with %s", args) filename = osp.abspath(filename) rbs = remove_backslashes command = "runfile('%s', args='%s')" % (rbs(filename), rbs(args)) if set_focus: self.shell.setFocus() self.change_visibility(True, True) self.shell.write(command + '\n') self.shell.run_command(command) def go_to_error(self, text): """ Go to error if relevant. """ match = get_error_match(to_text_string(text)) if match: fname, lnb = match.groups() self.edit_script(fname, int(lnb)) def edit_script(self, filename=None, goto=-1): """ Edit script. """ if filename is not None: # Called from InternalShell self.shell.external_editor(filename, goto) self.sig_edit_goto_requested.emit(osp.abspath(filename), goto, '') def execute_lines(self, lines): """ Execute lines and give focus to shell. """ self.shell.execute_lines(to_text_string(lines)) self.shell.setFocus() @Slot() def change_max_line_count(self, value=None): """" Change maximum line count. """ valid = True if value is None: value, valid = QInputDialog.getInt( self, _('Buffer'), _('Maximum line count'), self.get_option('max_line_count'), 0, 1000000, ) if valid: self.set_option('max_line_count', value) @Slot() def change_exteditor(self, path=None): """ Change external editor path. """ valid = True if path is None: path, valid = QInputDialog.getText( self, _('External editor'), _('External editor executable path:'), QLineEdit.Normal, self.get_option('external_editor/path'), ) if valid: self.set_option('external_editor/path', to_text_string(path)) def set_exit_function(self, func): """ Set the callback function to execute when the `exit_interpreter` is called. """ self.shell.exitfunc = func def set_font(self, font): """ Set font of the internal shell. """ self.shell.set_font(font) def redirect_stds(self): """ Redirect stdout and stderr when using open file dialogs. """ self.shell.interpreter.redirect_stds() def restore_stds(self): """ Restore stdout and stderr when using open file dialogs. """ self.shell.interpreter.restore_stds() def set_namespace_item(self, name, item): """ Add an object to the namespace dictionary of the internal console. """ self.shell.interpreter.namespace[name] = item def exit_interpreter(self): """ Exit the internal console interpreter. This is equivalent to requesting the main application to quit. """ self.shell.exit_interpreter()
class livePlot(QtCore.QObject): """ Class to enable live plotting of data. Attributes: datafunction: the function to call for data acquisition sweepInstrument: the instrument to which sweepparams belong sweepparams: the parameter(s) being swept sweepranges: the range over which sweepparams are being swept verbose (int): output level of logging information show_controls (bool): show gui elements for control of the live plotting alpha (float): parameter (value between 0 and 1) which determines the weight given in averaging to the latest measurement result (alpha) and the previous measurement result (1-alpha), default value 0.3 """ from qtpy.QtCore import Signal sigMouseClicked = Signal(object) def __init__(self, datafunction=None, sweepInstrument=None, sweepparams=None, sweepranges=None, alpha=.3, verbose=1, show_controls=True, window_title='live view', plot_title=None, is1dscan=None, **kwargs): """Return a new livePlot object.""" super().__init__(**kwargs) self.window_title = window_title win = QtWidgets.QWidget() win.resize(800, 600) win.setWindowTitle(self.window_title) vertLayout = QtWidgets.QVBoxLayout() self._averaging_enabled = True if show_controls: topLayout = QtWidgets.QHBoxLayout() win.start_button = QtWidgets.QPushButton('Start') win.stop_button = QtWidgets.QPushButton('Stop') win.averaging_box = QtWidgets.QCheckBox('Averaging') win.averaging_box.setChecked(self._averaging_enabled) for b in [win.start_button, win.stop_button]: b.setMaximumHeight(24) topLayout.addWidget(win.start_button) topLayout.addWidget(win.stop_button) topLayout.addWidget(win.averaging_box) vertLayout.addLayout(topLayout) plotwin = pg.GraphicsWindow(title="Live view") vertLayout.addWidget(plotwin) win.setLayout(vertLayout) self.setGeometry = win.setGeometry self.win = win self.plotwin = plotwin self.verbose = verbose self.idx = 0 self.maxidx = 1e9 self.data = None self.data_avg = None self.sweepInstrument = sweepInstrument self.sweepparams = sweepparams self.sweepranges = sweepranges self.fps = pgeometry.fps_t(nn=6) self.datafunction = datafunction self.datafunction_result = None self.alpha = alpha if is1dscan is None: is1dscan = (isinstance(self.sweepparams, str) or (isinstance(self.sweepparams, (list, dict)) and len(self.sweepparams) == 1)) if isinstance(self.sweepparams, dict): if 'gates_horz' not in self.sweepparams: is1dscan = True if verbose: print('live_plotting: is1dscan %s' % is1dscan) if self.sweepparams is None: p1 = plotwin.addPlot(title="Videomode") p1.setLabel('left', 'param2') p1.setLabel('bottom', 'param1') if self.datafunction is None: raise Exception( 'Either specify a datafunction or sweepparams.') else: data = np.array(self.datafunction()) if data.ndim == 1: dd = np.zeros((0, )) plot = p1.plot(dd, pen='b') self.plot = plot else: self.plot = pg.ImageItem() p1.addItem(self.plot) elif is1dscan: p1 = plotwin.addPlot(title=plot_title) p1.setLabel('left', 'Value') p1.setLabel('bottom', self.sweepparams, units='mV') dd = np.zeros((0, )) plot = p1.plot(dd, pen='b') self.plot = plot vpen = pg.QtGui.QPen(pg.QtGui.QColor(130, 130, 175, 60), 0, pg.QtCore.Qt.SolidLine) gv = pg.InfiniteLine([0, 0], angle=90, pen=vpen) gv.setZValue(0) p1.addItem(gv) self._crosshair = [gv] self.crosshair(show=False) elif isinstance(self.sweepparams, (list, dict)): # 2D scan p1 = plotwin.addPlot(title=plot_title) if type(self.sweepparams) is dict: [xlabel, ylabel] = ['sweepparam_v', 'stepparam_v'] else: [xlabel, ylabel] = self.sweepparams p1.setLabel('bottom', xlabel, units='mV') p1.setLabel('left', ylabel, units='mV') self.plot = pg.ImageItem() p1.addItem(self.plot) vpen = pg.QtGui.QPen(pg.QtGui.QColor(0, 130, 235, 60), 0, pg.QtCore.Qt.SolidLine) gh = pg.InfiniteLine([0, 0], angle=90, pen=vpen) gv = pg.InfiniteLine([0, 0], angle=0, pen=vpen) gh.setZValue(0) gv.setZValue(0) p1.addItem(gh) p1.addItem(gv) self._crosshair = [gh, gv] self.crosshair(show=False) else: raise Exception( 'The number of sweep parameters should be either None, 1 or 2.' ) self.plothandle = p1 self.timer = QtCore.QTimer() self.timer.timeout.connect(self.updatebg) self.win.show() def connect_slot(target): """ Create a slot by dropping signal arguments """ # @Slot() def signal_drop_arguments(*args, **kwargs): # print('call %s' % target) target() return signal_drop_arguments if show_controls: win.start_button.clicked.connect(connect_slot(self.startreadout)) win.stop_button.clicked.connect(connect_slot(self.stopreadout)) win.averaging_box.clicked.connect( connect_slot(self.enable_averaging_slot)) self.datafunction_result = None self.plotwin.scene().sigMouseClicked.connect(self._onClick) def _onClick(self, event): image_pt = self.plot.mapFromScene(event.scenePos()) tr = self.plot.transform() pt = tr.map(image_pt.x(), image_pt.y()) if self.verbose >= 2: print('pt %s' % (pt, )) self.sigMouseClicked.emit(pt) def close(self): if self.verbose: print('LivePlot.close()') self.stopreadout() self.win.close() def resetdata(self): self.idx = 0 self.data = None def crosshair(self, show=None, pos=None): """ Enable or disable crosshair Args: show (None, True or False) pos (None or position) """ for x in self._crosshair: if show is not None: if show: x.show() else: x.hide() if pos is not None: x.setPos(pos) def update(self, data=None, processevents=True): self.win.setWindowTitle('%s, fps: %.2f' % (self.window_title, self.fps.framerate())) if self.verbose >= 2: print('livePlot: update: idx %d ' % self.idx) if data is not None: self.data = np.array(data) if self.data_avg is None: self.data_avg = self.data # depending on value of self.averaging_enabled either do or # don't do the averaging if self._averaging_enabled: self.data_avg = self.alpha * self.data + \ (1 - self.alpha) * self.data_avg else: self.data_avg = self.data if self.data.ndim == 1: if None in (self.sweepInstrument, self.sweepparams, self.sweepranges): sweepvalues = np.arange(0, self.data_avg.size) self.plot.setData(sweepvalues, self.data_avg) else: if type(self.sweepparams) is dict: paramval = 0 else: sweep_param = getattr(self.sweepInstrument, self.sweepparams) paramval = sweep_param.get_latest() sweepvalues = np.linspace(paramval - self.sweepranges / 2, self.sweepranges / 2 + paramval, len(data)) self.plot.setData(sweepvalues, self.data_avg) self._sweepvalues = [sweepvalues] self.crosshair(show=None, pos=[paramval, 0]) elif self.data.ndim == 2: self.plot.setImage(self.data_avg.T) if None not in (self.sweepInstrument, self.sweepparams, self.sweepranges): if isinstance(self.sweepparams, dict): value_x = 0 value_y = 0 else: if isinstance(self.sweepparams[0], dict): value_x = 0 value_y = 0 else: value_x = self.sweepInstrument.get( self.sweepparams[0]) value_y = self.sweepInstrument.get( self.sweepparams[1]) self.horz_low = value_x - self.sweepranges[0] / 2 self.horz_range = self.sweepranges[0] self.vert_low = value_y - self.sweepranges[1] / 2 self.vert_range = self.sweepranges[1] self.rect = QtCore.QRect(self.horz_low, self.vert_low, self.horz_range, self.vert_range) self.plot.setRect(self.rect) self.crosshair(show=None, pos=[value_x, value_y]) self._sweepvalues = [ np.linspace(self.horz_low, self.horz_low + self.horz_range, self.data.shape[1]), np.linspace(self.vert_low, self.vert_low + self.vert_range, self.data.shape[0]) ] else: raise Exception('ndim %d not supported' % self.data.ndim) else: pass self.idx = self.idx + 1 if self.idx > self.maxidx: self.idx = 0 self.timer.stop() if processevents: QtWidgets.QApplication.processEvents() def updatebg(self): """ Update function for the widget Calls the datafunction() and update() function """ if self.idx % 10 == 0: logging.debug('livePlot: updatebg %d' % self.idx) self.idx = self.idx + 1 self.fps.addtime(time.time()) if self.datafunction is not None: try: dd = self.datafunction() self.datafunction_result = dd self.update(data=dd) except Exception as e: logging.exception(e) print('livePlot: Exception in updatebg, stopping readout') self.stopreadout() else: self.stopreadout() dd = None if self.fps.framerate() < 10: time.sleep(0.1) time.sleep(0.00001) def enable_averaging(self, value): self._averaging_enabled = value if self.verbose >= 1: if self._averaging_enabled == 2: print('enable_averaging called, alpha = ' + str(self.alpha)) elif self._averaging_enabled == 0: print('enable_averaging called, averaging turned off') else: print('enable_averaging called, undefined') def enable_averaging_slot(self, *args, **kwargs): """ Update the averaging mode of the widget """ self._averaging_enabled = self.win.averaging_box.checkState() self.enable_averaging(self._averaging_enabled) def startreadout(self, callback=None, rate=30, maxidx=None): """ Args: rate (float): sample rate in ms """ if maxidx is not None: self.maxidx = maxidx if callback is not None: self.datafunction = callback self.timer.start(1000 * (1. / rate)) if self.verbose: print('live_plotting: start readout: rate %.1f Hz' % rate) def stopreadout(self): if self.verbose: print('live_plotting: stop readout') self.timer.stop() self.win.setWindowTitle('Live view stopped')
class FitInteractiveTool(QObject): fit_start_x_moved = Signal(float) fit_end_x_moved = Signal(float) def __init__(self, canvas): super(FitInteractiveTool, self).__init__() self.canvas = canvas ax = canvas.figure.get_axes()[0] self.ax = ax xlim = ax.get_xlim() dx = (xlim[1] - xlim[0]) / 20. start_x = xlim[0] + dx end_x = xlim[1] - dx self.fit_start_x = VerticalMarker(canvas, start_x, 'green') self.fit_end_x = VerticalMarker(canvas, end_x, 'green') self.fit_start_x.moved.connect(self.fit_start_x_moved) self.fit_end_x.moved.connect(self.fit_end_x_moved) self._cids = [] self._cids.append(canvas.mpl_connect('draw_event', self.draw_callback)) self._cids.append( canvas.mpl_connect('motion_notify_event', self.motion_notify_callback)) self._cids.append( canvas.mpl_connect('button_press_event', self.on_click)) self._cids.append( canvas.mpl_connect('button_release_event', self.on_release)) self.is_cursor_overridden = False def disconnect(self): for cid in self._cids: self.canvas.mpl_disconnect(cid) self.fit_start_x.remove() self.fit_end_x.remove() def draw_callback(self, event): if self.fit_start_x.x > self.fit_end_x.x: x = self.fit_start_x.x self.fit_start_x.x = self.fit_end_x.x self.fit_end_x.x = x self.fit_start_x.redraw() self.fit_end_x.redraw() def motion_notify_callback(self, event): x = event.x if x is not None and (self.fit_start_x.should_override_cursor(x) or self.fit_end_x.should_override_cursor(x)): if not self.is_cursor_overridden: QApplication.setOverrideCursor(QCursor(Qt.SizeHorCursor)) self.is_cursor_overridden = True else: QApplication.restoreOverrideCursor() self.is_cursor_overridden = False self.fit_start_x.mouse_move(event.xdata) self.fit_end_x.mouse_move(event.xdata) self.canvas.draw() def on_click(self, event): if event.button == 1: self.fit_start_x.on_click(event.x) self.fit_end_x.on_click(event.x) def on_release(self, event): self.fit_start_x.stop() self.fit_end_x.stop() def move_start_x(self, xd): if xd is not None: self.fit_start_x.x = xd self.canvas.draw() def move_end_x(self, xd): if xd is not None: self.fit_end_x.x = xd self.canvas.draw()
class ShellWidget(NamepaceBrowserWidget, HelpWidget, DebuggingWidget): """ Shell widget for the IPython Console This is the widget in charge of executing code """ # NOTE: Signals can't be assigned separately to each widget # That's why we define all needed signals here. # For NamepaceBrowserWidget sig_namespace_view = Signal(object) sig_var_properties = Signal(object) # For DebuggingWidget sig_input_reply = Signal() sig_pdb_step = Signal(str, int) sig_prompt_ready = Signal() sig_dbg_kernel_restart = Signal() # For ShellWidget focus_changed = Signal() new_client = Signal() sig_got_reply = Signal() sig_kernel_restarted = Signal(str) def __init__(self, ipyclient, additional_options, interpreter_versions, external_kernel, *args, **kw): # To override the Qt widget used by RichJupyterWidget self.custom_control = ControlWidget self.custom_page_control = PageControlWidget super(ShellWidget, self).__init__(*args, **kw) self.ipyclient = ipyclient self.additional_options = additional_options self.interpreter_versions = interpreter_versions self.external_kernel = external_kernel self.set_background_color() # Keyboard shortcuts self.shortcuts = self.create_shortcuts() # To save kernel replies in silent execution self._kernel_reply = None #---- Public API ---------------------------------------------------------- def set_exit_callback(self): """Set exit callback for this shell.""" self.exit_requested.connect(self.ipyclient.exit_callback) def is_running(self): if self.kernel_client is not None and \ self.kernel_client.channels_running: return True else: return False def set_cwd(self, dirname): """Set shell current working directory.""" return self.silent_execute( u"get_ipython().kernel.set_cwd(r'{}')".format(dirname)) # --- To handle the banner def long_banner(self): """Banner for IPython widgets with pylab message""" # Default banner try: from IPython.core.usage import quick_guide except Exception: quick_guide = '' banner_parts = [ 'Python %s\n' % self.interpreter_versions['python_version'], 'Type "copyright", "credits" or "license" for more information.\n\n', 'IPython %s -- An enhanced Interactive Python.\n' % \ self.interpreter_versions['ipython_version'], quick_guide ] banner = ''.join(banner_parts) # Pylab additions pylab_o = self.additional_options['pylab'] autoload_pylab_o = self.additional_options['autoload_pylab'] 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\n") banner = banner + pylab_message # Sympy additions sympy_o = self.additional_options['sympy'] 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 if (pylab_o and sympy_o): lines = """ Warning: pylab (numpy and matplotlib) and symbolic math (sympy) are both enabled at the same time. Some pylab functions are going to be overrided by the sympy module (e.g. plot) """ banner = banner + lines return banner def short_banner(self): """Short banner with Python and QtConsole versions""" banner = 'Python %s -- IPython %s' % ( self.interpreter_versions['python_version'], self.interpreter_versions['ipython_version']) return banner # --- To define additional shortcuts def clear_console(self): self.execute("%clear") def reset_namespace(self, force=False): """Reset the namespace by removing all names defined by the user.""" reset_str = _("Reset IPython namespace") warn_str = _("All user-defined variables will be removed." "<br>Are you sure you want to reset the namespace?") if not force: reply = QMessageBox.question(self, reset_str, warn_str, QMessageBox.Yes | QMessageBox.No) if reply == QMessageBox.Yes: self.execute("%reset -f") else: self.silent_execute("%reset -f") def set_background_color(self): light_color_o = self.additional_options['light_color'] if not light_color_o: self.set_default_style(colors='linux') def create_shortcuts(self): inspect = config_shortcut(self._control.inspect_current_object, context='Console', name='Inspect current object', parent=self) clear_console = config_shortcut(self.clear_console, context='Console', name='Clear shell', parent=self) restart_kernel = config_shortcut(self.ipyclient.restart_kernel, context='ipython_console', name='Restart kernel', parent=self) new_tab = config_shortcut(lambda: self.new_client.emit(), context='ipython_console', name='new tab', parent=self) reset_namespace = config_shortcut(lambda: self.reset_namespace(), context='ipython_console', name='reset namespace', parent=self) array_inline = config_shortcut(lambda: self.enter_array_inline(), context='array_builder', name='enter array inline', parent=self) array_table = config_shortcut(lambda: self.enter_array_table(), context='array_builder', name='enter array table', parent=self) return [ inspect, clear_console, restart_kernel, new_tab, reset_namespace, array_inline, array_table ] # --- To communicate with the kernel def silent_execute(self, code): """Execute code in the kernel without increasing the prompt""" self.kernel_client.execute(to_text_string(code), silent=True) def silent_exec_method(self, code): """Silently execute a kernel method and save its reply The methods passed here **don't** involve getting the value of a variable but instead replies that can be handled by ast.literal_eval. To get a value see `get_value` Parameters ---------- code : string Code that contains the kernel method as part of its string See Also -------- handle_exec_method : Method that deals with the reply Note ---- This is based on the _silent_exec_callback method of RichJupyterWidget. Therefore this is licensed BSD """ # Generate uuid, which would be used as an indication of whether or # not the unique request originated from here local_uuid = to_text_string(uuid.uuid1()) code = to_text_string(code) msg_id = self.kernel_client.execute( '', silent=True, user_expressions={local_uuid: code}) self._kernel_methods[local_uuid] = code self._request_info['execute'][msg_id] = self._ExecutionRequest( msg_id, 'silent_exec_method') def handle_exec_method(self, msg): """ Handle data returned by silent executions of kernel methods This is based on the _handle_exec_callback of RichJupyterWidget. Therefore this is licensed BSD. """ user_exp = msg['content'].get('user_expressions') if not user_exp: return for expression in user_exp: if expression in self._kernel_methods: # Process kernel reply method = self._kernel_methods[expression] reply = user_exp[expression] data = reply.get('data') if 'get_namespace_view' in method: if data is not None and 'text/plain' in data: view = ast.literal_eval(data['text/plain']) else: view = None self.sig_namespace_view.emit(view) elif 'get_var_properties' in method: if data is not None and 'text/plain' in data: properties = ast.literal_eval(data['text/plain']) else: properties = None self.sig_var_properties.emit(properties) else: if data is not None and 'text/plain' in data: self._kernel_reply = ast.literal_eval( data['text/plain']) else: self._kernel_reply = None self.sig_got_reply.emit() # Remove method after being processed self._kernel_methods.pop(expression) #---- Private methods (overrode by us) --------------------------------- def _context_menu_make(self, pos): """Reimplement the IPython context menu""" menu = super(ShellWidget, 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 """ # Don't change banner for external kernels if self.external_kernel: return '' show_banner_o = self.additional_options['show_banner'] if show_banner_o: return self.long_banner() else: return self.short_banner() def _kernel_restarted_message(self, died=True): msg = _("Kernel died, restarting") if died else _("Kernel restarting") self.sig_kernel_restarted.emit(msg) #---- Qt methods ---------------------------------------------------------- def focusInEvent(self, event): """Reimplement Qt method to send focus change notification""" self.focus_changed.emit() return super(ShellWidget, self).focusInEvent(event) def focusOutEvent(self, event): """Reimplement Qt method to send focus change notification""" self.focus_changed.emit() return super(ShellWidget, self).focusOutEvent(event)
class GitRepoModel(QtGui.QStandardItemModel): """Provides an interface into a git repository for browsing purposes.""" model_updated = Signal() restore = Signal() def __init__(self, parent): QtGui.QStandardItemModel.__init__(self, parent) self.setColumnCount(len(Columns.ALL)) self.entries = {} cfg = gitcfg.current() self.turbo = cfg.get('cola.turbo', False) self.default_author = cfg.get('user.name', N_('Author')) self._parent = parent self._interesting_paths = set() self._interesting_files = set() self._runtask = qtutils.RunTask(parent=parent) self.model_updated.connect(self.refresh, type=Qt.QueuedConnection) model = main.model() model.add_observer(model.message_updated, self._model_updated) self.file_icon = icons.file_text() self.dir_icon = icons.directory() def mimeData(self, indexes): paths = qtutils.paths_from_indexes(self, indexes, item_type=GitRepoNameItem.TYPE) return qtutils.mimedata_from_paths(paths) def mimeTypes(self): return qtutils.path_mimetypes() def clear(self): self.entries.clear() super(GitRepoModel, self).clear() def hasChildren(self, index): if index.isValid(): item = self.itemFromIndex(index) result = item.hasChildren() else: result = True return result def get(self, path, default=None): if not path: item = self.invisibleRootItem() else: item = self.entries.get(path, default) return item def create_row(self, path, create=True, is_dir=False): try: row = self.entries[path] except KeyError: if create: column = self.create_column row = self.entries[path] = [ column(c, path, is_dir) for c in Columns.ALL ] else: row = None return row def create_column(self, col, path, is_dir): """Creates a StandardItem for use in a treeview cell.""" # GitRepoNameItem is the only one that returns a custom type() # and is used to infer selections. if col == Columns.NAME: item = GitRepoNameItem(path, is_dir) else: item = GitRepoItem(path) return item def populate(self, item): self.populate_dir(item, item.path + '/') def add_directory(self, parent, path): """Add a directory entry to the model.""" # Create model items row_items = self.create_row(path, is_dir=True) # Use a standard directory icon name_item = row_items[0] name_item.setIcon(self.dir_icon) parent.appendRow(row_items) return name_item def add_file(self, parent, path): """Add a file entry to the model.""" # Create model items row_items = self.create_row(path) name_item = row_items[0] # Use a standard file icon for the name field name_item.setIcon(self.file_icon) # Add file paths at the end of the list parent.appendRow(row_items) def populate_dir(self, parent, path): """Populate a subtree""" dirs, paths = gitcmds.listdir(path) # Insert directories before file paths for dirname in dirs: self.add_directory(parent, dirname) self.update_entry(dirname) for filename in paths: self.add_file(parent, filename) self.update_entry(filename) def path_is_interesting(self, path): """Return True if path has a status.""" return path in self._interesting_paths def get_paths(self, files=None): """Return paths of interest; e.g. paths with a status.""" if files is None: files = self.get_files() return utils.add_parents(files) def get_files(self): model = main.model() return set(model.staged + model.unstaged) def _model_updated(self): """Observes model changes and updates paths accordingly.""" self.model_updated.emit() def refresh(self): old_files = self._interesting_files old_paths = self._interesting_paths new_files = self.get_files() new_paths = self.get_paths(files=new_files) if new_files != old_files or not old_paths: selected = self._parent.selected_paths() self.clear() self._initialize() self.restore.emit() # Existing items for path in sorted(new_paths.union(old_paths)): self.update_entry(path) self._interesting_files = new_files self._interesting_paths = new_paths def _initialize(self): self.setHorizontalHeaderLabels(Columns.text_values()) self.entries = {} self._interesting_files = files = self.get_files() self._interesting_paths = self.get_paths(files=files) root = self.invisibleRootItem() self.populate_dir(root, './') def update_entry(self, path): if self.turbo or path not in self.entries: return # entry doesn't currently exist task = GitRepoInfoTask(self._parent, path, self.default_author) self._runtask.start(task)
class SpyderPluginV2(QObject, SpyderActionMixin, SpyderConfigurationObserver): """ A Spyder plugin to extend functionality without a dockable widget. If you want to create a plugin that adds a new pane, please use SpyderDockableWidget. """ # --- API: Mandatory attributes ------------------------------------------ # ------------------------------------------------------------------------ # Name of the plugin that will be used to refer to it. # This name must be unique and will only be loaded once. NAME = None # --- API: Optional attributes ------------------------------------------ # ------------------------------------------------------------------------ # List of required plugin dependencies. # Example: [Plugins.Plots, Plugins.IPythonConsole, ...]. # These values are defined in the `Plugins` class present in this file. # If a plugin is using a widget from another plugin, that other # must be declared as a required dependency. REQUIRES = None # List of optional plugin dependencies. # Example: [Plugins.Plots, Plugins.IPythonConsole, ...]. # These values are defined in the `Plugins` class present in this file. # A plugin might be performing actions when connectiong to other plugins, # but the main functionality of the plugin does not depend on other # plugins. For example, the Help plugin might render information from # the Editor or from the Console or from another source, but it does not # depend on either of those plugins. # Methods in the plugin that make use of optional plugins must check # existence before using those methods or applying signal connections. OPTIONAL = None # This must subclass a `PluginMainContainer` for non dockable plugins that # create a widget, like a status bar widget, a toolbar, a menu, etc. # For non dockable plugins that do not define widgets of any kind this can # be `None`, for example a plugin that only exposes a configuration page. CONTAINER_CLASS = None # Name of the configuration section that's going to be # used to record the plugin's permanent data in Spyder # config system (i.e. in spyder.ini) CONF_SECTION = None # Use a separate configuration file for the plugin. CONF_FILE = True # Define configuration defaults if using a separate file. # List of tuples, with the first item in the tuple being the section # name and the second item being the default options dictionary. # # CONF_DEFAULTS_EXAMPLE = [ # ('section-name', {'option-1': 'some-value', # 'option-2': True,}), # ('another-section-name', {'option-3': 'some-other-value', # 'option-4': [1, 2, 3],}), # ] CONF_DEFAULTS = None # Define configuration version if using a separate file # # IMPORTANT NOTES: # 1. If you want to *change* the default value of a current option, you # need to do a MINOR update in config version, e.g. from 3.0.0 to 3.1.0 # 2. If you want to *remove* options that are no longer needed or if you # want to *rename* options, then you need to do a MAJOR update in # version, e.g. from 3.0.0 to 4.0.0 # 3. You don't need to touch this value if you're just adding a new option CONF_VERSION = None # Widget to be used as entry in Spyder Preferences dialog. CONF_WIDGET_CLASS = None # Some plugins may add configuration options for other plugins. # Example: # ADDITIONAL_CONF_OPTIONS = {'section': <new value to add>} ADDITIONAL_CONF_OPTIONS = None # Define additional configurable options (via a tab) to # another's plugin configuration page. All configuration tabs should # inherit from `SpyderPreferencesTab`. # Example: # ADDITIONAL_CONF_TABS = {'plugin_name': [<SpyderPreferencesTab classes>]} ADDITIONAL_CONF_TABS = None # Path for images relative to the plugin path # A Python package can include one or several Spyder plugins. In this case # the package may be using images from a global folder outside the plugin # folder IMG_PATH = 'images' # Control the font size relative to the global fonts defined in Spyder FONT_SIZE_DELTA = 0 RICH_FONT_SIZE_DELTA = 0 # --- API: Signals ------------------------------------------------------- # ------------------------------------------------------------------------ # Signals here are automatically connected by the Spyder main window and # connected to the the respective global actions defined on it. sig_free_memory_requested = Signal() """ This signal can be emitted to request the main application to garbage collect deleted objects. """ sig_quit_requested = Signal() """ This signal can be emitted to request the main application to quit. """ sig_restart_requested = Signal() """ This signal can be emitted to request the main application to restart. """ sig_status_message_requested = Signal(str, int) """ This signal can be emitted to request the main application to display a message in the status bar. Parameters ---------- message: str The actual message to display. timeout: int The timeout before the message disappears. """ sig_redirect_stdio_requested = Signal(bool) """ This signal can be emitted to request the main application to redirect standard output/error when using Open/Save/Browse dialogs within widgets. Parameters ---------- enable: bool Enable/Disable standard input/output redirection. """ sig_exception_occurred = Signal(dict) """ This signal can be emitted to report an exception from any plugin. Parameters ---------- error_data: dict The dictionary containing error data. The expected keys are: >>> error_data= { "text": str, "is_traceback": bool, "repo": str, "title": str, "label": str, "steps": str, } Notes ----- The `is_traceback` key indicates if `text` contains plain text or a Python error traceback. The `title` and `repo` keys indicate how the error data should customize the report dialog and Github error submission. The `label` and `steps` keys allow customizing the content of the error dialog. This signal is automatically connected to the main container/widget. """ # --- Private attributes ------------------------------------------------- # ------------------------------------------------------------------------ # Define configuration name map for plugin to split configuration # among several files. See spyder/config/main.py _CONF_NAME_MAP = None def __init__(self, parent, configuration=None): super().__init__(parent) self._main = parent self._widget = None self._conf = configuration self._plugin_path = os.path.dirname(inspect.getfile(self.__class__)) self._container = None self._added_toolbars = OrderedDict() self._actions = {} self.is_compatible = None self.is_registered = None self.main = parent if self.CONTAINER_CLASS is not None: self._container = container = self.CONTAINER_CLASS( name=self.NAME, plugin=self, parent=parent ) if isinstance(container, SpyderWidgetMixin): container.setup() container.update_actions() if isinstance(container, PluginMainContainer): # Default signals to connect in main container or main widget. container.sig_exception_occurred.connect( self.sig_exception_occurred) container.sig_free_memory_requested.connect( self.sig_free_memory_requested) container.sig_quit_requested.connect( self.sig_quit_requested) container.sig_redirect_stdio_requested.connect( self.sig_redirect_stdio_requested) container.sig_restart_requested.connect( self.sig_restart_requested) self.after_container_creation() if hasattr(container, '_setup'): container._setup() # --- Private methods ---------------------------------------------------- # ------------------------------------------------------------------------ def _register(self): """ Setup and register plugin in Spyder's main window and connect it to other plugins. """ # Checks # -------------------------------------------------------------------- if self.NAME is None: raise SpyderAPIError('A Spyder Plugin must define a `NAME`!') if self.NAME in self._main._PLUGINS: raise SpyderAPIError( 'A Spyder Plugin with NAME="{}" already exists!'.format( self.NAME)) # Setup configuration # -------------------------------------------------------------------- if self._conf is not None: self._conf.register_plugin(self) # Signals # -------------------------------------------------------------------- self.is_registered = True self.update_font() def _unregister(self): """ Disconnect signals and clean up the plugin to be able to stop it while Spyder is running. """ if self._conf is not None: self._conf.unregister_plugin() self._container = None self.is_compatible = None self.is_registered = False # --- API: available methods --------------------------------------------- # ------------------------------------------------------------------------ def get_path(self): """ Return the plugin's system path. """ return self._plugin_path def get_container(self): """ Return the plugin main container. """ return self._container def get_configuration(self): """ Return the Spyder configuration object. """ return self._conf def get_main(self): """ Return the Spyder main window.. """ return self._main def get_plugin(self, plugin_name): """ Return a plugin instance by providing the plugin's NAME. """ # Ensure that this plugin has the plugin corresponding to # `plugin_name` listed as required or optional. requires = self.REQUIRES or [] optional = self.OPTIONAL or [] deps = [] for dependency in requires + optional: deps.append(dependency) PLUGINS = self._main._PLUGINS if plugin_name in deps: for name, plugin_instance in PLUGINS.items(): if name == plugin_name and name in deps: return plugin_instance else: if plugin_name in requires: raise SpyderAPIError( 'Required Plugin "{}" not found!'.format(plugin_name)) else: return None else: raise SpyderAPIError( 'Plugin "{}" not part of REQUIRES or ' 'OPTIONAL requirements!'.format(plugin_name) ) def get_conf(self, option, default=NoDefault, section=None): """ Get an option from Spyder configuration system. Parameters ---------- option: str Name of the option to get its value from. default: bool, int, str, tuple, list, dict, NoDefault Value to get from the configuration system, passed as a Python object. section: str Section in the configuration system, e.g. `shortcuts`. Returns ------- bool, int, str, tuple, list, dict Value associated with `option`. """ if self._conf is not None: section = self.CONF_SECTION if section is None else section if section is None: raise SpyderAPIError( 'A spyder plugin must define a `CONF_SECTION` class ' 'attribute!' ) return self._conf.get(section, option, default) @Slot(str, object) @Slot(str, object, str) def set_conf(self, option, value, section=None, recursive_notification=True): """ Set an option in Spyder configuration system. Parameters ---------- option: str Name of the option (e.g. 'case_sensitive') value: bool, int, str, tuple, list, dict Value to save in the configuration system, passed as a Python object. section: str Section in the configuration system, e.g. `shortcuts`. recursive_notification: bool If True, all objects that observe all changes on the configuration section and objects that observe partial tuple paths are notified. For example if the option `opt` of section `sec` changes, then the observers for section `sec` are notified. Likewise, if the option `(a, b, c)` changes, then observers for `(a, b, c)`, `(a, b)` and a are notified as well. """ if self._conf is not None: section = self.CONF_SECTION if section is None else section if section is None: raise SpyderAPIError( 'A spyder plugin must define a `CONF_SECTION` class ' 'attribute!' ) self._conf.set(section, option, value, recursive_notification=recursive_notification) self.apply_conf({option}, False) def remove_conf(self, option, section=None): """ Delete an option in the Spyder configuration system. Parameters ---------- option: Union[str, Tuple[str, ...]] Name of the option, either a string or a tuple of strings. section: str Section in the configuration system. """ if self._conf is not None: section = self.CONF_SECTION if section is None else section if section is None: raise SpyderAPIError( 'A spyder plugin must define a `CONF_SECTION` class ' 'attribute!' ) self._conf.remove_option(section, option) self.apply_conf({option}, False) def apply_conf(self, options_set, notify=True): """ Apply `options_set` to this plugin's widget. """ if self._conf is not None and options_set: container = self.get_container() if notify: self.after_configuration_update(list(options_set)) @Slot(str) @Slot(str, int) def show_status_message(self, message, timeout=0): """ Show message in status bar. Parameters ---------- message: str Message to display in the status bar. timeout: int Amount of time to display the message. """ self.sig_status_message_requested.emit(message, timeout) def before_long_process(self, message): """ Show a message in main window's status bar and change the mouse pointer to Qt.WaitCursor when starting a long process. Parameters ---------- message: str Message to show in the status bar when the long process starts. """ if message: self.show_status_message(message) QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) QApplication.processEvents() def after_long_process(self, message=""): """ Clear main window's status bar after a long process and restore mouse pointer to the OS deault. Parameters ---------- message: str Message to show in the status bar when the long process finishes. """ QApplication.restoreOverrideCursor() self.show_status_message(message, timeout=2000) QApplication.processEvents() def get_color_scheme(self): """ Get the current color scheme. Returns ------- dict Dictionary with properties and colors of the color scheme used in the Editor. Notes ----- This is useful to set the color scheme of all instances of CodeEditor used by the plugin. """ if self._conf is not None: return get_color_scheme(self._conf.get('appearance', 'selected')) @staticmethod def create_icon(name, path=None): """ Provide icons from the theme and icon manager. """ return ima.icon(name, icon_path=path) @classmethod def get_font(cls, rich_text=False): """ Return plain or rich text font used in Spyder. Parameters ---------- rich_text: bool Return rich text font (i.e. the one used in the Help pane) or plain text one (i.e. the one used in the Editor). Returns ------- QFont QFont object to be passed to other Qt widgets. Notes ----- All plugins in Spyder use the same, global font. This is a convenience method in case some plugins want to use a delta size based on the default one. That can be controlled by using FONT_SIZE_DELTA or RICH_FONT_SIZE_DELTA (declared in `SpyderPlugin`). """ if rich_text: option = 'rich_font' font_size_delta = cls.RICH_FONT_SIZE_DELTA else: option = 'font' font_size_delta = cls.FONT_SIZE_DELTA return get_font(option=option, font_size_delta=font_size_delta) def get_actions(self): """ Return a dictionary of actions exposed by the plugin and child widgets. It returns all actions defined by the Spyder plugin widget, wheter it is a PluginMainWidget or PluginMainContainer subclass. Notes ----- 1. Actions should be created once. Creating new actions on menu popup is *highly* discouraged. 2. Actions can be created directly on a PluginMainWidget or PluginMainContainer subclass. Child widgets can also create actions, but they need to subclass SpyderWidgetMixin. 3. The PluginMainWidget or PluginMainContainer will collect any actions defined in subwidgets (if defined) and expose them in the get_actions method at the plugin level. 4. Any action created this way is now exposed as a possible shortcut automatically without manual shortcut registration. If an option is found in the config system then it is assigned, otherwise it's left with an empty shortcut. 5. There is no need to override this method. """ container = self.get_container() actions = container.get_actions() if container is not None else {} actions.update(super().get_actions()) return actions def get_action(self, name): """ Return action defined in any of the child widgets by name. """ actions = self.get_actions() if name in actions: return actions[name] else: raise SpyderAPIError('Action "{0}" not found! Available ' 'actions are: {1}'.format(name, actions)) # --- API: Mandatory methods to define ----------------------------------- # ------------------------------------------------------------------------ def get_name(self): """ Return the plugin localized name. Returns ------- str Localized name of the plugin. Notes ----- This is a method to be able to update localization without a restart. """ raise NotImplementedError('A plugin name must be defined!') def get_description(self): """ Return the plugin localized description. Returns ------- str Localized description of the plugin. Notes ----- This is a method to be able to update localization without a restart. """ raise NotImplementedError('A plugin description must be defined!') def get_icon(self): """ Return the plugin associated icon. Returns ------- QIcon QIcon instance """ raise NotImplementedError('A plugin icon must be defined!') def register(self): """ Setup and register plugin in Spyder's main window and connect it to other plugins. """ raise NotImplementedError('Must define a register method!') # --- API: Optional methods to override ---------------------------------- # ------------------------------------------------------------------------ def unregister(self): """ Disconnect signals and clean up the plugin to be able to stop it while Spyder is running. """ pass @staticmethod def check_compatibility(): """ This method can be reimplemented to check compatibility of a plugin with the user's current environment. Returns ------- (bool, str) The first value tells Spyder if the plugin has passed the compatibility test defined in this method. The second value is a message that must explain users why the plugin was found to be incompatible (e.g. 'This plugin does not work with PyQt4'). It will be shown at startup in a QMessageBox. """ valid = True message = '' # Note: Remember to use _('') to localize the string return valid, message def on_first_registration(self): """ Actions to be performed the first time the plugin is started. It can also be used to perform actions that are needed only the first time this is loaded after installation. This method is called after the main window is visible. """ pass def on_mainwindow_visible(self): """ Actions to be performed after the main window's has been shown. """ pass def on_close(self, cancelable=False): """ Perform actions before the main window is closed. Returns ------- bool Whether the plugin may be closed immediately or not. Notes ----- The returned value is ignored if *cancelable* is False. """ return True def update_font(self): """ This must be reimplemented by plugins that need to adjust their fonts. The following plugins illustrate the usage of this method: * spyder/plugins/help/plugin.py * spyder/plugins/onlinehelp/plugin.py """ pass def update_style(self): """ This must be reimplemented by plugins that need to adjust their style. Changing from the dark to the light interface theme might require specific styles or stylesheets to be applied. When the theme is changed by the user through our Preferences, this method will be called for all plugins. """ pass def after_container_creation(self): """ Perform necessary operations before setting up the container. This must be reimplemented by plugins whose containers emit signals in on_option_update that need to be connected before applying those options to our config system. """ pass def after_configuration_update(self, options: List[Union[str, tuple]]): """ Perform additional operations after updating the plugin configuration values. This can be implemented by plugins that do not have a container and need to act on configuration updates. Parameters ---------- options: List[Union[str, tuple]] A list that contains the options that were updated. """ pass
class ResultsBrowser(OneColumnTree): sig_edit_goto = Signal(str, int, str) def __init__(self, parent): OneColumnTree.__init__(self, parent) self.search_text = None self.results = None self.nb = None self.error_flag = None self.completed = None self.data = None self.set_title('') self.root_items = None def activated(self, item): """Double-click event""" itemdata = self.data.get(id(self.currentItem())) if itemdata is not None: filename, lineno = itemdata self.sig_edit_goto.emit(filename, lineno, self.search_text) def clicked(self, item): """Click event""" self.activated(item) def set_results(self, search_text, results, pathlist, nb, error_flag, completed): self.search_text = search_text self.results = results self.pathlist = pathlist self.nb = nb self.error_flag = error_flag self.completed = completed self.refresh() if not self.error_flag and self.nb: self.restore() def refresh(self): """ Refreshing search results panel """ title = "'%s' - " % self.search_text if self.results is None: text = _('Search canceled') else: nb_files = len(self.results) if nb_files == 0: text = _('String not found') else: text_matches = _('matches in') text_files = _('file') if nb_files > 1: text_files += 's' text = "%d %s %d %s" % (self.nb, text_matches, nb_files, text_files) if self.error_flag: text += ' (' + self.error_flag + ')' elif self.results is not None and not self.completed: text += ' (' + _('interrupted') + ')' self.set_title(title+text) self.clear() self.data = {} if not self.results: # First search interrupted *or* No result return # Directory set dir_set = set() for filename in sorted(self.results.keys()): dirname = osp.abspath(osp.dirname(filename)) dir_set.add(dirname) # Root path root_path_list = None _common = get_common_path(list(dir_set)) if _common is not None: root_path_list = [_common] else: _common = get_common_path(self.pathlist) if _common is not None: root_path_list = [_common] else: root_path_list = self.pathlist if not root_path_list: return for _root_path in root_path_list: dir_set.add(_root_path) # Populating tree: directories def create_dir_item(dirname, parent): if dirname not in root_path_list: displayed_name = osp.basename(dirname) else: displayed_name = dirname item = QTreeWidgetItem(parent, [displayed_name], QTreeWidgetItem.Type) item.setIcon(0, ima.icon('DirClosedIcon')) return item dirs = {} for dirname in sorted(list(dir_set)): if dirname in root_path_list: parent = self else: parent_dirname = abspardir(dirname) parent = dirs.get(parent_dirname) if parent is None: # This is related to directories which contain found # results only in some of their children directories if osp.commonprefix([dirname]+root_path_list): # create new root path pass items_to_create = [] while dirs.get(parent_dirname) is None: items_to_create.append(parent_dirname) parent_dirname = abspardir(parent_dirname) items_to_create.reverse() for item_dir in items_to_create: item_parent = dirs[abspardir(item_dir)] dirs[item_dir] = create_dir_item(item_dir, item_parent) parent_dirname = abspardir(dirname) parent = dirs[parent_dirname] dirs[dirname] = create_dir_item(dirname, parent) self.root_items = [dirs[_root_path] for _root_path in root_path_list] # Populating tree: files for filename in sorted(self.results.keys()): parent_item = dirs[osp.dirname(filename)] file_item = QTreeWidgetItem(parent_item, [osp.basename(filename)], QTreeWidgetItem.Type) file_item.setIcon(0, get_filetype_icon(filename)) colno_dict = {} fname_res = [] for lineno, colno, line in self.results[filename]: if lineno not in colno_dict: fname_res.append((lineno, colno, line)) colno_dict[lineno] = colno_dict.get(lineno, [])+[str(colno)] for lineno, colno, line in fname_res: colno_str = ",".join(colno_dict[lineno]) item = QTreeWidgetItem(file_item, ["%d (%s): %s" % (lineno, colno_str, line.rstrip())], QTreeWidgetItem.Type) item.setIcon(0, ima.icon('arrow')) self.data[id(item)] = (filename, lineno) # Removing empty directories top_level_items = [self.topLevelItem(index) for index in range(self.topLevelItemCount())] for item in top_level_items: if not item.childCount(): self.takeTopLevelItem(self.indexOfTopLevelItem(item))
class EditSofQDialog(QDialog): """ Extended dialog class to edit S(Q) """ MyEditSignal = Signal(str, float, float) MySaveSignal = Signal(str) live_scale = None live_shift = None lock_plot = False def __init__(self, parent_window): """ initialization """ super(EditSofQDialog, self).__init__() # check inputs assert parent_window is not None, 'Parent window cannot be None.' self._myParentWindow = parent_window self._myDriver = parent_window.controller # initialize class variables self._scaleMin = None self._scaleMax = None self._shiftMin = None self._shiftMax = None self._shiftSlideMutex = False self._scaleSlideMutex = False # set up UI #self.ui = load_ui('colorStyleSetup.ui', baseinstance=self) self.ui = load_ui('editSq.ui', baseinstance=self) # set up default value self._init_widgets() # set up event handlers self.ui.pushButton_quit.clicked.connect(self.do_quit) self.ui.pushButton_saveNewSq.clicked.connect(self.do_save) # connect widgets' events with methods #self.ui.pushButton_editSQ.clicked.connect(self.do_edit_sq) self.ui.pushButton_cache.clicked.connect(self.do_cache_edited_sq) self.ui.pushButton_setScaleRange.clicked.connect(self.do_set_scale_range) self.ui.pushButton_setShiftRange.clicked.connect(self.do_set_shift_range) # connect q-slide # self.ui.horizontalSlider_scale.valueChanged.connect(self.scale_slider_value_changed) # self.ui.horizontalSlider_shift.valueChanged.connect(self.shift_slider_value_changed) # self.ui.horizontalSlider_scale.sliderPressed.connect(self.block_scale_value_changed) # self.ui.horizontalSlider_shift.sliderPressed.connect(self.block_shift_value_changed) # self.ui.horizontalSlider_scale.sliderReleased.connect(self.event_cal_sq) # self.ui.horizontalSlider_shift.sliderReleased.connect(self.event_cal_sq) self.ui.horizontalSlider_scale.valueChanged.connect(self.event_cal_sq) self.ui.horizontalSlider_shift.valueChanged.connect(self.event_cal_sq) self.ui.horizontalSlider_scale.sliderPressed.connect(self.lock_plot_refresh) self.ui.horizontalSlider_shift.sliderPressed.connect(self.lock_plot_refresh) self.ui.horizontalSlider_scale.sliderReleased.connect(self.slider_released) self.ui.horizontalSlider_shift.sliderReleased.connect(self.slider_released) # connect signals self.MyEditSignal.connect(self._myParentWindow.edit_sq) self.MySaveSignal.connect(self._myParentWindow.do_save_sq) # random number random.seed(1) def _init_widgets(self): """ initialize widgets by their default values :return: """ self.ui.comboBox_workspaces.clear() self.ui.scale_value.setText('1.') self.ui.shift_value.setText('0.') # slider limit self.ui.lineEdit_scaleMin.setText('0.0000') self.ui.lineEdit_scaleMax.setText('5') self.ui.lineEdit_shiftMin.setText('-5') self.ui.lineEdit_shiftMax.setText('5') # set up class variable self._scaleMin = 0.0000000001 self._scaleMax = 5 self._shiftMin = -5 self._shiftMax = 5 # set up the sliders self._shiftSlideMutex = True self.ui.horizontalSlider_shift.setMinimum(0) self.ui.horizontalSlider_shift.setMaximum(100) self.ui.horizontalSlider_shift.setValue(49) self._shiftSlideMutex = False # set up the scale self._scaleSlideMutex = True self.ui.horizontalSlider_scale.setMinimum(0) self.ui.horizontalSlider_scale.setMaximum(100) self.ui.horizontalSlider_scale.setValue(20) self._scaleSlideMutex = False self.calculate_shift_value() self.calculate_scale_value() def do_cache_edited_sq(self): """ cache the currently edited S(Q) :return: """ # get the current shift and scale factor shift_value = float(self.ui.shift_value.text()) scale_factor = float(self.ui.scale_value.text()) # convert them to string with 16 precision float %.16f % () key_shift = '%.16f' % shift_value key_scale = '%.16f' % scale_factor # only the raw workspace name is in the combo box. What wee need is the 'edited' version curr_ws_name = str(self.ui.comboBox_workspaces.currentText()) + '_Edit' # check whether any workspace has these key: shift_str/scale_str if self._myParentWindow.has_edit_sofq(curr_ws_name, key_shift, key_scale): print('Workspace {0} with shift = {1} and scale factor = {2} has already been cached.' ''.format(curr_ws_name, key_shift, key_scale)) return # get the name of current S(Q) with random sequence new_sq_ws_name = curr_ws_name + '_edit_{0}'.format(random.randint(1000, 9999)) # clone current workspace to new name, add to tree and combo box self._myDriver.clone_workspace(curr_ws_name, new_sq_ws_name) self._myParentWindow.add_edited_sofq(curr_ws_name, new_sq_ws_name, key_shift, key_scale) # clone G(r) to new name and add to tree generate_gr_step2(self._myParentWindow, [new_sq_ws_name]) def do_quit(self): """ close the window and quit :return: """ self.close() def do_save(self): """ save SofQ :return: """ # get the selected S(Q) sq_ws_name = str(self.ui.comboBox_workspaces.currentText()) self.MyEditSignal.emit(sq_ws_name) def add_sq_by_name(self, sq_name_list): """ add a list of S(Q) by workspace name :param sq_name_list: :return: """ # check assert isinstance(sq_name_list, list), 'S(Q) workspace names {0} must be given by list but not {1}' \ ''.format(sq_name_list, type(sq_name_list)) # add for sq_ws_name in sq_name_list: # TODO/FUTURE - Need to make this one work! (ALL) if sq_ws_name != 'All': self.add_workspace(sq_ws_name) def add_workspace(self, ws_name): """ add workspace name :return: """ # check input assert isinstance(ws_name, str), 'Input workspace name {0} must be a string but not a {1}.'.\ format(ws_name, type(ws_name)) self.ui.comboBox_workspaces.addItem(ws_name) def do_set_scale_range(self): """set the range of scale factor slider bar :return: """ # get new scale range min_scale = float(self.ui.lineEdit_scaleMin.text()) max_scale = float(self.ui.lineEdit_scaleMax.text()) # check valid or not! if min_scale >= max_scale: # if not valid: set the values back to stored original print('[ERROR] Minimum scale factor value {0} cannot exceed maximum scale factor value {1}.' ''.format(min_scale, max_scale)) return else: # re-set the class variable as the new min/max is accepted self._scaleMin = min_scale self._scaleMax = max_scale # otherwise, re-set the slider current_scale_factor = float(self.ui.scale_value.text()) if current_scale_factor < min_scale: current_scale_factor = min_scale elif current_scale_factor > max_scale: current_scale_factor = max_scale # TODO/ISSUE/NOW - clean up to multiple steps and check! delta_scale = max_scale - min_scale delta_slider_scale = self.ui.horizontalSlider_scale.maximum() - self.ui.horizontalSlider_scale.minimum() scale_factor_int = int(current_scale_factor/delta_scale * delta_slider_scale) self.ui.horizontalSlider_scale.setValue(scale_factor_int) def do_set_shift_range(self): """set the new range of shift slider :return: """ # get new scale range min_shift = float(self.ui.lineEdit_shiftMin.text()) max_shift = float(self.ui.lineEdit_shiftMax.text()) # check valid or not! if min_shift >= max_shift: # if not valid: set the values back to stored original print('[ERROR] Minimum scale factor value {0} cannot exceed maximum scale factor value {1}.' ''.format(min_shift, max_shift)) return else: # re-set the class variable as the new min/max is accepted self._shiftMin = min_shift self._shiftMax = max_shift # otherwise, re-set the slider curr_shift = float(self.ui.shift_value.text()) if curr_shift < min_shift: curr_shift = min_shift elif curr_shift > max_shift: curr_shift = max_shift # TODO/ISSUE/NOW - clean up to multiple steps and check! delta_shift = max_shift - min_shift delta_slider_shift = self.ui.horizontalSlider_shift.maximum() - self.ui.horizontalSlider_shift.minimum() shift_int = int(curr_shift/delta_shift * delta_slider_shift) self.ui.horizontalSlider_shift.setValue(shift_int) def shift_slider_value_pressed(self): self.shift_slider_value_changed(-1) def shift_slider_value_changed(self, _): # check whether mutex is on or off if self._shiftSlideMutex or self._scaleSlideMutex: # return if either mutex is on: it is not a time to do calculation return shift = self.get_shift_value() self.ui.shift_value.setText('%.7f' % shift) def get_shift_value(self): self.calculate_shift_value() return self.live_shift def calculate_shift_value(self): # read the value of sliders # note: change is [min, max]. and the default is [0, 100] shift_int = self.ui.horizontalSlider_shift.value() # convert to double delta_shift = self._shiftMax - self._shiftMin delta_shift_slider = self.ui.horizontalSlider_shift.maximum() - self.ui.horizontalSlider_shift.minimum() shift = self._shiftMin + float(shift_int) / delta_shift_slider * delta_shift self.live_shift = shift def scale_slider_value_pressed(self): self.scale_slider_value_changed(-1) def scale_slider_value_changed(self, value): # check whether mutex is on or off if self._shiftSlideMutex or self._scaleSlideMutex: # return if either mutex is on: it is not a time to do calculation return scale = self.get_scale_value() self.ui.scale_value.setText('%.7f' % scale) def get_scale_value(self): self.calculate_scale_value() return self.live_scale def calculate_scale_value(self): # read the value of sliders # note: change is [min, max]. and the default is [0, 100] scale_int = self.ui.horizontalSlider_scale.value() delta_scale = self._scaleMax - self._scaleMin delta_scale_slider = self.ui.horizontalSlider_scale.maximum() - self.ui.horizontalSlider_scale.minimum() scale = self._scaleMin + float(scale_int) / delta_scale_slider * delta_scale self.live_scale = scale def lock_plot_refresh(self): self.lock_plot = True def unlock_plot_refresh(self): self.lock_plot = False def slider_released(self): self.unlock_plot_refresh() self.event_cal_sq() def event_cal_sq(self): """handling the events from a moving sliding bar such that a new S(Q) will be calculated :return: """ self.scale_slider_value_pressed() self.shift_slider_value_pressed() if self.lock_plot: return # call edit_sq() self.edit_sq(self.live_shift, self.live_scale) # enable mutex self._shiftSlideMutex = True self._scaleSlideMutex = True # edit line edits for shift and scale self.ui.scale_value.setText('%.7f' % self.live_scale) self.ui.shift_value.setText('%.7f' % self.live_shift) # disable mutex self._shiftSlideMutex = False self._scaleSlideMutex = False def edit_sq(self, shift, scale_factor): """ edit S(Q) :param shift: :param scale_factor: :return: """ # check assert isinstance(shift, float), 'Shift {0} must be a float but not a {1}.' \ ''.format(shift, type(shift)) assert isinstance(scale_factor, float), 'Scale factor {0} must be a float but not a {1}.' \ ''.format(scale_factor, type(scale_factor)) # get the workspace name workspace_name = str(self.ui.comboBox_workspaces.currentText()) if len(workspace_name) == 0: print('[INFO] No workspace is selected') # set out the signal self.MyEditSignal.emit(workspace_name, scale_factor, shift) def set_slider_scale_value(self, scale_factor): # TODO/ISSUE/NOW pass def set_slider_shift_value(self, shift): """ set the new shift value to the slide bar :param shift: :return: """ # check input assert isinstance(shift, float) or isinstance(shift, int), 'Shift value {0} must be an integer or float,' \ 'but not a {1}.'.format(shift, type(shift)) # convert from user-interface shift value to slider integer value ratio_float = (shift - self._shiftMin) / (self._shiftMax - self._shiftMin) slider_range = self.ui.horizontalSlider_shift.maximum() - self.ui.horizontalSlider_shift.minimum() slide_value = int(ratio_float * slider_range) # set the shift value self.ui.horizontalSlider_shift.setValue(slide_value)
class PydocBrowser(PluginMainWidget): """PyDoc browser widget.""" DEFAULT_OPTIONS = { 'handle_links': False, 'max_history_entries': 10, 'zoom_factor': 1, } ENABLE_SPINNER = True # --- Signals # ------------------------------------------------------------------------ sig_load_finished = Signal() """ This signal is emitted to indicate the help page has finished loading. """ def __init__(self, name=None, plugin=None, parent=None, options=DEFAULT_OPTIONS): super().__init__(name, plugin, parent=parent, options=options) self._is_running = False self.home_url = None self.server = None # Widgets self.label = QLabel(_("Package:")) self.url_combo = UrlComboBox(self) self.webview = WebView(self, handle_links=self.get_option('handle_links')) self.find_widget = FindReplace(self) # Setup self.find_widget.set_editor(self.webview) self.find_widget.hide() self.url_combo.setMaxCount(self.get_option('max_history_entries')) tip = _('Write a package name here, e.g. pandas') self.url_combo.lineEdit().setPlaceholderText(tip) self.url_combo.lineEdit().setToolTip(tip) self.webview.setup() self.webview.set_zoom_factor(self.get_option('zoom_factor')) # Layout spacing = 10 layout = QVBoxLayout() layout.addWidget(self.webview) layout.addSpacing(spacing) layout.addWidget(self.find_widget) layout.addSpacing(int(spacing / 2)) self.setLayout(layout) # Signals self.url_combo.valid.connect( lambda x: self._handle_url_combo_activation()) self.webview.loadStarted.connect(self._start) self.webview.loadFinished.connect(self._finish) self.webview.titleChanged.connect(self.setWindowTitle) self.webview.urlChanged.connect(self._change_url) if not WEBENGINE: self.webview.iconChanged.connect(self._handle_icon_change) # --- PluginMainWidget API # ------------------------------------------------------------------------ def get_title(self): return _('Online help') def get_focus_widget(self): self.url_combo.lineEdit().selectAll() return self.url_combo def setup(self, options={}): # Actions home_action = self.create_action( PydocBrowserActions.Home, text=_("Home"), tip=_("Home"), icon=self.create_icon('home'), triggered=self.go_home, ) find_action = self.create_action( PydocBrowserActions.Find, text=_("Find"), tip=_("Find text"), icon=self.create_icon('find'), toggled=self.toggle_find_widget, initial=False, ) stop_action = self.get_action(WebViewActions.Stop) refresh_action = self.get_action(WebViewActions.Refresh) # Toolbar toolbar = self.get_main_toolbar() for item in [ self.get_action(WebViewActions.Back), self.get_action(WebViewActions.Forward), refresh_action, stop_action, home_action, self.label, self.url_combo, self.get_action(WebViewActions.ZoomIn), self.get_action(WebViewActions.ZoomOut), find_action, ]: self.add_item_to_toolbar( item, toolbar=toolbar, section=PydocBrowserMainToolbarSections.Main, ) # Signals self.find_widget.visibility_changed.connect(find_action.setChecked) for __, action in self.get_actions().items(): if action: # IMPORTANT: Since we are defining the main actions in here # and the context is WidgetWithChildrenShortcut we need to # assign the same actions to the children widgets in order # for shortcuts to work self.webview.addAction(action) self.sig_toggle_view_changed.connect(self.initialize) def update_actions(self): stop_action = self.get_action(WebViewActions.Stop) refresh_action = self.get_action(WebViewActions.Refresh) refresh_action.setVisible(not self._is_running) stop_action.setVisible(self._is_running) def on_option_update(self, option, value): pass # --- Private API # ------------------------------------------------------------------------ def _start(self): """Webview load started.""" self._is_running = True self.start_spinner() self.update_actions() def _finish(self, code): """Webview load finished.""" self._is_running = False self.stop_spinner() self.update_actions() self.sig_load_finished.emit() def _continue_initialization(self): """Load home page.""" self.go_home() QApplication.restoreOverrideCursor() def _handle_url_combo_activation(self): """Load URL from combo box first item.""" if not self._is_running: text = str(self.url_combo.currentText()) self.go_to(self.text_to_url(text)) else: self.get_action(WebViewActions.Stop).trigger() self.get_focus_widget().setFocus() def _change_url(self, url): """ Displayed URL has changed -> updating URL combo box. """ self.url_combo.add_text(self.url_to_text(url)) def _handle_icon_change(self): """ Handle icon changes. """ self.url_combo.setItemIcon(self.url_combo.currentIndex(), self.webview.icon()) self.setWindowIcon(self.webview.icon()) # --- Qt overrides # ------------------------------------------------------------------------ def closeEvent(self, event): self.server.quit_server() event.accept() # --- Public API # ------------------------------------------------------------------------ def load_history(self, history): """ Load history. Parameters ---------- history: list List of searched items. """ self.url_combo.addItems(history) @Slot(bool) def initialize(self, checked=True): """ Start pydoc server. Parameters ---------- checked: bool, optional This method is connected to the `sig_toggle_view_changed` signal, so that the first time the widget is made visible it will start the server. Default is True. """ if checked and self.server is None: self.sig_toggle_view_changed.disconnect(self.initialize) QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) QApplication.processEvents() self.start_server() def is_server_running(self): """Return True if pydoc server is already running.""" return self.server is not None def start_server(self): """Start pydoc server.""" if self.server is None: self.set_home_url('http://127.0.0.1:{}/'.format(PORT)) elif self.server.isRunning(): self.server.sig_server_started.disconnect( self._continue_initialization) self.server.quit() self.server = PydocServer(port=PORT) self.server.sig_server_started.connect(self._continue_initialization) self.server.start() def quit_server(self): """Quit the server.""" if self.server is None: return if self.server.is_running(): self.server.sig_server_started.disconnect( self._continue_initialization) self.server.quit_server() self.server.quit() def get_label(self): """Return address label text""" return _("Package:") def reload(self): """Reload page.""" if self.server: self.webview.reload() def text_to_url(self, text): """ Convert text address into QUrl object. Parameters ---------- text: str Url address. """ if text != 'about:blank': text += '.html' if text.startswith('/'): text = text[1:] return QUrl(self.home_url.toString() + text) def url_to_text(self, url): """ Convert QUrl object to displayed text in combo box. Parameters ---------- url: QUrl Url address. """ string_url = url.toString() if 'about:blank' in string_url: return 'about:blank' elif 'get?key=' in string_url or 'search?key=' in string_url: return url.toString().split('=')[-1] return osp.splitext(str(url.path()))[0][1:] def set_home_url(self, text): """ Set home URL. Parameters ---------- text: str Home url address. """ self.home_url = QUrl(text) def set_url(self, url): """ Set current URL. Parameters ---------- url: QUrl or str Url address. """ self._change_url(url) self.go_to(url) def go_to(self, url_or_text): """ Go to page URL. """ if isinstance(url_or_text, str): url = QUrl(url_or_text) else: url = url_or_text self.webview.load(url) @Slot() def go_home(self): """ Go to home page. """ if self.home_url is not None: self.set_url(self.home_url) def get_zoom_factor(self): """ Get the current zoom factor. Returns ------- int Zoom factor. """ return self.webview.get_zoom_factor() def get_history(self): """ Return the list of history items in the combobox. Returns ------- list List of strings. """ history = [] for index in range(self.url_combo.count()): history.append(str(self.url_combo.itemText(index))) return history @Slot(bool) def toggle_find_widget(self, state): """ Show/hide the find widget. Parameters ---------- state: bool True to show and False to hide the find widget. """ if state: self.find_widget.show() else: self.find_widget.hide()
class EditableLineEdit(QWidget): """ """ sig_text_changed = Signal(object, object) # old_text, new_text def __init__(self, title, text, regex=None, allow_empty=False): super(EditableLineEdit, self).__init__() self._label = QLabel(title) self._text = QLineEdit() self.button_edit = QPushButton() self.allow_empty = allow_empty self.regex = regex self.qregex = None self.button_edit.setIcon(qta.icon('fa.edit')) self._text.setText(text) layout = QVBoxLayout() layout.addWidget(self._label) layout_h = QHBoxLayout() layout_h.addWidget(self._text) layout_h.addWidget(self.button_edit) layout.addLayout(layout_h) self.setLayout(layout) self._text.setDisabled(True) self.button_edit.clicked.connect(self.edit) self.last_text = self._text.text() self.set_regex(regex) # def focusOutEvent(self, event): # """ # Qt override. # FIXME: # """ # super(EditableLineEdit, self).focusOutEvent(event) # event = QKeyEvent(QKeyEvent.KeyPress, Qt.Key_Escape) # self.keyPressEvent(event) def keyPressEvent(self, event): """ Qt override. """ super(EditableLineEdit, self).keyPressEvent(event) key = event.key() if key in [Qt.Key_Enter, Qt.Key_Return]: self.check_text() elif key in [Qt.Key_Escape]: self._text.setText(self.last_text) self.check_text(escaped=True) # --- Public API # ------------------------------------------------------------------------- def text(self): return self._text.text() def setText(self, text): self.set_text(text) def set_text(self, text): """ """ self._text.setText(text) def set_label_text(self, text): """ """ self.label.setText(text) def set_regex(self, regex): """ """ if regex: self.regex = regex self.qregex = QRegExp(regex) validator = QRegExpValidator(self.qregex) self._text.setValidator(validator) def check_text(self, escaped=False): """ """ self._text.setDisabled(True) self.button_edit.setDisabled(False) new_text = self._text.text() if not self.allow_empty and len(new_text) == 0: self.edit() if self.last_text != new_text and not escaped: self.sig_text_changed.emit(self.last_text, new_text) self.last_text = new_text def edit(self): """ """ self._text.setDisabled(False) self.button_edit.setDisabled(True) self._text.setFocus() self._text.setCursorPosition(len(self._text.text())) self.last_text = self._text.text()
class NamespaceBrowser(QWidget): """Namespace browser (global variables explorer widget)""" sig_option_changed = Signal(str, object) sig_collapse = Signal() sig_free_memory = Signal() def __init__(self, parent, options_button=None, plugin_actions=[]): QWidget.__init__(self, parent) self.shellwidget = None self.is_visible = True 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.minmax = None # Other setting self.dataframe_format = 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.options_button = options_button self.actions = None self.plugin_actions = plugin_actions self.filename = None def setup(self, check_all=None, exclude_private=None, exclude_uppercase=None, exclude_capitalized=None, exclude_unsupported=None, excluded_names=None, minmax=None, dataframe_format=None): """ Setup the namespace browser with provided settings. Args: dataframe_format (string): default floating-point format for DataFrame editor """ 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.minmax = minmax self.dataframe_format = dataframe_format if self.editor is not None: self.editor.setup_menu(minmax) self.editor.set_dataframe_format(dataframe_format) 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) self.refresh_table() return self.editor = RemoteCollectionsEditorTableView( self, data=None, minmax=minmax, shellwidget=self.shellwidget, dataframe_format=dataframe_format) self.editor.sig_option_changed.connect(self.sig_option_changed.emit) self.editor.sig_files_dropped.connect(self.import_data) self.editor.sig_free_memory.connect(self.sig_free_memory.emit) self.setup_option_actions(exclude_private, exclude_uppercase, exclude_capitalized, exclude_unsupported) # Setup toolbar layout. self.tools_layout = QHBoxLayout() toolbar = self.setup_toolbar() for widget in toolbar: self.tools_layout.addWidget(widget) self.tools_layout.addStretch() self.setup_options_button() # Setup layout. layout = create_plugin_layout(self.tools_layout, self.editor) self.setLayout(layout) self.sig_option_changed.connect(self.option_changed) def set_shellwidget(self, shellwidget): """Bind shellwidget instance to namespace browser""" self.shellwidget = shellwidget shellwidget.set_namespacebrowser(self) def get_actions(self): """Get actions of the widget.""" return self.actions def setup_toolbar(self): """Setup toolbar""" load_button = create_toolbutton(self, text=_('Import data'), icon=ima.icon('fileimport'), triggered=lambda: 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) reset_namespace_button = create_toolbutton( self, text=_("Remove all variables"), icon=ima.icon('editdelete'), triggered=self.reset_namespace) return [ load_button, self.save_button, save_as_button, reset_namespace_button ] def setup_option_actions(self, exclude_private, exclude_uppercase, exclude_capitalized, exclude_unsupported): """Setup the actions to show in the cog menu.""" self.setup_in_progress = True 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) self.actions = [ self.exclude_private_action, self.exclude_uppercase_action, self.exclude_capitalized_action, self.exclude_unsupported_action ] if is_module_installed('numpy'): self.actions.extend([MENU_SEPARATOR, self.editor.minmax_action]) self.setup_in_progress = False def setup_options_button(self): """Add the cog menu button to the toolbar.""" if not self.options_button: self.options_button = create_toolbutton( self, text=_('Options'), icon=ima.icon('tooloptions')) actions = self.actions + [MENU_SEPARATOR] + self.plugin_actions self.options_menu = QMenu(self) add_actions(self.options_menu, actions) self.options_button.setMenu(self.options_menu) if self.tools_layout.itemAt(self.tools_layout.count() - 1) is None: self.tools_layout.insertWidget(self.tools_layout.count() - 1, self.options_button) else: self.tools_layout.addWidget(self.options_button) def option_changed(self, option, value): """Option has changed""" setattr(self, to_text_string(option), value) self.shellwidget.set_namespace_view_settings() self.refresh_table() def get_view_settings(self): """Return dict editor view settings""" settings = {} for name in REMOTE_SETTINGS: settings[name] = getattr(self, name) return settings def refresh_table(self): """Refresh variable table""" if self.is_visible and self.isVisible(): self.shellwidget.refresh_namespacebrowser() try: self.editor.resizeRowToContents() except TypeError: pass def process_remote_view(self, remote_view): """Process remote view""" if remote_view is not None: self.set_data(remote_view) def set_var_properties(self, properties): """Set properties of variables""" if properties is not None: self.editor.var_properties = properties 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(bool) @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_or_home() 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) 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() self.editor.new_value(var_name, clip_data) except Exception as error: error_message = str(error) else: QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) QApplication.processEvents() error_message = self.shellwidget.load_data(self.filename, ext) self.shellwidget._kernel_reply = None 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 reset_namespace(self): warning = CONF.get('ipython_console', 'show_reset_namespace_warning') self.shellwidget.reset_namespace(warning=warning, silent=True, message=True) @Slot() def save_data(self, filename=None): """Save data""" if filename is None: filename = self.filename if filename is None: filename = getcwd_or_home() 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() error_message = self.shellwidget.save_namespace(self.filename) self.shellwidget._kernel_reply = None 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 FallbackActor(QObject): #: Signal emitted when the Thread is ready sig_fallback_ready = Signal() sig_set_tokens = Signal(object, list) sig_mailbox = Signal(dict) def __init__(self, parent): QObject.__init__(self) self.stopped = False self.daemon = True self.mutex = QMutex() self.file_tokens = {} self.diff_patch = diff_match_patch() self.thread = QThread() self.moveToThread(self.thread) self.thread.started.connect(self.started) self.sig_mailbox.connect(self.handle_msg) def tokenize(self, text, language): """ Return all tokens in `text` and all keywords associated by Pygments to `language`. """ try: lexer = get_lexer_by_name(language) keywords = get_keywords(lexer) except Exception: keywords = [] keyword_set = set(keywords) keywords = [{ 'kind': CompletionItemKind.KEYWORD, 'insertText': keyword, 'sortText': keyword[0].lower(), 'filterText': keyword, 'documentation': '' } for keyword in keywords] # logger.debug(keywords) # tokens = list(lexer.get_tokens(text)) # logger.debug(tokens) tokens = get_words(text, language) tokens = [{ 'kind': CompletionItemKind.TEXT, 'insertText': token, 'sortText': token[0].lower(), 'filterText': token, 'documentation': '' } for token in tokens] for token in tokens: if token['insertText'] not in keyword_set: keywords.append(token) return keywords def stop(self): """Stop actor.""" with QMutexLocker(self.mutex): logger.debug("Fallback plugin stopping...") self.thread.quit() def start(self): """Start thread.""" self.thread.start() def started(self): """Thread started.""" logger.debug('Fallback plugin starting...') self.sig_fallback_ready.emit() @Slot(dict) def handle_msg(self, message): """Handle one message""" msg_type, file, msg, editor = [ message[k] for k in ('type', 'file', 'msg', 'editor') ] if msg_type == 'update': if file not in self.file_tokens: self.file_tokens[file] = { 'text': '', 'language': msg['language'] } diff = msg['diff'] text = self.file_tokens[file] text, _ = self.diff_patch.patch_apply(diff, text['text']) self.file_tokens[file]['text'] = text elif msg_type == 'close': self.file_tokens.pop(file, {}) elif msg_type == 'retrieve': tokens = [] if file in self.file_tokens: text_info = self.file_tokens[file] tokens = self.tokenize(text_info['text'], text_info['language']) self.sig_set_tokens.emit(editor, tokens)
class ProfilerWidget(QWidget): """ Profiler widget """ DATAPATH = get_conf_path('profiler.results') VERSION = '0.0.1' redirect_stdio = Signal(bool) def __init__(self, parent, max_entries=100, options_button=None): QWidget.__init__(self, parent) self.setWindowTitle("Profiler") self.output = None self.error_output = None self._last_wdir = None self._last_args = None self._last_pythonpath = None self.filecombo = PythonModulesComboBox(self) self.start_button = create_toolbutton(self, icon=ima.icon('run'), text=_("Profile"), tip=_("Run profiler"), triggered=lambda: self.start(), text_beside_icon=True) self.stop_button = create_toolbutton(self, icon=ima.icon('stop'), text=_("Stop"), tip=_("Stop current profiling"), text_beside_icon=True) self.filecombo.valid.connect(self.start_button.setEnabled) #self.connect(self.filecombo, SIGNAL('valid(bool)'), self.show_data) # FIXME: The combobox emits this signal on almost any event # triggering show_data() too early, too often. browse_button = create_toolbutton(self, icon=ima.icon('fileopen'), tip=_('Select Python script'), triggered=self.select_file) self.datelabel = QLabel() self.log_button = create_toolbutton(self, icon=ima.icon('log'), text=_("Output"), text_beside_icon=True, tip=_("Show program's output"), triggered=self.show_log) self.datatree = ProfilerDataTree(self) self.collapse_button = create_toolbutton( self, icon=ima.icon('collapse'), triggered=lambda dD: self.datatree.change_view(-1), tip=_('Collapse one level up')) self.expand_button = create_toolbutton( self, icon=ima.icon('expand'), triggered=lambda dD: self.datatree.change_view(1), tip=_('Expand one level down')) self.save_button = create_toolbutton(self, text_beside_icon=True, text=_("Save data"), icon=ima.icon('filesave'), triggered=self.save_data, tip=_('Save profiling data')) self.load_button = create_toolbutton( self, text_beside_icon=True, text=_("Load data"), icon=ima.icon('fileimport'), triggered=self.compare, tip=_('Load profiling data for comparison')) self.clear_button = create_toolbutton(self, text_beside_icon=True, text=_("Clear comparison"), icon=ima.icon('editdelete'), triggered=self.clear) hlayout1 = QHBoxLayout() hlayout1.addWidget(self.filecombo) hlayout1.addWidget(browse_button) hlayout1.addWidget(self.start_button) hlayout1.addWidget(self.stop_button) if options_button: hlayout1.addWidget(options_button) hlayout2 = QHBoxLayout() hlayout2.addWidget(self.collapse_button) hlayout2.addWidget(self.expand_button) hlayout2.addStretch() hlayout2.addWidget(self.datelabel) hlayout2.addStretch() hlayout2.addWidget(self.log_button) hlayout2.addWidget(self.save_button) hlayout2.addWidget(self.load_button) hlayout2.addWidget(self.clear_button) layout = QVBoxLayout() layout.addLayout(hlayout1) layout.addLayout(hlayout2) layout.addWidget(self.datatree) self.setLayout(layout) self.process = None self.set_running_state(False) self.start_button.setEnabled(False) self.clear_button.setEnabled(False) if not is_profiler_installed(): # This should happen only on certain GNU/Linux distributions # or when this a home-made Python build because the Python # profilers are included in the Python standard library for widget in (self.datatree, self.filecombo, self.start_button, self.stop_button): widget.setDisabled(True) url = 'https://docs.python.org/3/library/profile.html' text = '%s <a href=%s>%s</a>' % (_('Please install'), url, _("the Python profiler modules")) self.datelabel.setText(text) else: pass # self.show_data() def save_data(self): """Save data""" title = _("Save profiler result") filename, _selfilter = getsavefilename( self, title, getcwd_or_home(), _("Profiler result") + " (*.Result)") if filename: self.datatree.save_data(filename) def compare(self): filename, _selfilter = getopenfilename( self, _("Select script to compare"), getcwd_or_home(), _("Profiler result") + " (*.Result)") if filename: self.datatree.compare(filename) self.show_data() self.clear_button.setEnabled(True) def clear(self): self.datatree.compare(None) self.datatree.hide_diff_cols(True) self.show_data() self.clear_button.setEnabled(False) def analyze(self, filename, wdir=None, args=None, pythonpath=None): if not is_profiler_installed(): return self.kill_if_running() #index, _data = self.get_data(filename) index = None # FIXME: storing data is not implemented yet if index is None: self.filecombo.addItem(filename) self.filecombo.setCurrentIndex(self.filecombo.count() - 1) else: self.filecombo.setCurrentIndex(self.filecombo.findText(filename)) self.filecombo.selected() if self.filecombo.is_valid(): if wdir is None: wdir = osp.dirname(filename) self.start(wdir, args, pythonpath) def select_file(self): self.redirect_stdio.emit(False) filename, _selfilter = getopenfilename( self, _("Select Python script"), getcwd_or_home(), _("Python scripts") + " (*.py ; *.pyw)") self.redirect_stdio.emit(True) if filename: self.analyze(filename) def show_log(self): if self.output: TextEditor(self.output, title=_("Profiler output"), readonly=True, size=(700, 500)).exec_() def show_errorlog(self): if self.error_output: TextEditor(self.error_output, title=_("Profiler output"), readonly=True, size=(700, 500)).exec_() def start(self, wdir=None, args=None, pythonpath=None): filename = to_text_string(self.filecombo.currentText()) if wdir is None: wdir = self._last_wdir if wdir is None: wdir = osp.basename(filename) if args is None: args = self._last_args if args is None: args = [] if pythonpath is None: pythonpath = self._last_pythonpath self._last_wdir = wdir self._last_args = args self._last_pythonpath = pythonpath self.datelabel.setText(_('Profiling, please wait...')) self.process = QProcess(self) self.process.setProcessChannelMode(QProcess.SeparateChannels) self.process.setWorkingDirectory(wdir) self.process.readyReadStandardOutput.connect(self.read_output) self.process.readyReadStandardError.connect( lambda: self.read_output(error=True)) self.process.finished.connect( lambda ec, es=QProcess.ExitStatus: self.finished(ec, es)) self.stop_button.clicked.connect(self.kill) if pythonpath is not None: env = [ to_text_string(_pth) for _pth in self.process.systemEnvironment() ] add_pathlist_to_PYTHONPATH(env, pythonpath) processEnvironment = QProcessEnvironment() for envItem in env: envName, separator, envValue = envItem.partition('=') processEnvironment.insert(envName, envValue) self.process.setProcessEnvironment(processEnvironment) self.output = '' self.error_output = '' self.stopped = False p_args = ['-m', 'cProfile', '-o', self.DATAPATH] if os.name == 'nt': # On Windows, one has to replace backslashes by slashes to avoid # confusion with escape characters (otherwise, for example, '\t' # will be interpreted as a tabulation): p_args.append(osp.normpath(filename).replace(os.sep, '/')) else: p_args.append(filename) if args: p_args.extend(shell_split(args)) executable = sys.executable if executable.endswith("spyder.exe"): # py2exe distribution executable = "python.exe" self.process.start(executable, p_args) running = self.process.waitForStarted() self.set_running_state(running) if not running: QMessageBox.critical(self, _("Error"), _("Process failed to start")) def kill(self): """Stop button pressed.""" self.process.kill() self.stopped = True def set_running_state(self, state=True): self.start_button.setEnabled(not state) self.stop_button.setEnabled(state) def read_output(self, error=False): if error: self.process.setReadChannel(QProcess.StandardError) else: self.process.setReadChannel(QProcess.StandardOutput) qba = QByteArray() while self.process.bytesAvailable(): if error: qba += self.process.readAllStandardError() else: qba += self.process.readAllStandardOutput() text = to_text_string(locale_codec.toUnicode(qba.data())) if error: self.error_output += text else: self.output += text def finished(self, exit_code, exit_status): self.set_running_state(False) self.show_errorlog() # If errors occurred, show them. self.output = self.error_output + self.output # FIXME: figure out if show_data should be called here or # as a signal from the combobox self.show_data(justanalyzed=True) def kill_if_running(self): if self.process is not None: if self.process.state() == QProcess.Running: self.process.kill() self.process.waitForFinished() def show_data(self, justanalyzed=False): if not justanalyzed: self.output = None self.log_button.setEnabled(self.output is not None and len(self.output) > 0) self.kill_if_running() filename = to_text_string(self.filecombo.currentText()) if not filename: return if self.stopped: self.datelabel.setText(_('Run stopped by user.')) self.datatree.initialize_view() return self.datelabel.setText(_('Sorting data, please wait...')) QApplication.processEvents() self.datatree.load_data(self.DATAPATH) self.datatree.show_tree() text_style = "<span style=\'color: #444444\'><b>%s </b></span>" date_text = text_style % time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) self.datelabel.setText(date_text)
class PylintWidget(PluginMainWidget): """ Pylint widget. """ DEFAULT_OPTIONS = { "history_filenames": [], "max_entries": 30, "project_dir": None, } ENABLE_SPINNER = True DATAPATH = get_conf_path("pylint.results") VERSION = "1.1.0" # --- Signals sig_edit_goto_requested = Signal(str, int, str) """ This signal will request to open a file in a given row and column using a code editor. Parameters ---------- path: str Path to file. row: int Cursor starting row position. word: str Word to select on given row. """ sig_start_analysis_requested = Signal() """ This signal will request the plugin to start the analysis. This is to be able to interact with other plugins, which can only be done at the plugin level. """ def __init__(self, name=None, plugin=None, parent=None, options=DEFAULT_OPTIONS): super().__init__(name, plugin, parent, options) # Attributes self._process = None self.output = None self.error_output = None self.filename = None self.rdata = [] self.curr_filenames = self.get_option("history_filenames") self.code_analysis_action = None self.browse_action = None # Widgets self.filecombo = PythonModulesComboBox(self) self.ratelabel = QLabel(self) self.datelabel = QLabel(self) self.treewidget = ResultsTree(self) if osp.isfile(self.DATAPATH): try: with open(self.DATAPATH, "rb") as fh: data = pickle.loads(fh.read()) if data[0] == self.VERSION: self.rdata = data[1:] except (EOFError, ImportError): pass # Widget setup self.filecombo.setInsertPolicy(self.filecombo.InsertAtTop) for fname in self.curr_filenames[::-1]: self.set_filename(fname) # Layout layout = QVBoxLayout() layout.addWidget(self.treewidget) self.setLayout(layout) # Signals self.filecombo.valid.connect(self._check_new_file) self.treewidget.sig_edit_goto_requested.connect( self.sig_edit_goto_requested) # --- Private API # ------------------------------------------------------------------------ @Slot() def _start(self): """Start the code analysis.""" self.start_spinner() self.output = "" self.error_output = "" self._process = process = QProcess(self) process.setProcessChannelMode(QProcess.SeparateChannels) process.setWorkingDirectory(getcwd_or_home()) process.readyReadStandardOutput.connect(self._read_output) process.readyReadStandardError.connect( lambda: self._read_output(error=True)) process.finished.connect( lambda ec, es=QProcess.ExitStatus: self._finished(ec, es)) command_args = self.get_command(self.get_filename()) processEnvironment = QProcessEnvironment() processEnvironment.insert("PYTHONIOENCODING", "utf8") # resolve spyder-ide/spyder#14262 if running_in_mac_app(): pyhome = os.environ.get("PYTHONHOME") processEnvironment.insert("PYTHONHOME", pyhome) process.setProcessEnvironment(processEnvironment) process.start(sys.executable, command_args) running = process.waitForStarted() if not running: self.stop_spinner() QMessageBox.critical( self, _("Error"), _("Process failed to start"), ) def _read_output(self, error=False): process = self._process if error: process.setReadChannel(QProcess.StandardError) else: process.setReadChannel(QProcess.StandardOutput) qba = QByteArray() while process.bytesAvailable(): if error: qba += process.readAllStandardError() else: qba += process.readAllStandardOutput() text = str(qba.data(), "utf-8") if error: self.error_output += text else: self.output += text self.update_actions() def _finished(self, exit_code, exit_status): if not self.output: self.stop_spinner() if self.error_output: QMessageBox.critical( self, _("Error"), self.error_output, ) print("pylint error:\n\n" + self.error_output, file=sys.stderr) return filename = self.get_filename() rate, previous, results = self.parse_output(self.output) self._save_history() self.set_data(filename, (time.localtime(), rate, previous, results)) self.output = self.error_output + self.output self.show_data(justanalyzed=True) self.update_actions() self.stop_spinner() def _check_new_file(self): fname = self.get_filename() if fname != self.filename: self.filename = fname self.show_data() def _is_running(self): process = self._process return process is not None and process.state() == QProcess.Running def _kill_process(self): self._process.kill() self._process.waitForFinished() self.stop_spinner() def _update_combobox_history(self): """Change the number of files listed in the history combobox.""" max_entries = self.get_option("max_entries") if self.filecombo.count() > max_entries: num_elements = self.filecombo.count() diff = num_elements - max_entries for __ in range(diff): num_elements = self.filecombo.count() self.filecombo.removeItem(num_elements - 1) self.filecombo.selected() else: num_elements = self.filecombo.count() diff = max_entries - num_elements for i in range(num_elements, num_elements + diff): if i >= len(self.curr_filenames): break act_filename = self.curr_filenames[i] self.filecombo.insertItem(i, act_filename) def _save_history(self): """Save the current history filenames.""" if self.parent: list_save_files = [] for fname in self.curr_filenames: if _("untitled") not in fname: filename = osp.normpath(fname) list_save_files.append(fname) self.curr_filenames = list_save_files[:MAX_HISTORY_ENTRIES] self.set_option("history_filenames", self.curr_filenames) else: self.curr_filenames = [] # --- PluginMainWidget API # ------------------------------------------------------------------------ def get_title(self): return _("Code Analysis") def get_focus_widget(self): return self.treewidget def setup(self, options): change_history_depth_action = self.create_action( PylintWidgetActions.ChangeHistory, text=_("History..."), tip=_("Set history maximum entries"), icon=self.create_icon("history"), triggered=self.change_history_depth, ) self.code_analysis_action = self.create_action( PylintWidgetActions.RunCodeAnalysis, icon_text=_("Analyze"), text=_("Run code analysis"), tip=_("Run code analysis"), icon=self.create_icon("run"), triggered=lambda: self.sig_start_analysis_requested.emit(), context=Qt.ApplicationShortcut, register_shortcut=True) self.browse_action = self.create_action( PylintWidgetActions.BrowseFile, text=_("Select Python file"), tip=_("Select Python file"), icon=self.create_icon("fileopen"), triggered=self.select_file, ) self.log_action = self.create_action( PylintWidgetActions.ShowLog, text=_("Output"), icon_text=_("Output"), tip=_("Complete output"), icon=self.create_icon("log"), triggered=self.show_log, ) options_menu = self.get_options_menu() self.add_item_to_menu( self.treewidget.get_action(OneColumnTreeActions.CollapseAllAction), menu=options_menu, section=PylintWidgetOptionsMenuSections.Global, ) self.add_item_to_menu( self.treewidget.get_action(OneColumnTreeActions.ExpandAllAction), menu=options_menu, section=PylintWidgetOptionsMenuSections.Global, ) self.add_item_to_menu( self.treewidget.get_action( OneColumnTreeActions.CollapseSelectionAction), menu=options_menu, section=PylintWidgetOptionsMenuSections.Section, ) self.add_item_to_menu( self.treewidget.get_action( OneColumnTreeActions.ExpandSelectionAction), menu=options_menu, section=PylintWidgetOptionsMenuSections.Section, ) self.add_item_to_menu( change_history_depth_action, menu=options_menu, section=PylintWidgetOptionsMenuSections.History, ) # Update OneColumnTree contextual menu self.add_item_to_menu( change_history_depth_action, menu=self.treewidget.menu, section=PylintWidgetOptionsMenuSections.History, ) self.treewidget.restore_action.setVisible(False) toolbar = self.get_main_toolbar() for item in [ self.filecombo, self.browse_action, self.code_analysis_action ]: self.add_item_to_toolbar( item, toolbar, section=PylintWidgetMainToolbarSections.Main, ) secondary_toolbar = self.create_toolbar("secondary") for item in [ self.ratelabel, self.create_stretcher(), self.datelabel, self.create_stretcher(), self.log_action ]: self.add_item_to_toolbar( item, secondary_toolbar, section=PylintWidgetMainToolbarSections.Main, ) self.show_data() if self.rdata: self.remove_obsolete_items() self.filecombo.insertItems(0, self.get_filenames()) self.code_analysis_action.setEnabled(self.filecombo.is_valid()) else: self.code_analysis_action.setEnabled(False) # Signals self.filecombo.valid.connect(self.code_analysis_action.setEnabled) def on_option_update(self, option, value): if option == "max_entries": self._update_combobox_history() elif option == "history_filenames": self.curr_filenames = value self._update_combobox_history() def update_actions(self): fm = self.ratelabel.fontMetrics() toolbar = self.get_main_toolbar() width = max([fm.width(_("Stop")), fm.width(_("Analyze"))]) widget = toolbar.widgetForAction(self.code_analysis_action) if widget: widget.setMinimumWidth(width * 1.5) if self._is_running(): self.code_analysis_action.setIconText(_("Stop")) self.code_analysis_action.setIcon(self.create_icon("stop")) else: self.code_analysis_action.setIconText(_("Analyze")) self.code_analysis_action.setIcon(self.create_icon("run")) self.remove_obsolete_items() # --- Public API # ------------------------------------------------------------------------ @Slot() @Slot(int) def change_history_depth(self, value=None): """ Set history maximum entries. Parameters ---------- value: int or None, optional The valur to set the maximum history depth. If no value is provided, an input dialog will be launched. Default is None. """ if value is None: dialog = QInputDialog(self) # Set dialog properties dialog.setModal(False) dialog.setWindowTitle(_("History")) dialog.setLabelText(_("Maximum entries")) dialog.setInputMode(QInputDialog.IntInput) dialog.setIntRange(MIN_HISTORY_ENTRIES, MAX_HISTORY_ENTRIES) dialog.setIntStep(1) dialog.setIntValue(self.get_option("max_entries")) # Connect slot dialog.intValueSelected.connect( lambda value: self.set_option("max_entries", value)) dialog.show() else: self.set_option("max_entries", value) def get_filename(self): """ Get current filename in combobox. """ return str(self.filecombo.currentText()) @Slot(str) def set_filename(self, filename): """ Set current filename in combobox. """ if self._is_running(): self._kill_process() filename = str(filename) filename = osp.normpath(filename) # Normalize path for Windows # Don't try to reload saved analysis for filename, if filename # is the one currently displayed. # Fixes spyder-ide/spyder#13347 if self.get_filename() == filename: return index, _data = self.get_data(filename) if filename not in self.curr_filenames: self.filecombo.insertItem(0, filename) self.curr_filenames.insert(0, filename) self.filecombo.setCurrentIndex(0) else: try: index = self.filecombo.findText(filename) self.filecombo.removeItem(index) self.curr_filenames.pop(index) except IndexError: self.curr_filenames.remove(filename) self.filecombo.insertItem(0, filename) self.curr_filenames.insert(0, filename) self.filecombo.setCurrentIndex(0) num_elements = self.filecombo.count() if num_elements > self.get_option("max_entries"): self.filecombo.removeItem(num_elements - 1) self.filecombo.selected() def start_code_analysis(self, filename=None): """ Perform code analysis for given `filename`. If `filename` is None default to current filename in combobox. If this method is called while still running it will stop the code analysis. """ if self._is_running(): self._kill_process() else: if filename is not None: self.set_filename(filename) if self.filecombo.is_valid(): self._start() self.update_actions() def stop_code_analysis(self): """ Stop the code analysis process. """ if self._is_running(): self._kill_process() def remove_obsolete_items(self): """ Removing obsolete items. """ self.rdata = [(filename, data) for filename, data in self.rdata if is_module_or_package(filename)] def get_filenames(self): """ Return all filenames for which there is data available. """ return [filename for filename, _data in self.rdata] def get_data(self, filename): """ Get and load code analysis data for given `filename`. """ filename = osp.abspath(filename) for index, (fname, data) in enumerate(self.rdata): if fname == filename: return index, data else: return None, None def set_data(self, filename, data): """ Set and save code analysis `data` for given `filename`. """ filename = osp.abspath(filename) index, _data = self.get_data(filename) if index is not None: self.rdata.pop(index) self.rdata.insert(0, (filename, data)) while len(self.rdata) > self.get_option("max_entries"): self.rdata.pop(-1) with open(self.DATAPATH, "wb") as fh: pickle.dump([self.VERSION] + self.rdata, fh, 2) def show_data(self, justanalyzed=False): """ Show data in treewidget. """ text_color = MAIN_TEXT_COLOR prevrate_color = MAIN_PREVRATE_COLOR if not justanalyzed: self.output = None self.log_action.setEnabled(self.output is not None and len(self.output) > 0) if self._is_running(): self._kill_process() filename = self.get_filename() if not filename: return _index, data = self.get_data(filename) if data is None: text = _("Source code has not been rated yet.") self.treewidget.clear_results() date_text = "" else: datetime, rate, previous_rate, results = data if rate is None: text = _("Analysis did not succeed " "(see output for more details).") self.treewidget.clear_results() date_text = "" else: text_style = "<span style=\"color: %s\"><b>%s </b></span>" rate_style = "<span style=\"color: %s\"><b>%s</b></span>" prevrate_style = "<span style=\"color: %s\">%s</span>" color = DANGER_COLOR if float(rate) > 5.: color = SUCCESS_COLOR elif float(rate) > 3.: color = WARNING_COLOR text = _("Global evaluation:") text = ((text_style % (text_color, text)) + (rate_style % (color, ("%s/10" % rate)))) if previous_rate: text_prun = _("previous run:") text_prun = " (%s %s/10)" % (text_prun, previous_rate) text += prevrate_style % (prevrate_color, text_prun) self.treewidget.set_results(filename, results) date = time.strftime("%Y-%m-%d %H:%M:%S", datetime) date_text = text_style % (text_color, date) self.ratelabel.setText(text) self.datelabel.setText(date_text) @Slot() def show_log(self): """ Show output log dialog. """ if self.output: output_dialog = TextEditor(self.output, title=_("Code analysis output"), parent=self, readonly=True) output_dialog.resize(700, 500) output_dialog.exec_() # --- Python Specific # ------------------------------------------------------------------------ def get_pylintrc_path(self, filename): """ Get the path to the most proximate pylintrc config to the file. """ search_paths = [ # File"s directory osp.dirname(filename), # Working directory getcwd_or_home(), # Project directory self.get_option("project_dir"), # Home directory osp.expanduser("~"), ] return get_pylintrc_path(search_paths=search_paths) @Slot() def select_file(self, filename=None): """ Select filename using a open file dialog and set as current filename. If `filename` is provided, the dialog is not used. """ if filename is None: self.sig_redirect_stdio_requested.emit(False) filename, _selfilter = getopenfilename( self, _("Select Python file"), getcwd_or_home(), _("Python files") + " (*.py ; *.pyw)", ) self.sig_redirect_stdio_requested.emit(True) if filename: self.set_filename(filename) self.start_code_analysis() def get_command(self, filename): """ Return command to use to run code analysis on given filename """ command_args = [] if PYLINT_VER is not None: command_args = [ "-m", "pylint", "--output-format=text", "--msg-template=" '{msg_id}:{symbol}:{line:3d},{column}: {msg}"', ] pylintrc_path = self.get_pylintrc_path(filename=filename) if pylintrc_path is not None: command_args += ["--rcfile={}".format(pylintrc_path)] command_args.append(filename) return command_args def parse_output(self, output): """ Parse output and return current revious rate and results. """ # Convention, Refactor, Warning, Error results = {"C:": [], "R:": [], "W:": [], "E:": []} txt_module = "************* Module " module = "" # Should not be needed - just in case something goes wrong for line in output.splitlines(): if line.startswith(txt_module): # New module module = line[len(txt_module):] continue # Supporting option include-ids: ("R3873:" instead of "R:") if not re.match(r"^[CRWE]+([0-9]{4})?:", line): continue items = {} idx_0 = 0 idx_1 = 0 key_names = ["msg_id", "message_name", "line_nb", "message"] for key_idx, key_name in enumerate(key_names): if key_idx == len(key_names) - 1: idx_1 = len(line) else: idx_1 = line.find(":", idx_0) if idx_1 < 0: break item = line[(idx_0):idx_1] if not item: break if key_name == "line_nb": item = int(item.split(",")[0]) items[key_name] = item idx_0 = idx_1 + 1 else: pylint_item = (module, items["line_nb"], items["message"], items["msg_id"], items["message_name"]) results[line[0] + ":"].append(pylint_item) # Rate rate = None txt_rate = "Your code has been rated at " i_rate = output.find(txt_rate) if i_rate > 0: i_rate_end = output.find("/10", i_rate) if i_rate_end > 0: rate = output[i_rate + len(txt_rate):i_rate_end] # Previous run previous = "" if rate is not None: txt_prun = "previous run: " i_prun = output.find(txt_prun, i_rate_end) if i_prun > 0: i_prun_end = output.find("/10", i_prun) previous = output[i_prun + len(txt_prun):i_prun_end] return rate, previous, results
class TableWorkspaceDisplayView(QTableWidget): repaint_signal = Signal() def __init__(self, presenter, parent=None): super(TableWorkspaceDisplayView, self).__init__(parent) self.presenter = presenter self.COPY_ICON = mantidqt.icons.get_icon("fa.files-o") self.DELETE_ROW = mantidqt.icons.get_icon("fa.minus-square-o") self.STATISTICS_ON_ROW = mantidqt.icons.get_icon('fa.fighter-jet') self.GRAPH_ICON = mantidqt.icons.get_icon('fa.line-chart') self.TBD = mantidqt.icons.get_icon('fa.question') item_delegate = QStyledItemDelegate(self) item_delegate.setItemEditorFactory(PreciseDoubleFactory()) self.setItemDelegate(item_delegate) self.setAttribute(Qt.WA_DeleteOnClose, True) self.repaint_signal.connect(self._run_repaint) header = self.horizontalHeader() header.sectionDoubleClicked.connect(self.handle_double_click) def resizeEvent(self, event): QTableWidget.resizeEvent(self, event) header = self.horizontalHeader() header.setSectionResizeMode(QHeaderView.Interactive) def emit_repaint(self): self.repaint_signal.emit() @Slot() def _run_repaint(self): self.viewport().update() def handle_double_click(self, section): header = self.horizontalHeader() header.resizeSection(section, header.defaultSectionSize()) def keyPressEvent(self, event): if event.matches(QKeySequence.Copy): self.presenter.action_keypress_copy() return elif event.key() == Qt.Key_F2 or event.key() == Qt.Key_Return or event.key() == Qt.Key_Enter: self.edit(self.currentIndex()) return def set_context_menu_actions(self, table): """ Sets up the context menu actions for the table :type table: QTableView :param table: The table whose context menu actions will be set up. :param ws_read_function: The read function used to efficiently retrieve data directly from the workspace """ copy_action = QAction(self.COPY_ICON, "Copy", table) copy_action.triggered.connect(self.presenter.action_copy_cells) table.setContextMenuPolicy(Qt.ActionsContextMenu) table.addAction(copy_action) horizontalHeader = table.horizontalHeader() horizontalHeader.setContextMenuPolicy(Qt.CustomContextMenu) horizontalHeader.customContextMenuRequested.connect(self.custom_context_menu) verticalHeader = table.verticalHeader() verticalHeader.setContextMenuPolicy(Qt.ActionsContextMenu) verticalHeader.setSectionResizeMode(QHeaderView.Fixed) copy_spectrum_values = QAction(self.COPY_ICON, "Copy", verticalHeader) copy_spectrum_values.triggered.connect(self.presenter.action_copy_spectrum_values) delete_row = QAction(self.DELETE_ROW, "Delete Row", verticalHeader) delete_row.triggered.connect(self.presenter.action_delete_row) separator2 = self.make_separator(verticalHeader) verticalHeader.addAction(copy_spectrum_values) verticalHeader.addAction(separator2) verticalHeader.addAction(delete_row) def custom_context_menu(self, position): menu_main = QMenu() plot = QMenu("Plot...", menu_main) plot_line = QAction("Line", plot) plot_line.triggered.connect(partial(self.presenter.action_plot, PlotType.LINEAR)) plot_line_with_yerr = QAction("Line with Y Errors", plot) plot_line_with_yerr.triggered.connect(partial(self.presenter.action_plot, PlotType.LINEAR_WITH_ERR)) plot_scatter = QAction("Scatter", plot) plot_scatter.triggered.connect(partial(self.presenter.action_plot, PlotType.SCATTER)) plot_line_and_points = QAction("Line + Symbol", plot) plot_line_and_points.triggered.connect(partial(self.presenter.action_plot, PlotType.LINE_AND_SYMBOL)) plot.addAction(plot_line) plot.addAction(plot_line_with_yerr) plot.addAction(plot_scatter) plot.addAction(plot_line_and_points) menu_main.addMenu(plot) copy_bin_values = QAction(self.COPY_ICON, "Copy", menu_main) copy_bin_values.triggered.connect(self.presenter.action_copy_bin_values) set_as_x = QAction("Set as X", menu_main) set_as_x.triggered.connect(self.presenter.action_set_as_x) set_as_y = QAction("Set as Y", menu_main) set_as_y.triggered.connect(self.presenter.action_set_as_y) set_as_none = QAction("Set as None", menu_main) set_as_none.triggered.connect(self.presenter.action_set_as_none) statistics_on_columns = QAction("Statistics on Columns", menu_main) statistics_on_columns.triggered.connect(self.presenter.action_statistics_on_columns) hide_selected = QAction("Hide Selected", menu_main) hide_selected.triggered.connect(self.presenter.action_hide_selected) show_all_columns = QAction("Show All Columns", menu_main) show_all_columns.triggered.connect(self.presenter.action_show_all_columns) sort_ascending = QAction("Sort Ascending", menu_main) sort_ascending.triggered.connect(partial(self.presenter.action_sort, True)) sort_descending = QAction("Sort Descending", menu_main) sort_descending.triggered.connect(partial(self.presenter.action_sort, False)) menu_main.addAction(copy_bin_values) menu_main.addAction(self.make_separator(menu_main)) menu_main.addAction(set_as_x) menu_main.addAction(set_as_y) marked_y_cols = self.presenter.get_columns_marked_as_y() num_y_cols = len(marked_y_cols) # If any columns are marked as Y then generate the set error menu if num_y_cols > 0: menu_set_as_y_err = QMenu("Set error for Y...") for label_index in range(num_y_cols): set_as_y_err = QAction("Y{}".format(label_index), menu_main) # This is the column index of the Y column for which a YERR column is being added. # The column index is relative to the whole table, this is necessary # so that later the data of the column marked as error can be retrieved related_y_column = marked_y_cols[label_index] # label_index here holds the index in the LABEL (multiple Y columns have labels Y0, Y1, YN...) # this is NOT the same as the column relative to the WHOLE table set_as_y_err.triggered.connect( partial(self.presenter.action_set_as_y_err, related_y_column, label_index)) menu_set_as_y_err.addAction(set_as_y_err) menu_main.addMenu(menu_set_as_y_err) menu_main.addAction(set_as_none) menu_main.addAction(self.make_separator(menu_main)) menu_main.addAction(statistics_on_columns) menu_main.addAction(self.make_separator(menu_main)) menu_main.addAction(hide_selected) menu_main.addAction(show_all_columns) menu_main.addAction(self.make_separator(menu_main)) menu_main.addAction(sort_ascending) menu_main.addAction(sort_descending) menu_main.exec_(self.mapToGlobal(position)) def make_separator(self, horizontalHeader): separator1 = QAction(horizontalHeader) separator1.setSeparator(True) return separator1 @staticmethod def copy_to_clipboard(data): """ Uses the QGuiApplication to copy to the system clipboard. :type data: str :param data: The data that will be copied to the clipboard :return: """ cb = QtGui.QGuiApplication.clipboard() cb.setText(data, mode=cb.Clipboard) def ask_confirmation(self, message, title="Mantid Workbench"): """ :param message: :return: """ reply = QMessageBox.question(self, title, message, QMessageBox.Yes, QMessageBox.No) return True if reply == QMessageBox.Yes else False def show_warning(self, message, title="Mantid Workbench"): QMessageBox.warning(self, title, message)
class ResultsTree(OneColumnTree): sig_edit_goto_requested = Signal(str, int, str) """ This signal will request to open a file in a given row and column using a code editor. Parameters ---------- path: str Path to file. row: int Cursor starting row position. word: str Word to select on given row. """ def __init__(self, parent): super().__init__(parent) self.filename = None self.results = None self.data = None self.set_title("") def activated(self, item): """Double-click event""" data = self.data.get(id(item)) if data is not None: fname, lineno = data self.sig_edit_goto_requested.emit(fname, lineno, "") def clicked(self, item): """Click event""" self.activated(item) def clear_results(self): self.clear() self.set_title("") def set_results(self, filename, results): self.filename = filename self.results = results self.refresh() def refresh(self): title = _("Results for ") + self.filename self.set_title(title) self.clear() self.data = {} # Populating tree results = ( (_("Convention"), ima.icon("convention"), self.results["C:"]), (_("Refactor"), ima.icon("refactor"), self.results["R:"]), (_("Warning"), ima.icon("warning"), self.results["W:"]), (_("Error"), ima.icon("error"), self.results["E:"]), ) for title, icon, messages in results: title += " (%d message%s)" % (len(messages), "s" if len(messages) > 1 else "") title_item = QTreeWidgetItem(self, [title], QTreeWidgetItem.Type) title_item.setIcon(0, icon) if not messages: title_item.setDisabled(True) modules = {} for message_data in messages: # If message data is legacy version without message_name if len(message_data) == 4: message_data = tuple(list(message_data) + [None]) module, lineno, message, msg_id, message_name = message_data basename = osp.splitext(osp.basename(self.filename))[0] if not module.startswith(basename): # Pylint bug i_base = module.find(basename) module = module[i_base:] dirname = osp.dirname(self.filename) if module.startswith(".") or module == basename: modname = osp.join(dirname, module) else: modname = osp.join(dirname, *module.split(".")) if osp.isdir(modname): modname = osp.join(modname, "__init__") for ext in (".py", ".pyw"): if osp.isfile(modname + ext): modname = modname + ext break if osp.isdir(self.filename): parent = modules.get(modname) if parent is None: item = QTreeWidgetItem(title_item, [module], QTreeWidgetItem.Type) item.setIcon(0, ima.icon("python")) modules[modname] = item parent = item else: parent = title_item if len(msg_id) > 1: if not message_name: message_string = "{msg_id} " else: message_string = "{msg_id} ({message_name}) " message_string += "line {lineno}: {message}" message_string = message_string.format( msg_id=msg_id, message_name=message_name, lineno=lineno, message=message) msg_item = QTreeWidgetItem(parent, [message_string], QTreeWidgetItem.Type) msg_item.setIcon(0, ima.icon("arrow")) self.data[id(msg_item)] = (modname, lineno)
class ShellWidget(NamepaceBrowserWidget, HelpWidget, DebuggingWidget, FigureBrowserWidget): """ Shell widget for the IPython Console This is the widget in charge of executing code """ # NOTE: Signals can't be assigned separately to each widget # That's why we define all needed signals here. # For NamepaceBrowserWidget sig_show_syspath = Signal(object) sig_show_env = Signal(object) # For FigureBrowserWidget sig_new_inline_figure = Signal(object, str) # For DebuggingWidget sig_pdb_step = Signal(str, int) sig_pdb_state_changed = Signal(bool, dict) sig_pdb_prompt_ready = Signal() # For ShellWidget sig_focus_changed = Signal() new_client = Signal() sig_is_spykernel = Signal(object) sig_kernel_restarted_message = Signal(str) sig_kernel_restarted = Signal() sig_prompt_ready = Signal() sig_remote_execute = Signal() # For global working directory sig_working_directory_changed = Signal(str) # For printing internal errors sig_exception_occurred = Signal(dict) def __init__(self, ipyclient, additional_options, interpreter_versions, external_kernel, *args, **kw): # To override the Qt widget used by RichJupyterWidget self.custom_control = ControlWidget self.custom_page_control = PageControlWidget self.custom_edit = True self.spyder_kernel_comm = KernelComm() self.spyder_kernel_comm.sig_exception_occurred.connect( self.sig_exception_occurred) super(ShellWidget, self).__init__(*args, **kw) self.ipyclient = ipyclient self.additional_options = additional_options self.interpreter_versions = interpreter_versions self.external_kernel = external_kernel self._cwd = '' # Keyboard shortcuts self.shortcuts = self.create_shortcuts() # Set the color of the matched parentheses here since the qtconsole # uses a hard-coded value that is not modified when the color scheme is # set in the qtconsole constructor. See spyder-ide/spyder#4806. self.set_bracket_matcher_color_scheme(self.syntax_style) self.shutdown_called = False self.kernel_manager = None self.kernel_client = None self.shutdown_thread = None handlers = { 'pdb_state': self.set_pdb_state, 'pdb_execute': self.pdb_execute, 'get_pdb_settings': self.get_pdb_settings, 'run_cell': self.handle_run_cell, 'cell_count': self.handle_cell_count, 'current_filename': self.handle_current_filename, 'get_file_code': self.handle_get_file_code, 'set_debug_state': self.set_debug_state, 'update_syspath': self.update_syspath, 'do_where': self.do_where, 'pdb_input': self.pdb_input, 'request_interrupt_eventloop': self.request_interrupt_eventloop, } for request_id in handlers: self.spyder_kernel_comm.register_call_handler( request_id, handlers[request_id]) self._execute_queue = [] self.executed.connect(self.pop_execute_queue) # Internal kernel are always spyder kernels self._is_spyder_kernel = not external_kernel # Show a message in our installers to explain users how to use # modules that don't come with them. self.show_modules_message = is_pynsist() or running_in_mac_app() def __del__(self): """Avoid destroying shutdown_thread.""" if (self.shutdown_thread is not None and self.shutdown_thread.isRunning()): self.shutdown_thread.wait() # ---- Public API --------------------------------------------------------- def is_spyder_kernel(self): """Is the widget a spyder kernel.""" return self._is_spyder_kernel def shutdown(self): """Shutdown kernel""" self.shutdown_called = True self.spyder_kernel_comm.close() self.kernel_manager.stop_restarter() self.shutdown_thread = QThread() self.shutdown_thread.run = self.kernel_manager.shutdown_kernel if self.kernel_client is not None: self.shutdown_thread.finished.connect( self.kernel_client.stop_channels) self.shutdown_thread.start() super(ShellWidget, self).shutdown() def will_close(self, externally_managed): """ Close communication channels with the kernel if shutdown was not called. If the kernel is not externally managed, shutdown the kernel as well. """ if not self.shutdown_called and not externally_managed: # Make sure the channels are stopped self.spyder_kernel_comm.close() self.kernel_manager.stop_restarter() self.kernel_manager.shutdown_kernel(now=True) if self.kernel_client is not None: self.kernel_client.stop_channels() if externally_managed: self.spyder_kernel_comm.close() if self.kernel_client is not None: self.kernel_client.stop_channels() super(ShellWidget, self).will_close(externally_managed) def call_kernel(self, interrupt=False, blocking=False, callback=None, timeout=None, display_error=False): """ Send message to Spyder kernel connected to this console. Parameters ---------- interrupt: bool Interrupt the kernel while running or in Pdb to perform the call. blocking: bool Make a blocking call, i.e. wait on this side until the kernel sends its response. callback: callable Callable to process the response sent from the kernel on the Spyder side. timeout: int or None Maximum time (in seconds) before giving up when making a blocking call to the kernel. If None, a default timeout (defined in commbase.py, present in spyder-kernels) is used. display_error: bool If an error occurs, should it be printed to the console. """ return self.spyder_kernel_comm.remote_call( interrupt=interrupt, blocking=blocking, callback=callback, timeout=timeout, display_error=display_error ) def set_kernel_client_and_manager(self, kernel_client, kernel_manager): """Set the kernel client and manager""" self.kernel_manager = kernel_manager self.kernel_client = kernel_client self.spyder_kernel_comm.open_comm(kernel_client) # Redefine the complete method to work while debugging. self._redefine_complete_for_dbg(self.kernel_client) def pop_execute_queue(self): """Pop one waiting instruction.""" if self._execute_queue: self.execute(*self._execute_queue.pop(0)) # ---- Public API --------------------------------------------------------- def interrupt_kernel(self): """Attempts to interrupt the running kernel.""" # Empty queue when interrupting # Fixes spyder-ide/spyder#7293. self._execute_queue = [] super(ShellWidget, self).interrupt_kernel() def execute(self, source=None, hidden=False, interactive=False): """ Executes source or the input buffer, possibly prompting for more input. """ if self._executing: self._execute_queue.append((source, hidden, interactive)) return super(ShellWidget, self).execute(source, hidden, interactive) def request_interrupt_eventloop(self): """Send a message to the kernel to interrupt the eventloop.""" self.call_kernel()._interrupt_eventloop() def set_exit_callback(self): """Set exit callback for this shell.""" self.exit_requested.connect(self.ipyclient.exit_callback) def is_running(self): if self.kernel_client is not None and \ self.kernel_client.channels_running: return True else: return False def check_spyder_kernel(self): """Determine if the kernel is from Spyder.""" code = u"getattr(get_ipython().kernel, 'set_value', False)" if self._reading: return else: self.silent_exec_method(code) def set_cwd(self, dirname): """Set shell current working directory.""" if os.name == 'nt': # Use normpath instead of replacing '\' with '\\' # See spyder-ide/spyder#10785 dirname = osp.normpath(dirname) if self.ipyclient.hostname is None: self.call_kernel(interrupt=self.is_debugging()).set_cwd(dirname) self._cwd = dirname def update_cwd(self): """Update current working directory in the kernel.""" if self.kernel_client is None: return self.call_kernel(callback=self.remote_set_cwd).get_cwd() def remote_set_cwd(self, cwd): """Get current working directory from kernel.""" self._cwd = cwd self.sig_working_directory_changed.emit(self._cwd) def set_bracket_matcher_color_scheme(self, color_scheme): """Set color scheme for matched parentheses.""" bsh = sh.BaseSH(parent=self, color_scheme=color_scheme) mpcolor = bsh.get_matched_p_color() self._bracket_matcher.format.setBackground(mpcolor) def set_color_scheme(self, color_scheme, reset=True): """Set color scheme of the shell.""" self.set_bracket_matcher_color_scheme(color_scheme) self.style_sheet, dark_color = create_qss_style(color_scheme) self.syntax_style = color_scheme self._style_sheet_changed() self._syntax_style_changed() if reset: self.reset(clear=True) if not dark_color: # Needed to change the colors of tracebacks self.silent_execute("%colors linux") self.call_kernel().set_sympy_forecolor(background_color='dark') else: self.silent_execute("%colors lightbg") self.call_kernel().set_sympy_forecolor(background_color='light') def update_syspath(self, path_dict, new_path_dict): """Update sys.path contents on kernel.""" self.call_kernel( interrupt=True, blocking=False).update_syspath(path_dict, new_path_dict) def request_syspath(self): """Ask the kernel for sys.path contents.""" self.call_kernel( interrupt=True, callback=self.sig_show_syspath.emit).get_syspath() def request_env(self): """Ask the kernel for environment variables.""" self.call_kernel( interrupt=True, callback=self.sig_show_env.emit).get_env() def set_show_calltips(self, show_calltips): """Enable/Disable showing calltips.""" self.enable_calltips = show_calltips def set_buffer_size(self, buffer_size): """Set buffer size for the shell.""" self.buffer_size = buffer_size def set_completion_type(self, completion_type): """Set completion type (Graphical, Terminal, Plain) for the shell.""" self.gui_completion = completion_type def set_in_prompt(self, in_prompt): """Set appereance of the In prompt.""" self.in_prompt = in_prompt def set_out_prompt(self, out_prompt): """Set appereance of the Out prompt.""" self.out_prompt = out_prompt def get_matplotlib_backend(self): """Call kernel to get current backend.""" return self.call_kernel( interrupt=True, blocking=True).get_matplotlib_backend() def set_matplotlib_backend(self, backend_option, pylab=False): """Set matplotlib backend given a backend name.""" cmd = "get_ipython().kernel.set_matplotlib_backend('{}', {})" self.execute(cmd.format(backend_option, pylab), hidden=True) def set_mpl_inline_figure_format(self, figure_format): """Set matplotlib inline figure format.""" cmd = "get_ipython().kernel.set_mpl_inline_figure_format('{}')" self.execute(cmd.format(figure_format), hidden=True) def set_mpl_inline_resolution(self, resolution): """Set matplotlib inline resolution (savefig.dpi/figure.dpi).""" cmd = "get_ipython().kernel.set_mpl_inline_resolution({})" self.execute(cmd.format(resolution), hidden=True) def set_mpl_inline_figure_size(self, width, height): """Set matplotlib inline resolution (savefig.dpi/figure.dpi).""" cmd = "get_ipython().kernel.set_mpl_inline_figure_size({}, {})" self.execute(cmd.format(width, height), hidden=True) def set_mpl_inline_bbox_inches(self, bbox_inches): """Set matplotlib inline print figure bbox_inches ('tight' or not).""" cmd = "get_ipython().kernel.set_mpl_inline_bbox_inches({})" self.execute(cmd.format(bbox_inches), hidden=True) def set_jedi_completer(self, use_jedi): """Set if jedi completions should be used.""" cmd = "get_ipython().kernel.set_jedi_completer({})" self.execute(cmd.format(use_jedi), hidden=True) def set_greedy_completer(self, use_greedy): """Set if greedy completions should be used.""" cmd = "get_ipython().kernel.set_greedy_completer({})" self.execute(cmd.format(use_greedy), hidden=True) def set_autocall(self, autocall): """Set if autocall functionality is enabled or not.""" cmd = "get_ipython().kernel.set_autocall({})" self.execute(cmd.format(autocall), hidden=True) # --- To handle the banner def long_banner(self): """Banner for clients with additional content.""" # Default banner py_ver = self.interpreter_versions['python_version'].split('\n')[0] ipy_ver = self.interpreter_versions['ipython_version'] banner_parts = [ 'Python %s\n' % py_ver, 'Type "copyright", "credits" or "license" for more information.\n\n', 'IPython %s -- An enhanced Interactive Python.\n' % ipy_ver ] banner = ''.join(banner_parts) # Pylab additions pylab_o = self.additional_options['pylab'] autoload_pylab_o = self.additional_options['autoload_pylab'] if pylab_o and autoload_pylab_o: pylab_message = ("\nPopulating the interactive namespace from " "numpy and matplotlib\n") banner = banner + pylab_message # Sympy additions sympy_o = self.additional_options['sympy'] 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 if (pylab_o and sympy_o): lines = """ Warning: pylab (numpy and matplotlib) and symbolic math (sympy) are both enabled at the same time. Some pylab functions are going to be overrided by the sympy module (e.g. plot) """ banner = banner + lines return banner def short_banner(self): """Short banner with Python and IPython versions only.""" py_ver = self.interpreter_versions['python_version'].split(' ')[0] ipy_ver = self.interpreter_versions['ipython_version'] banner = 'Python %s -- IPython %s' % (py_ver, ipy_ver) return banner # --- To define additional shortcuts def clear_console(self): self.execute("%clear") # Stop reading as any input has been removed. self._reading = False def _reset_namespace(self): warning = self.get_conf('show_reset_namespace_warning') self.reset_namespace(warning=warning) def reset_namespace(self, warning=False, message=False): """Reset the namespace by removing all names defined by the user.""" reset_str = _("Remove all variables") warn_str = _("All user-defined variables will be removed. " "Are you sure you want to proceed?") # Don't show the warning when running our tests. if running_under_pytest(): warning = False # This is necessary to make resetting variables work in external # kernels. # See spyder-ide/spyder#9505. try: kernel_env = self.kernel_manager._kernel_spec.env except AttributeError: kernel_env = {} if warning: box = MessageCheckBox(icon=QMessageBox.Warning, parent=self) box.setWindowTitle(reset_str) box.set_checkbox_text(_("Don't show again.")) box.setStandardButtons(QMessageBox.Yes | QMessageBox.No) box.setDefaultButton(QMessageBox.Yes) box.set_checked(False) box.set_check_visible(True) box.setText(warn_str) answer = box.show() # Update checkbox based on user interaction self.set_conf( 'show_reset_namespace_warning', not box.is_checked()) self.ipyclient.reset_warning = not box.is_checked() if answer != QMessageBox.Yes: return try: if self.is_waiting_pdb_input(): self.execute('%reset -f') else: if message: self.reset() self._append_html( _("<br><br>Removing all variables...<br>"), before_prompt=False ) self.insert_horizontal_ruler() self.silent_execute("%reset -f") if kernel_env.get('SPY_AUTOLOAD_PYLAB_O') == 'True': self.silent_execute("from pylab import *") if kernel_env.get('SPY_SYMPY_O') == 'True': sympy_init = """ 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) init_printing()""" self.silent_execute(dedent(sympy_init)) if kernel_env.get('SPY_RUN_CYTHON') == 'True': self.silent_execute("%reload_ext Cython") # This doesn't need to interrupt the kernel because # "%reset -f" is being executed before it. # Fixes spyder-ide/spyder#12689 self.refresh_namespacebrowser(interrupt=False) if not self.external_kernel: self.call_kernel().close_all_mpl_figures() except AttributeError: pass def create_shortcuts(self): """Create shortcuts for ipyconsole.""" inspect = self.config_shortcut( self._control.inspect_current_object, context='Console', name='Inspect current object', parent=self) clear_console = self.config_shortcut( self.clear_console, context='Console', name='Clear shell', parent=self) restart_kernel = self.config_shortcut( self.ipyclient.restart_kernel, context='ipython_console', name='Restart kernel', parent=self) new_tab = self.config_shortcut( lambda: self.new_client.emit(), context='ipython_console', name='new tab', parent=self) reset_namespace = self.config_shortcut( lambda: self._reset_namespace(), context='ipython_console', name='reset namespace', parent=self) array_inline = self.config_shortcut( self._control.enter_array_inline, context='array_builder', name='enter array inline', parent=self) array_table = self.config_shortcut( self._control.enter_array_table, context='array_builder', name='enter array table', parent=self) clear_line = self.config_shortcut( self.ipyclient.clear_line, context='console', name='clear line', parent=self) return [inspect, clear_console, restart_kernel, new_tab, reset_namespace, array_inline, array_table, clear_line] # --- To communicate with the kernel def silent_execute(self, code): """Execute code in the kernel without increasing the prompt""" try: if self.is_debugging(): self.pdb_execute(code, hidden=True) else: self.kernel_client.execute(to_text_string(code), silent=True) except AttributeError: pass def silent_exec_method(self, code): """Silently execute a kernel method and save its reply The methods passed here **don't** involve getting the value of a variable but instead replies that can be handled by ast.literal_eval. To get a value see `get_value` Parameters ---------- code : string Code that contains the kernel method as part of its string See Also -------- handle_exec_method : Method that deals with the reply Note ---- This is based on the _silent_exec_callback method of RichJupyterWidget. Therefore this is licensed BSD """ # Generate uuid, which would be used as an indication of whether or # not the unique request originated from here local_uuid = to_text_string(uuid.uuid1()) code = to_text_string(code) if self.kernel_client is None: return msg_id = self.kernel_client.execute('', silent=True, user_expressions={ local_uuid:code }) self._kernel_methods[local_uuid] = code self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'silent_exec_method') def handle_exec_method(self, msg): """ Handle data returned by silent executions of kernel methods This is based on the _handle_exec_callback of RichJupyterWidget. Therefore this is licensed BSD. """ user_exp = msg['content'].get('user_expressions') if not user_exp: return for expression in user_exp: if expression in self._kernel_methods: # Process kernel reply method = self._kernel_methods[expression] reply = user_exp[expression] data = reply.get('data') if 'getattr' in method: if data is not None and 'text/plain' in data: is_spyder_kernel = data['text/plain'] if 'SpyderKernel' in is_spyder_kernel: self._is_spyder_kernel = True self.sig_is_spykernel.emit(self) # Remove method after being processed self._kernel_methods.pop(expression) def set_backend_for_mayavi(self, command): """ Mayavi plots require the Qt backend, so we try to detect if one is generated to change backends """ 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._append_plain_text(message + '\n') self.silent_execute("%gui inline\n%gui qt") def change_mpl_backend(self, command): """ If the user is trying to change Matplotlib backends with %matplotlib, send the same command again to the kernel to correctly change it. Fixes spyder-ide/spyder#4002. """ if (command.startswith('%matplotlib') and len(command.splitlines()) == 1): if not 'inline' in command: self.silent_execute(command) def append_html_message(self, html, before_prompt=False, msg_type='warning'): """ Append an html message enclosed in a box. Parameters ---------- before_prompt: bool Whether to add the message before the next prompt. msg_type: str Type of message to be showm. Possible values are 'warning' and 'error'. """ # The message is displayed in a table with a single cell. table_properties = ( "border='0.5'" + "width='90%'" + "cellpadding='8'" + "cellspacing='0'" ) if msg_type == 'error': header = _("Error") bgcolor = SpyderPalette.COLOR_ERROR_2 else: header = _("Warning") bgcolor = SpyderPalette.COLOR_WARN_1 self._append_html( f"<div align='center'><table {table_properties}>" + f"<tr><th bgcolor='{bgcolor}'>{header}</th></tr>" + "<tr><td>" + html + "</td></tr>" + "</table></div>", before_prompt=before_prompt ) def insert_horizontal_ruler(self): """ Insert a horizontal ruler at the current cursor position. Notes ----- This only works when adding a single horizontal line to a message. For more complex messages, please use append_html_message. """ self._control.insert_horizontal_ruler() # ---- Spyder-kernels methods --------------------------------------------- def get_editor(self, filename): """Get editor for filename and set it as the current editor.""" editorstack = self.get_editorstack() if editorstack is None: return None if not filename: return None index = editorstack.has_filename(filename) if index is None: return None return editorstack.data[index].editor def get_editorstack(self): """Get the current editorstack.""" plugin = self.ipyclient.plugin if plugin.main.editor is not None: editor = plugin.main.editor return editor.get_current_editorstack() raise RuntimeError('No editorstack found.') def handle_get_file_code(self, filename, save_all=True): """ Return the bytes that compose the file. Bytes are returned instead of str to support non utf-8 files. """ editorstack = self.get_editorstack() if save_all and self.get_conf( 'save_all_before_run', default=True, section='editor'): editorstack.save_all(save_new_files=False) editor = self.get_editor(filename) if editor is None: # Load it from file instead text, _enc = encoding.read(filename) return text return editor.toPlainText() def handle_run_cell(self, cell_name, filename): """ Get cell code from cell name and file name. """ editorstack = self.get_editorstack() editor = self.get_editor(filename) if editor is None: raise RuntimeError( "File {} not open in the editor".format(filename)) editorstack.last_cell_call = (filename, cell_name) # The file is open, load code from editor return editor.get_cell_code(cell_name) def handle_cell_count(self, filename): """Get number of cells in file to loop.""" editor = self.get_editor(filename) if editor is None: raise RuntimeError( "File {} not open in the editor".format(filename)) # The file is open, get cell count from editor return editor.get_cell_count() def handle_current_filename(self): """Get the current filename.""" return self.get_editorstack().get_current_finfo().filename # ---- Public methods (overrode by us) ------------------------------------ def request_restart_kernel(self): """Reimplemented to call our own restart mechanism.""" self.ipyclient.restart_kernel() # ---- Private methods (overrode by us) ----------------------------------- def _handle_error(self, msg): """ Reimplemented to reset the prompt if the error comes after the reply """ self._process_execute_error(msg) def _context_menu_make(self, pos): """Reimplement the IPython context menu""" menu = super(ShellWidget, 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 """ # Don't change banner for external kernels if self.external_kernel: return '' show_banner_o = self.additional_options['show_banner'] if show_banner_o: return self.long_banner() else: return self.short_banner() def _kernel_restarted_message(self, died=True): msg = _("Kernel died, restarting") if died else _("Kernel restarting") self.sig_kernel_restarted_message.emit(msg) def _handle_kernel_restarted(self, *args, **kwargs): super(ShellWidget, self)._handle_kernel_restarted(*args, **kwargs) self.sig_kernel_restarted.emit() def _syntax_style_changed(self): """Refresh the highlighting with the current syntax style by class.""" if self._highlighter is None: # ignore premature calls return if self.syntax_style: self._highlighter._style = create_style_class(self.syntax_style) self._highlighter._clear_caches() else: self._highlighter.set_style_sheet(self.style_sheet) def _prompt_started_hook(self): """Emit a signal when the prompt is ready.""" if not self._reading: self._highlighter.highlighting_on = True self.sig_prompt_ready.emit() def _handle_execute_input(self, msg): """Handle an execute_input message""" super(ShellWidget, self)._handle_execute_input(msg) self.sig_remote_execute.emit() def _process_execute_error(self, msg): """ Display a message when using our installers to explain users how to use modules that doesn't come with them. """ super(ShellWidget, self)._process_execute_error(msg) if self.show_modules_message: error = msg['content']['traceback'] if any(['ModuleNotFoundError' in frame or 'ImportError' in frame for frame in error]): self.append_html_message( _("It seems you're trying to use a module that doesn't " "come with our installer. Check " "<a href='{}'>this FAQ</a> in our docs to learn how " "to do this.").format(MODULES_FAQ_URL), before_prompt=True ) self.show_modules_message = False #---- Qt methods ---------------------------------------------------------- def focusInEvent(self, event): """Reimplement Qt method to send focus change notification""" self.sig_focus_changed.emit() return super(ShellWidget, self).focusInEvent(event) def focusOutEvent(self, event): """Reimplement Qt method to send focus change notification""" self.sig_focus_changed.emit() return super(ShellWidget, self).focusOutEvent(event)
class SearchThread(QThread): """Find in files search thread""" sig_finished = Signal(bool) def __init__(self, parent): QThread.__init__(self, parent) self.mutex = QMutex() self.stopped = None self.results = None self.pathlist = None self.nb = None self.error_flag = None self.rootpath = None self.python_path = None self.hg_manifest = None self.include = None self.exclude = None self.texts = None self.text_re = None self.completed = None self.get_pythonpath_callback = None def initialize(self, path, python_path, hg_manifest, include, exclude, texts, text_re): self.rootpath = path self.python_path = python_path self.hg_manifest = hg_manifest self.include = include self.exclude = exclude self.texts = texts self.text_re = text_re self.stopped = False self.completed = False def run(self): try: self.filenames = [] if self.hg_manifest: ok = self.find_files_in_hg_manifest() elif self.python_path: ok = self.find_files_in_python_path() else: ok = self.find_files_in_path(self.rootpath) if ok: self.find_string_in_files() except Exception: # Important note: we have to handle unexpected exceptions by # ourselves because they won't be catched by the main thread # (known QThread limitation/bug) traceback.print_exc() self.error_flag = _("Unexpected error: see internal console") self.stop() self.sig_finished.emit(self.completed) def stop(self): with QMutexLocker(self.mutex): self.stopped = True def find_files_in_python_path(self): pathlist = os.environ.get('PYTHONPATH', '').split(os.pathsep) if self.get_pythonpath_callback is not None: pathlist += self.get_pythonpath_callback() if os.name == "nt": # The following avoid doublons on Windows platforms: # (e.g. "d:\Python" in PYTHONPATH environment variable, # and "D:\Python" in Spyder's python path would lead # to two different search folders) winpathlist = [] lcpathlist = [] for path in pathlist: lcpath = osp.normcase(path) if lcpath not in lcpathlist: lcpathlist.append(lcpath) winpathlist.append(path) pathlist = winpathlist ok = True for path in set(pathlist): if osp.isdir(path): ok = self.find_files_in_path(path) if not ok: break return ok def find_files_in_hg_manifest(self): p = programs.run_shell_command('hg manifest', cwd=self.rootpath) hgroot = get_vcs_root(self.rootpath) self.pathlist = [hgroot] for path in p.stdout.read().decode().splitlines(): with QMutexLocker(self.mutex): if self.stopped: return False dirname = osp.dirname(path) try: if re.search(self.exclude, dirname+os.sep): continue filename = osp.basename(path) if re.search(self.exclude, filename): continue if re.search(self.include, filename): self.filenames.append(osp.join(hgroot, path)) except re.error: self.error_flag = _("invalid regular expression") return False return True def find_files_in_path(self, path): if self.pathlist is None: self.pathlist = [] self.pathlist.append(path) for path, dirs, files in os.walk(path): with QMutexLocker(self.mutex): if self.stopped: return False try: for d in dirs[:]: dirname = os.path.join(path, d) if re.search(self.exclude, dirname+os.sep): dirs.remove(d) for f in files: filename = os.path.join(path, f) if re.search(self.exclude, filename): continue if re.search(self.include, filename): self.filenames.append(filename) except re.error: self.error_flag = _("invalid regular expression") return False return True def find_string_in_files(self): self.results = {} self.nb = 0 self.error_flag = False for fname in self.filenames: with QMutexLocker(self.mutex): if self.stopped: return try: for lineno, line in enumerate(open(fname, 'rb')): for text, enc in self.texts: if self.text_re: found = re.search(text, line) if found is not None: break else: found = line.find(text) if found > -1: break try: line_dec = line.decode(enc) except UnicodeDecodeError: line_dec = line if self.text_re: for match in re.finditer(text, line): res = self.results.get(osp.abspath(fname), []) res.append((lineno+1, match.start(), line_dec)) self.results[osp.abspath(fname)] = res self.nb += 1 else: while found > -1: res = self.results.get(osp.abspath(fname), []) res.append((lineno+1, found, line_dec)) self.results[osp.abspath(fname)] = res for text, enc in self.texts: found = line.find(text, found+1) if found > -1: break self.nb += 1 except IOError as xxx_todo_changeme: (_errno, _strerror) = xxx_todo_changeme.args self.error_flag = _("permission denied errors were encountered") except re.error: self.error_flag = _("invalid regular expression") self.completed = True def get_results(self): return self.results, self.pathlist, self.nb, self.error_flag
class FindOptions(QWidget): """Find widget with options""" REGEX_INVALID = "background-color:rgb(255, 175, 90);" find = Signal() stop = Signal() redirect_stdio = Signal(bool) def __init__(self, parent, search_text, search_text_regexp, search_path, exclude, exclude_idx, exclude_regexp, supported_encodings, in_python_path, more_options, case_sensitive, external_path_history, options_button=None): QWidget.__init__(self, parent) if search_path is None: search_path = getcwd_or_home() if not isinstance(search_text, (list, tuple)): search_text = [search_text] if not isinstance(search_path, (list, tuple)): search_path = [search_path] if not isinstance(exclude, (list, tuple)): exclude = [exclude] if not isinstance(external_path_history, (list, tuple)): external_path_history = [external_path_history] self.supported_encodings = supported_encodings # Layout 1 hlayout1 = QHBoxLayout() self.search_text = PatternComboBox(self, search_text, _("Search pattern")) self.edit_regexp = create_toolbutton(self, icon=ima.icon('advanced'), tip=_('Regular expression')) self.case_button = create_toolbutton(self, icon=get_icon("upper_lower.png"), tip=_("Case Sensitive")) self.case_button.setCheckable(True) self.case_button.setChecked(case_sensitive) self.edit_regexp.setCheckable(True) self.edit_regexp.setChecked(search_text_regexp) self.more_widgets = () self.more_options = create_toolbutton(self, toggled=self.toggle_more_options) self.more_options.setCheckable(True) self.more_options.setChecked(more_options) self.ok_button = create_toolbutton(self, text=_("Search"), icon=ima.icon('find'), triggered=lambda: self.find.emit(), tip=_("Start search"), text_beside_icon=True) self.ok_button.clicked.connect(self.update_combos) self.stop_button = create_toolbutton( self, text=_("Stop"), icon=ima.icon('editclear'), triggered=lambda: self.stop.emit(), tip=_("Stop search"), text_beside_icon=True) self.stop_button.setEnabled(False) for widget in [ self.search_text, self.edit_regexp, self.case_button, self.ok_button, self.stop_button, self.more_options ]: hlayout1.addWidget(widget) if options_button: hlayout1.addWidget(options_button) # Layout 2 hlayout2 = QHBoxLayout() self.exclude_pattern = PatternComboBox(self, exclude, _("Excluded filenames pattern")) if exclude_idx is not None and exclude_idx >= 0 \ and exclude_idx < self.exclude_pattern.count(): self.exclude_pattern.setCurrentIndex(exclude_idx) self.exclude_regexp = create_toolbutton(self, icon=ima.icon('advanced'), tip=_('Regular expression')) self.exclude_regexp.setCheckable(True) self.exclude_regexp.setChecked(exclude_regexp) exclude_label = QLabel(_("Exclude:")) exclude_label.setBuddy(self.exclude_pattern) for widget in [ exclude_label, self.exclude_pattern, self.exclude_regexp ]: hlayout2.addWidget(widget) # Layout 3 hlayout3 = QHBoxLayout() search_on_label = QLabel(_("Search in:")) self.path_selection_combo = SearchInComboBox(external_path_history, parent) hlayout3.addWidget(search_on_label) hlayout3.addWidget(self.path_selection_combo) self.search_text.valid.connect(lambda valid: self.find.emit()) self.exclude_pattern.valid.connect(lambda valid: self.find.emit()) vlayout = QVBoxLayout() vlayout.setContentsMargins(0, 0, 0, 0) vlayout.addLayout(hlayout1) vlayout.addLayout(hlayout2) vlayout.addLayout(hlayout3) self.more_widgets = (hlayout2, ) self.toggle_more_options(more_options) self.setLayout(vlayout) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum) @Slot(bool) def toggle_more_options(self, state): for layout in self.more_widgets: for index in range(layout.count()): if state and self.isVisible() or not state: layout.itemAt(index).widget().setVisible(state) if state: icon = ima.icon('options_less') tip = _('Hide advanced options') else: icon = ima.icon('options_more') tip = _('Show advanced options') self.more_options.setIcon(icon) self.more_options.setToolTip(tip) def update_combos(self): self.search_text.lineEdit().returnPressed.emit() self.exclude_pattern.lineEdit().returnPressed.emit() def set_search_text(self, text): if text: self.search_text.add_text(text) self.search_text.lineEdit().selectAll() self.search_text.setFocus() def get_options(self, all=False): # Getting options self.search_text.lineEdit().setStyleSheet("") self.exclude_pattern.lineEdit().setStyleSheet("") utext = to_text_string(self.search_text.currentText()) if not utext: return try: texts = [(utext.encode('utf-8'), 'utf-8')] except UnicodeEncodeError: texts = [] for enc in self.supported_encodings: try: texts.append((utext.encode(enc), enc)) except UnicodeDecodeError: pass text_re = self.edit_regexp.isChecked() exclude = to_text_string(self.exclude_pattern.currentText()) exclude_re = self.exclude_regexp.isChecked() case_sensitive = self.case_button.isChecked() python_path = False if not case_sensitive: texts = [(text[0].lower(), text[1]) for text in texts] file_search = self.path_selection_combo.is_file_search() path = self.path_selection_combo.get_current_searchpath() if not exclude_re: items = [ fnmatch.translate(item.strip()) for item in exclude.split(",") if item.strip() != '' ] exclude = '|'.join(items) # Validate regular expressions: try: if exclude: exclude = re.compile(exclude) except Exception: exclude_edit = self.exclude_pattern.lineEdit() exclude_edit.setStyleSheet(self.REGEX_INVALID) return None if text_re: try: texts = [(re.compile(x[0]), x[1]) for x in texts] except Exception: self.search_text.lineEdit().setStyleSheet(self.REGEX_INVALID) return None if all: search_text = [ to_text_string(self.search_text.itemText(index)) for index in range(self.search_text.count()) ] exclude = [ to_text_string(self.exclude_pattern.itemText(index)) for index in range(self.exclude_pattern.count()) ] path_history = self.path_selection_combo.get_external_paths() exclude_idx = self.exclude_pattern.currentIndex() more_options = self.more_options.isChecked() return (search_text, text_re, [], exclude, exclude_idx, exclude_re, python_path, more_options, case_sensitive, path_history) else: return (path, file_search, exclude, texts, text_re, case_sensitive) @property def path(self): return self.path_selection_combo.path def set_directory(self, directory): self.path_selection_combo.path = osp.abspath(directory) @property def project_path(self): return self.path_selection_combo.project_path def set_project_path(self, path): self.path_selection_combo.set_project_path(path) def disable_project_search(self): self.path_selection_combo.set_project_path(None) @property def file_path(self): return self.path_selection_combo.file_path def set_file_path(self, path): self.path_selection_combo.file_path = path def keyPressEvent(self, event): """Reimplemented to handle key events""" ctrl = event.modifiers() & Qt.ControlModifier shift = event.modifiers() & Qt.ShiftModifier if event.key() in (Qt.Key_Enter, Qt.Key_Return): self.find.emit() elif event.key() == Qt.Key_F and ctrl and shift: # Toggle find widgets self.parent().toggle_visibility.emit(not self.isVisible()) else: QWidget.keyPressEvent(self, event)
class FindOptions(QWidget): """Find widget with options""" find = Signal() stop = Signal() redirect_stdio = Signal(bool) def __init__(self, parent, search_text, search_text_regexp, search_path, include, include_idx, include_regexp, exclude, exclude_idx, exclude_regexp, supported_encodings, in_python_path, more_options): QWidget.__init__(self, parent) if search_path is None: search_path = getcwd() if not isinstance(search_text, (list, tuple)): search_text = [search_text] if not isinstance(search_path, (list, tuple)): search_path = [search_path] if not isinstance(include, (list, tuple)): include = [include] if not isinstance(exclude, (list, tuple)): exclude = [exclude] self.supported_encodings = supported_encodings # Layout 1 hlayout1 = QHBoxLayout() self.search_text = PatternComboBox(self, search_text, _("Search pattern")) self.edit_regexp = create_toolbutton(self, icon=ima.icon('advanced'), tip=_('Regular expression')) self.edit_regexp.setCheckable(True) self.edit_regexp.setChecked(search_text_regexp) self.more_widgets = () self.more_options = create_toolbutton(self, toggled=self.toggle_more_options) self.more_options.setCheckable(True) self.more_options.setChecked(more_options) self.ok_button = create_toolbutton(self, text=_("Search"), icon=ima.icon('DialogApplyButton'), triggered=lambda: self.find.emit(), tip=_("Start search"), text_beside_icon=True) self.ok_button.clicked.connect(self.update_combos) self.stop_button = create_toolbutton(self, text=_("Stop"), icon=ima.icon('stop'), triggered=lambda: self.stop.emit(), tip=_("Stop search"), text_beside_icon=True) self.stop_button.setEnabled(False) for widget in [self.search_text, self.edit_regexp, self.ok_button, self.stop_button, self.more_options]: hlayout1.addWidget(widget) # Layout 2 hlayout2 = QHBoxLayout() self.include_pattern = PatternComboBox(self, include, _("Included filenames pattern")) if include_idx is not None and include_idx >= 0 \ and include_idx < self.include_pattern.count(): self.include_pattern.setCurrentIndex(include_idx) self.include_regexp = create_toolbutton(self, icon=ima.icon('advanced'), tip=_('Regular expression')) self.include_regexp.setCheckable(True) self.include_regexp.setChecked(include_regexp) include_label = QLabel(_("Include:")) include_label.setBuddy(self.include_pattern) self.exclude_pattern = PatternComboBox(self, exclude, _("Excluded filenames pattern")) if exclude_idx is not None and exclude_idx >= 0 \ and exclude_idx < self.exclude_pattern.count(): self.exclude_pattern.setCurrentIndex(exclude_idx) self.exclude_regexp = create_toolbutton(self, icon=ima.icon('advanced'), tip=_('Regular expression')) self.exclude_regexp.setCheckable(True) self.exclude_regexp.setChecked(exclude_regexp) exclude_label = QLabel(_("Exclude:")) exclude_label.setBuddy(self.exclude_pattern) for widget in [include_label, self.include_pattern, self.include_regexp, exclude_label, self.exclude_pattern, self.exclude_regexp]: hlayout2.addWidget(widget) # Layout 3 hlayout3 = QHBoxLayout() self.python_path = QRadioButton(_("PYTHONPATH"), self) self.python_path.setChecked(in_python_path) self.python_path.setToolTip(_( "Search in all directories listed in sys.path which" " are outside the Python installation directory")) self.hg_manifest = QRadioButton(_("Hg repository"), self) self.detect_hg_repository() self.hg_manifest.setToolTip( _("Search in current directory hg repository")) self.custom_dir = QRadioButton(_("Here:"), self) self.custom_dir.setChecked(not in_python_path) self.dir_combo = PathComboBox(self) self.dir_combo.addItems(search_path) self.dir_combo.setToolTip(_("Search recursively in this directory")) self.dir_combo.open_dir.connect(self.set_directory) self.python_path.toggled.connect(self.dir_combo.setDisabled) self.hg_manifest.toggled.connect(self.dir_combo.setDisabled) browse = create_toolbutton(self, icon=ima.icon('DirOpenIcon'), tip=_('Browse a search directory'), triggered=self.select_directory) for widget in [self.python_path, self.hg_manifest, self.custom_dir, self.dir_combo, browse]: hlayout3.addWidget(widget) self.search_text.valid.connect(lambda valid: self.find.emit()) self.include_pattern.valid.connect(lambda valid: self.find.emit()) self.exclude_pattern.valid.connect(lambda valid: self.find.emit()) self.dir_combo.valid.connect(lambda valid: self.find.emit()) vlayout = QVBoxLayout() vlayout.setContentsMargins(0, 0, 0, 0) vlayout.addLayout(hlayout1) vlayout.addLayout(hlayout2) vlayout.addLayout(hlayout3) self.more_widgets = (hlayout2, hlayout3) self.toggle_more_options(more_options) self.setLayout(vlayout) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum) @Slot(bool) def toggle_more_options(self, state): for layout in self.more_widgets: for index in range(layout.count()): if state and self.isVisible() or not state: layout.itemAt(index).widget().setVisible(state) if state: icon = ima.icon('options_less') tip = _('Hide advanced options') else: icon = ima.icon('options_more') tip = _('Show advanced options') self.more_options.setIcon(icon) self.more_options.setToolTip(tip) def update_combos(self): self.search_text.lineEdit().returnPressed.emit() self.include_pattern.lineEdit().returnPressed.emit() self.exclude_pattern.lineEdit().returnPressed.emit() def detect_hg_repository(self, path=None): if path is None: path = getcwd() hg_repository = is_hg_installed() and get_vcs_root(path) is not None self.hg_manifest.setEnabled(hg_repository) if not hg_repository and self.hg_manifest.isChecked(): self.custom_dir.setChecked(True) def set_search_text(self, text): if text: self.search_text.add_text(text) self.search_text.lineEdit().selectAll() self.search_text.setFocus() def get_options(self, all=False): # Getting options utext = to_text_string(self.search_text.currentText()) if not utext: return try: texts = [(utext.encode('ascii'), 'ascii')] except UnicodeEncodeError: texts = [] for enc in self.supported_encodings: try: texts.append((utext.encode(enc), enc)) except UnicodeDecodeError: pass text_re = self.edit_regexp.isChecked() include = to_text_string(self.include_pattern.currentText()) include_re = self.include_regexp.isChecked() exclude = to_text_string(self.exclude_pattern.currentText()) exclude_re = self.exclude_regexp.isChecked() python_path = self.python_path.isChecked() hg_manifest = self.hg_manifest.isChecked() path = osp.abspath( to_text_string( self.dir_combo.currentText() ) ) # Finding text occurrences if not include_re: include = fnmatch.translate(include) if not exclude_re: exclude = fnmatch.translate(exclude) if all: search_text = [to_text_string(self.search_text.itemText(index)) \ for index in range(self.search_text.count())] search_path = [to_text_string(self.dir_combo.itemText(index)) \ for index in range(self.dir_combo.count())] include = [to_text_string(self.include_pattern.itemText(index)) \ for index in range(self.include_pattern.count())] include_idx = self.include_pattern.currentIndex() exclude = [to_text_string(self.exclude_pattern.itemText(index)) \ for index in range(self.exclude_pattern.count())] exclude_idx = self.exclude_pattern.currentIndex() more_options = self.more_options.isChecked() return (search_text, text_re, search_path, include, include_idx, include_re, exclude, exclude_idx, exclude_re, python_path, more_options) else: return (path, python_path, hg_manifest, include, exclude, texts, text_re) @Slot() def select_directory(self): """Select directory""" self.redirect_stdio.emit(False) directory = getexistingdirectory(self, _("Select directory"), self.dir_combo.currentText()) if directory: self.set_directory(directory) self.redirect_stdio.emit(True) def set_directory(self, directory): path = to_text_string(osp.abspath(to_text_string(directory))) self.dir_combo.setEditText(path) self.detect_hg_repository(path) def keyPressEvent(self, event): """Reimplemented to handle key events""" ctrl = event.modifiers() & Qt.ControlModifier shift = event.modifiers() & Qt.ShiftModifier if event.key() in (Qt.Key_Enter, Qt.Key_Return): self.find.emit() elif event.key() == Qt.Key_F and ctrl and shift: # Toggle find widgets self.parent().toggle_visibility.emit(not self.isVisible()) else: QWidget.keyPressEvent(self, event)
class ResultsBrowser(OneColumnTree): sig_edit_goto = Signal(str, int, str) def __init__(self, parent): OneColumnTree.__init__(self, parent) self.search_text = None self.results = None self.total_matches = None self.error_flag = None self.completed = None self.sorting = {} self.data = None self.files = None self.set_title('') self.set_sorting(OFF) self.setSortingEnabled(False) self.root_items = None self.sortByColumn(0, Qt.AscendingOrder) self.setItemDelegate(ItemDelegate(self)) self.setUniformRowHeights(False) self.header().sectionClicked.connect(self.sort_section) def activated(self, item): """Double-click event""" itemdata = self.data.get(id(self.currentItem())) if itemdata is not None: filename, lineno, colno = itemdata self.sig_edit_goto.emit(filename, lineno, self.search_text) def set_sorting(self, flag): """Enable result sorting after search is complete.""" self.sorting['status'] = flag self.header().setSectionsClickable(flag == ON) @Slot(int) def sort_section(self, idx): self.setSortingEnabled(True) def clicked(self, item): """Click event""" self.activated(item) def clear_title(self, search_text): self.clear() self.setSortingEnabled(False) self.num_files = 0 self.data = {} self.files = {} self.set_sorting(OFF) self.search_text = search_text title = "'%s' - " % search_text text = _('String not found') self.set_title(title + text) def truncate_result(self, line, start, end): ellipsis = u'...' max_line_length = 80 max_num_char_fragment = 40 html_escape_table = { "&": "&", '"': """, "'": "'", ">": ">", "<": "<", } def html_escape(text): """Produce entities within text.""" return "".join(html_escape_table.get(c, c) for c in text) if PY2: line = to_text_string(line, encoding='utf8') left, match, right = line[:start], line[start:end], line[end:] if len(line) > max_line_length: offset = (len(line) - len(match)) // 2 left = left.split(' ') num_left_words = len(left) if num_left_words == 1: left = left[0] if len(left) > max_num_char_fragment: left = ellipsis + left[-offset:] left = [left] right = right.split(' ') num_right_words = len(right) if num_right_words == 1: right = right[0] if len(right) > max_num_char_fragment: right = right[:offset] + ellipsis right = [right] left = left[-4:] right = right[:4] if len(left) < num_left_words: left = [ellipsis] + left if len(right) < num_right_words: right = right + [ellipsis] left = ' '.join(left) right = ' '.join(right) if len(left) > max_num_char_fragment: left = ellipsis + left[-30:] if len(right) > max_num_char_fragment: right = right[:30] + ellipsis line_match_format = to_text_string('{0}<b>{1}</b>{2}') left = html_escape(left) right = html_escape(right) match = html_escape(match) trunc_line = line_match_format.format(left, match, right) return trunc_line @Slot(tuple, int) def append_result(self, results, num_matches): """Real-time update of search results""" filename, lineno, colno, match_end, line = results if filename not in self.files: file_item = FileMatchItem(self, filename, self.sorting) file_item.setExpanded(True) self.files[filename] = file_item self.num_files += 1 search_text = self.search_text title = "'%s' - " % search_text nb_files = self.num_files if nb_files == 0: text = _('String not found') else: text_matches = _('matches in') text_files = _('file') if nb_files > 1: text_files += 's' text = "%d %s %d %s" % (num_matches, text_matches, nb_files, text_files) self.set_title(title + text) file_item = self.files[filename] line = self.truncate_result(line, colno, match_end) item = LineMatchItem(file_item, lineno, colno, line) self.data[id(item)] = (filename, lineno, colno)
class BasePluginWidget(QWidget, BasePluginWidgetMixin): """ Basic functionality for Spyder plugin widgets. WARNING: Don't override any methods or attributes present here! """ # Signal used to update the plugin title when it's undocked sig_update_plugin_title = Signal() def __init__(self, main=None): super(BasePluginWidget, self).__init__(main) # Dockwidget for the plugin, i.e. the pane that's going to be # displayed in Spyder for this plugin. # Note: This is created when you call the `add_dockwidget` # method, which must be done in the `register_plugin` one. self.dockwidget = None def add_dockwidget(self): """Add the plugin's QDockWidget to the main window.""" super(BasePluginWidget, self)._add_dockwidget() def tabify(self, core_plugin): """ Tabify plugin next to one of the core plugins. Parameters ---------- core_plugin: SpyderPluginWidget Core Spyder plugin this one will be tabified next to. Examples -------- >>> self.tabify(self.main.variableexplorer) >>> self.tabify(self.main.ipyconsole) Notes ----- The names of variables associated with each of the core plugins can be found in the `setup` method of `MainWindow`, present in `spyder/app/mainwindow.py`. """ super(BasePluginWidget, self)._tabify(core_plugin) def get_font(self, rich_text=False): """ Return plain or rich text font used in Spyder. Parameters ---------- rich_text: bool Return rich text font (i.e. the one used in the Help pane) or plain text one (i.e. the one used in the Editor). Returns ------- QFont: QFont object to be passed to other Qt widgets. Notes ----- All plugins in Spyder use the same, global font. This is a convenience method in case some plugins want to use a delta size based on the default one. That can be controlled by using FONT_SIZE_DELTA or RICH_FONT_SIZE_DELTA (declared below in `SpyderPluginWidget`). """ return super(BasePluginWidget, self)._get_font(rich_text) def register_shortcut(self, qaction_or_qshortcut, context, name, add_shortcut_to_tip=False): """ Register a shortcut associated to a QAction or a QShortcut to Spyder main application. Parameters ---------- qaction_or_qshortcut: QAction or QShortcut QAction to register the shortcut for or QShortcut. context: str Name of the plugin this shortcut applies to. For instance, if you pass 'Editor' as context, the shortcut will only work when the editor is focused. Note: You can use '_' if you want the shortcut to be work for the entire application. name: str Name of the action the shortcut refers to (e.g. 'Debug exit'). add_shortcut_to_tip: bool If True, the shortcut is added to the action's tooltip. This is useful if the action is added to a toolbar and users hover it to see what it does. """ super(BasePluginWidget, self)._register_shortcut( qaction_or_qshortcut, context, name, add_shortcut_to_tip) def register_widget_shortcuts(self, widget): """ Register shortcuts defined by a plugin's widget so they take effect when the plugin is focused. Parameters ---------- widget: QWidget Widget to register shortcuts for. Notes ----- The widget interface must have a method called `get_shortcut_data` for this to work. Please see `spyder/widgets/findreplace.py` for an example. """ for qshortcut, context, name in widget.get_shortcut_data(): self.register_shortcut(qshortcut, context, name) def get_color_scheme(self): """ Get the current color scheme. Returns ------- dict Dictionary with properties and colors of the color scheme used in the Editor. Notes ----- This is useful to set the color scheme of all instances of CodeEditor used by the plugin. """ return super(BasePluginWidget, self)._get_color_scheme() def switch_to_plugin(self): """ Switch to this plugin. Notes ----- This operation unmaximizes the current plugin (if any), raises this plugin to view (if it's hidden) and gives it focus (if possible). """ super(BasePluginWidget, self)._switch_to_plugin()