Exemple #1
0
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)
Exemple #2
0
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
Exemple #3
0
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
Exemple #4
0
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()
Exemple #5
0
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
Exemple #7
0
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)
Exemple #8
0
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)
Exemple #9
0
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()
Exemple #10
0
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')
Exemple #11
0
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()
Exemple #12
0
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)
Exemple #13
0
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)
Exemple #14
0
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
Exemple #15
0
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))
Exemple #16
0
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)
Exemple #17
0
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()
Exemple #18
0
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()
Exemple #19
0
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)
Exemple #20
0
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)
Exemple #21
0
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)
Exemple #22
0
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
Exemple #23
0
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)
Exemple #24
0
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)
Exemple #25
0
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)
Exemple #26
0
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
Exemple #27
0
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)
Exemple #28
0
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)
Exemple #29
0
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 = {
            "&": "&amp;",
            '"': "&quot;",
            "'": "&apos;",
            ">": "&gt;",
            "<": "&lt;",
        }

        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)
Exemple #30
0
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()