Example #1
0
class SubDict(UserDict, dict):
    """Class that keeps week reference to the base dictionary

This class is used by the :meth:`RcParams.find_and_replace` method
to provide an easy handable instance that keeps reference to the
base rcParams dictionary."""

    @property
    def data(self):
        """Dictionary representing this :class:`SubDict` instance

        See Also
        --------
        iteritems
        """
        return dict(self.iteritems())

    @property
    def replace(self):
        """:class:`bool`. If True, matching strings in the :attr:`base_str`
        attribute are replaced with an empty string."""
        return self._replace

    @replace.setter
    def replace(self, value):
        def replace_base(key):
            for pattern in self.patterns:
                try:
                    return pattern.match(key).group('key')
                except AttributeError:  # if match is None
                    pass
            raise KeyError(
                "Could not find any matching key for %s in the base "
                "dictionary!" % key)

        value = bool(value)
        if hasattr(self, '_replace') and value == self._replace:
            return
        if not hasattr(self, '_replace'):
            self._replace = value
            return
        # if the value has changed, we change the key in the SubDict instance
        # to match the ones in the base dictionary (if they exist)
        for key, val in DictMethods.iteritems(self):
            try:
                if value:
                    new_key = replace_base(key)
                else:
                    new_key = self._get_val_and_base(key)[0]
            except KeyError:
                continue
            else:
                dict.__setitem__(self, new_key, dict.pop(self, key))
        self._replace = value

    #: :class:`dict`. Reference dictionary
    base = {}

    #: list of strings. The strings that are used to set and get a specific key
    #: from the :attr:`base` dictionary
    base_str = []

    #: list of compiled patterns from the :attr:`base_str` attribute, that
    #: are used to look for the matching keys in :attr:`base`
    patterns = []

    #: :class:`bool`. If True, changes are traced back to the :attr:`base` dict
    trace = False

    @docstrings.get_sections(base='SubDict.add_base_str')
    @dedent
    def add_base_str(self, base_str, pattern='.+', pattern_base=None,
                     append=True):
        """
        Add further base string to this instance

        Parameters
        ----------
        base_str: str or list of str
            Strings that are used as to look for keys to get and set keys in
            the :attr:`base` dictionary. If a string does not contain
            ``'%(key)s'``, it will be appended at the end. ``'%(key)s'`` will
            be replaced by the specific key for getting and setting an item.
        pattern: str
            Default: ``'.+'``. This is the pattern that is inserted for
            ``%(key)s`` in a base string to look for matches (using the
            :mod:`re` module) in the `base` dictionary. The default `pattern`
            matches everything without white spaces.
        pattern_base: str or list or str
            If None, the whatever is given in the `base_str` is used.
            Those strings will be used for generating the final search
            patterns. You can specify this parameter by yourself to avoid the
            misinterpretation of patterns. For example for a `base_str` like
            ``'my.str'`` it is recommended to additionally provide the
            `pattern_base` keyword with ``'my\.str'``.
            Like for `base_str`, the ``%(key)s`` is appended if not already in
            the string.
        append: bool
            If True, the given `base_str` are appended (i.e. it is first
            looked for them in the :attr:`base` dictionary), otherwise they are
            put at the beginning"""
        base_str = safe_list(base_str)
        pattern_base = safe_list(pattern_base or [])
        for i, s in enumerate(base_str):
            if '%(key)s' not in s:
                base_str[i] += '%(key)s'
        if pattern_base:
            for i, s in enumerate(pattern_base):
                if '%(key)s' not in s:
                    pattern_base[i] += '%(key)s'
        else:
            pattern_base = base_str
        self.base_str = base_str + self.base_str
        self.patterns = list(map(lambda s: re.compile(s.replace(
            '%(key)s', '(?P<key>%s)' % pattern)), pattern_base)) + \
            self.patterns

    docstrings.delete_params('SubDict.add_base_str.parameters', 'append')

    @docstrings.get_sections(base='SubDict')
    @docstrings.dedent
    def __init__(self, base, base_str, pattern='.+', pattern_base=None,
                 trace=False, replace=True):
        """
        Parameters
        ----------
        base: dict
            base dictionary
        %(SubDict.add_base_str.parameters.no_append)s
        trace: bool
            Default: False. If True, changes in the SubDict are traced back to
            the `base` dictionary. You can change this behaviour also
            afterwards by changing the :attr:`trace` attribute
        replace: bool
            Default: True. If True, everything but the '%%(key)s' part in a
            base string is replaced (see examples below)


        Notes
        -----
        - If a key of matches multiple strings in `base_str`, the first
          matching one is used.
        - the SubDict class is (of course) not that efficient as the
          :attr:`base` dictionary, since we loop multiple times through it's
          keys

        Examples
        --------
        Initialization example::

            >>> from psyplot import rcParams
            >>> d = rcParams.find_and_replace(['plotter.baseplotter.',
            ...                                'plotter.vector.'])
            >>> print d['title']

            >>> print d['arrowsize']
            1.0

        To convert it to a usual dictionary, simply use the :attr:`data`
        attribute::

            >>> d.data
            {'title': None, 'arrowsize': 1.0, ...}

        Note that changing one keyword of your :class:`SubDict` will not change
        the :attr:`base` dictionary, unless you set the :attr:`trace` attribute
        to ``True``::

            >>> d['title'] = 'my title'
            >>> print(d['title'])
            my title

            >>> print(rcParams['plotter.baseplotter.title'])

            >>> d.trace = True
            >>> d['title'] = 'my second title'
            >>> print(d['title'])
            my second title
            >>> print(rcParams['plotter.baseplotter.title'])
            my second title

        Furthermore, changing the :attr:`replace` attribute will change how you
        can access the keys::

            >>> d.replace = False

            # now setting d['title'] = 'anything' would raise an error (since
            # d.trace is set to True and 'title' is not a key in the rcParams
            # dictionary. Instead we need
            >>> d['plotter.baseplotter.title'] = 'anything'

        See Also
        --------
        RcParams.find_and_replace"""
        self.base = base
        self.base_str = []
        self.patterns = []
        self.replace = bool(replace)
        self.trace = bool(trace)
        self.add_base_str(base_str, pattern=pattern, pattern_base=pattern_base,
                          append=False)

    def __getitem__(self, key):
        if key in DictMethods.iterkeys(self):
            return dict.__getitem__(self, key)
        if not self.replace:
            return self.base[key]
        return self._get_val_and_base(key)[1]

    def __setitem__(self, key, val):
        # set it in the SubDict instance if trace is False
        if not self.trace:
            dict.__setitem__(self, key, val)
            return
        base = self.base
        # set it with the given key, if trace is True
        if not self.replace:
            base[key] = val
            dict.pop(self, key, None)
            return
        # first look if the key already exists in the base dictionary
        for s, patt in self._iter_base_and_pattern(key):
            m = patt.match(s)
            if m and s in base:
                base[m.group()] = val
                return
        # if the key does not exist, we set it
        self.base[key] = val

    def _get_val_and_base(self, key):
        found = False
        e = None
        for s, patt in self._iter_base_and_pattern(key):
            found = True
            try:
                m = patt.match(s)
                if m:
                    return m.group(), self.base[m.group()]
                else:
                    raise KeyError(
                        "{0} does not match the specified pattern!".format(
                            s))
            except KeyError as e:
                pass
        if not found:
            if e is not None:
                raise
        raise KeyError("{0} does not match the specified pattern!".format(
                            key))

    def _iter_base_and_pattern(self, key):
        return zip(
            map(lambda s: safe_modulo(s, {'key': key}), self.base_str),
            self.patterns)

    def iterkeys(self):
        """Unsorted iterator over keys"""
        patterns = self.patterns
        replace = self.replace
        seen = set()
        for key in six.iterkeys(self.base):
            for pattern in patterns:
                m = pattern.match(key)
                if m:
                    ret = m.group('key') if replace else m.group()
                    if ret not in seen:
                        seen.add(ret)
                        yield ret
                    break
        for key in DictMethods.iterkeys(self):
            if key not in seen:
                yield key

    def iteritems(self):
        """Unsorted iterator over items"""
        return ((key, self[key]) for key in self.iterkeys())

    def itervalues(self):
        """Unsorted iterator over values"""
        return (val for key, val in self.iteritems())

    def update(self, *args, **kwargs):
        """Update the dictionary"""
        for k, v in six.iteritems(dict(*args, **kwargs)):
            self[k] = v
Example #2
0
    def iteritems(self):
        """Unsorted iterator over items"""
        return ((key, self[key]) for key in self.iterkeys())

    def itervalues(self):
        """Unsorted iterator over values"""
        return (val for key, val in self.iteritems())

    def update(self, *args, **kwargs):
        """Update the dictionary"""
        for k, v in six.iteritems(dict(*args, **kwargs)):
            self[k] = v


docstrings.delete_params('SubDict.parameters', 'base')


class RcParams(dict):
    """A dictionary object including validation

    validating functions are defined and associated with rc parameters in
    :data:`defaultParams`

    This class is essentially the same as in maplotlibs
    :class:`~matplotlib.RcParams` but has the additional
    :meth:`find_and_replace` method."""

    @property
    def validate(self):
        """Dictionary with validation methods as values"""
Example #3
0
from psy_simple.widgets import Switch2FmtButton, get_icon
import psy_simple.colors as psc
from psy_simple.plugin import BoundsType, CTicksType
from psyplot.docstring import docstrings
import numpy as np
import xarray as xr
import matplotlib as mpl
import matplotlib.colors as mcol
from PyQt5 import QtWidgets, QtCore, QtGui
from PyQt5.QtCore import Qt


mpl_version = tuple(map(int, mpl.__version__.split('.')[:2]))


docstrings.delete_params('show_colormaps.parameters', 'show', 'use_qt')


class ColormapModel(QtCore.QAbstractTableModel):
    """A model for displaying colormaps"""

    @docstrings.get_sections(base='ColormapModel')
    @docstrings.with_indent(8)
    def __init__(self, names=[], N=10, *args, **kwargs):
        """
        Parameters
        ----------
        %(show_colormaps.parameters.no_show|use_qt)s

        Other Parameters
        ----------------
Example #4
0
class ColormapDialog(QtWidgets.QDialog):
    """A widget for selecting a colormap"""

    @docstrings.with_indent(8)
    def __init__(self, names=[], N=10, editable=True, *args, **kwargs):
        """
        Parameters
        ----------
        %(ColormapModel.parameters)s

        Other Parameters
        ----------------
        ``*args, **kwargs``
            Anything else that is passed to the ColormapDialog
        """
        super(QtWidgets.QDialog, self).__init__(*args, **kwargs)
        vbox = QtWidgets.QVBoxLayout()
        self.table = ColormapTable(names=names, N=N, editable=editable)
        if editable:
            vbox.addWidget(QtWidgets.QLabel("Double-click a color to edit"))
        vbox.addWidget(self.table)
        self.setLayout(vbox)
        col_width = self.table.columnWidth(0)
        header_width = self.table.verticalHeader().width()
        row_height = self.table.rowHeight(0)
        available = QtWidgets.QDesktopWidget().availableGeometry()
        height = int(min(row_height * (self.table.rowCount() + 1),
                         2. * available.height() / 3.))
        width = int(min(header_width + col_width * N + 0.5 * col_width,
                        2. * available.width() / 3.))
        self.resize(QtCore.QSize(width, height))

    @classmethod
    @docstrings.with_indent(8)
    def get_colormap(cls, names=[], N=10, *args, **kwargs):
        """Open a :class:`ColormapDialog` and get a colormap

        Parameters
        ----------
        %(ColormapModel.parameters)s

        Other Parameters
        ----------------
        ``*args, **kwargs``
            Anything else that is passed to the ColormapDialog

        Returns
        -------
        str or matplotlib.colors.Colormap
            Either the name of a standard colormap available via
            :func:`psy_simple.colors.get_cmap` or a colormap
        """
        names = safe_list(names)
        obj = cls(names, N, *args, **kwargs)
        vbox = obj.layout()
        buttons = QtWidgets.QDialogButtonBox(
            QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel, parent=obj)
        buttons.button(QtWidgets.QDialogButtonBox.Ok).setEnabled(False)
        vbox.addWidget(buttons)
        buttons.accepted.connect(obj.accept)
        buttons.rejected.connect(obj.reject)

        obj.table.selectionModel().selectionChanged.connect(
            lambda indices: buttons.button(QtWidgets.QDialogButtonBox.Ok).setEnabled(
                bool(indices)))
        accepted = obj.exec_()
        if accepted:
            return obj.table.chosen_colormap

    docstrings.delete_params('show_colormaps.parameters', 'use_qt')

    @classmethod
    @docstrings.with_indent(8)
    def show_colormap(cls, names=[], N=10, show=True, *args, **kwargs):
        """Show a colormap dialog

        Parameters
        ----------
        %(show_colormaps.parameters.no_use_qt)s"""
        names = safe_list(names)
        obj = cls(names, N, *args, **kwargs)
        vbox = obj.layout()
        buttons = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Close, parent=obj)
        buttons.rejected.connect(obj.close)
        vbox.addWidget(buttons)
        if show:
            obj.show()
        return obj
Example #5
0
class DataFrameView(QTableView):
    """Data Frame view class"""

    @property
    def filled(self):
        """True if the table is filled with content"""
        return bool(self.model().rows_loaded)

    @docstrings.dedent
    def __init__(self, df, parent, *args, **kwargs):
        """
        Parameters
        ----------
        %(DataFrameModel.parameters)s
        """
        QTableView.__init__(self, parent)
        model = DataFrameModel(df, parent, *args, **kwargs)
        self.setModel(model)
        self.menu = self.setup_menu()

        self.frozen_table_view = FrozenTableView(self)
        self.frozen_table_view.update_geometry()

        self.setHorizontalScrollMode(1)
        self.setVerticalScrollMode(1)

        self.horizontalHeader().sectionResized.connect(
            self.update_section_width)
        self.verticalHeader().sectionResized.connect(
            self.update_section_height)

        self.sort_old = [None]
        self.header_class = self.horizontalHeader()
        self.header_class.sectionClicked.connect(self.sortByColumn)
        self.frozen_table_view.horizontalHeader().sectionClicked.connect(
            self.sortByColumn)
        self.horizontalScrollBar().valueChanged.connect(
                        lambda val: self.load_more_data(val, columns=True))
        self.verticalScrollBar().valueChanged.connect(
                        lambda val: self.load_more_data(val, rows=True))

    def update_section_width(self, logical_index, old_size, new_size):
        """Update the horizontal width of the frozen column when a
        change takes place in the first column of the table"""
        if logical_index == 0:
            self.frozen_table_view.setColumnWidth(0, new_size)
            self.frozen_table_view.update_geometry()

    def update_section_height(self, logical_index, old_size, new_size):
        """Update the vertical width of the frozen column when a
        change takes place on any of the rows"""
        self.frozen_table_view.setRowHeight(logical_index, new_size)

    def resizeEvent(self, event):
        """Update the frozen column dimensions.

        Updates takes place when the enclosing window of this
        table reports a dimension change
        """
        QTableView.resizeEvent(self, event)
        self.frozen_table_view.update_geometry()

    def moveCursor(self, cursor_action, modifiers):
        """Update the table position.

        Updates the position along with the frozen column
        when the cursor (selector) changes its position
        """
        current = QTableView.moveCursor(self, cursor_action, modifiers)

        col_width = (self.columnWidth(0) +
                     self.columnWidth(1))
        topleft_x = self.visualRect(current).topLeft().x()

        overflow = self.MoveLeft and current.column() > 1
        overflow = overflow and topleft_x < col_width

        if cursor_action == overflow:
            new_value = (self.horizontalScrollBar().value() +
                         topleft_x - col_width)
            self.horizontalScrollBar().setValue(new_value)
        return current

    def scrollTo(self, index, hint):
        """Scroll the table.

        It is necessary to ensure that the item at index is visible.
        The view will try to position the item according to the
        given hint. This method does not takes effect only if
        the frozen column is scrolled.
        """
        if index.column() > 1:
            QTableView.scrollTo(self, index, hint)

    def load_more_data(self, value, rows=False, columns=False):
        if rows and value == self.verticalScrollBar().maximum():
            self.model().fetch_more(rows=rows)
        if columns and value == self.horizontalScrollBar().maximum():
            self.model().fetch_more(columns=columns)

    def sortByColumn(self, index):
        """ Implement a Column sort """
        frozen_header = self.frozen_table_view.horizontalHeader()
        if not self.isSortingEnabled():
            self.header_class.setSortIndicatorShown(False)
            frozen_header.setSortIndicatorShown(False)
            return
        if self.sort_old == [None]:
            self.header_class.setSortIndicatorShown(True)
        frozen_header.setSortIndicatorShown(index == 0)
        if index == 0:
            sort_order = frozen_header.sortIndicatorOrder()
        else:
            sort_order = self.header_class.sortIndicatorOrder()
        if not self.model().sort(index, sort_order, True):
            if len(self.sort_old) != 2:
                self.header_class.setSortIndicatorShown(False)
                frozen_header.setSortIndicatorShown(False)
            else:
                self.header_class.setSortIndicator(self.sort_old[0],
                                                   self.sort_old[1])
                if index == 0:
                    frozen_header.setSortIndicator(self.sort_old[0],
                                                   self.sort_old[1])
            return
        self.sort_old = [index, self.header_class.sortIndicatorOrder()]

    def change_type(self, func):
        """A function that changes types of cells"""
        model = self.model()
        index_list = self.selectedIndexes()
        for i in index_list:
            model.setData(i, '', change_type=func)

    def insert_row_above_selection(self):
        """Insert rows above the selection

        The number of rows inserted depends on the number of selected rows"""
        rows, cols = self._selected_rows_and_cols()
        model = self.model()
        if not model.rowCount():
            model.insertRows(0, 1)
        elif not rows and not cols:
            return
        else:
            min_row = min(rows)
            nrows = len(set(rows))
            model.insertRows(min_row, nrows)

    def insert_row_below_selection(self):
        """Insert rows below the selection

        The number of rows inserted depends on the number of selected rows"""
        rows, cols = self._selected_rows_and_cols()
        model = self.model()
        if not model.rowCount():
            model.insertRows(0, 1)
        elif not rows and not cols:
            return
        else:
            max_row = max(rows)
            nrows = len(set(rows))
            model.insertRows(max_row + 1, nrows)

    def _selected_rows_and_cols(self):
        index_list = self.selectedIndexes()
        if not index_list:
            return [], []
        return list(zip(*[(i.row(), i.column()) for i in index_list]))

    docstrings.delete_params('DataFrameModel.parameters', 'parent')

    @docstrings.dedent
    def set_df(self, df, *args, **kwargs):
        """
        Set the :class:`~pandas.DataFrame` for this table

        Parameters
        ----------
        %(DataFrameModel.parameters.no_parent)s
        """
        model = DataFrameModel(df, self.parent(), *args, **kwargs)
        self.setModel(model)
        self.frozen_table_view.setModel(model)

    def reset_model(self):
        self.model().reset()

    def contextMenuEvent(self, event):
        """Reimplement Qt method"""
        model = self.model()
        for a in self.dtype_actions.values():
            a.setEnabled(model.dtypes_changeable)
        nrows = max(len(set(self._selected_rows_and_cols()[0])), 1)
        self.insert_row_above_action.setText('Insert %i row%s above' % (
            nrows, 's' if nrows - 1 else ''))
        self.insert_row_below_action.setText('Insert %i row%s below' % (
            nrows, 's' if nrows - 1 else ''))
        self.insert_row_above_action.setEnabled(model.index_editable)
        self.insert_row_below_action.setEnabled(model.index_editable)
        self.menu.popup(event.globalPos())
        event.accept()

    def setup_menu(self):
        """Setup context menu"""
        menu = QMenu(self)
        menu.addAction('Copy', self.copy, QtGui.QKeySequence.Copy)
        menu.addSeparator()
        functions = (("To bool", bool), ("To complex", complex),
                     ("To int", int), ("To float", float),
                     ("To str", str))
        self.dtype_actions = {
            name: menu.addAction(name, partial(self.change_type, func))
            for name, func in functions}
        menu.addSeparator()
        self.insert_row_above_action = menu.addAction(
            'Insert rows above', self.insert_row_above_selection)
        self.insert_row_below_action = menu.addAction(
            'Insert rows below', self.insert_row_below_selection)
        menu.addSeparator()
        self.set_index_action = menu.addAction(
            'Set as index', partial(self.set_index, False))
        self.append_index_action = menu.addAction(
            'Append to as index', partial(self.set_index, True))
        return menu

    def set_index(self, append=False):
        """Set the index from the selected columns"""
        model = self.model()
        df = model.df
        args = [model.dtypes_changeable, model.index_editable]
        cols = np.unique(self._selected_rows_and_cols()[1])
        if not append:
            cols += len(df.index.names) - 1
            df.reset_index(inplace=True)
        else:
            cols -= 1
        cols = cols.tolist()
        if len(cols) == 1:
            df.set_index(df.columns[cols[0]], inplace=True, append=append)
        else:
            df.set_index(df.columns[cols].tolist(), inplace=True,
                         append=append)
        self.set_df(df, *args)

    def copy(self):
        """Copy text to clipboard"""
        rows, cols = self._selected_rows_and_cols()
        if not rows and not cols:
            return
        row_min, row_max = min(rows), max(rows)
        col_min, col_max = min(cols), max(cols)
        index = header = False
        if col_min == 0:
            col_min = 1
            index = True
        df = self.model().df
        if col_max == 0:  # To copy indices
            contents = '\n'.join(map(str, df.index.tolist()[slice(row_min,
                                                            row_max+1)]))
        else:  # To copy DataFrame
            if (col_min == 0 or col_min == 1) and (df.shape[1] == col_max):
                header = True
            obj = df.iloc[slice(row_min, row_max+1), slice(col_min-1, col_max)]
            output = io.StringIO()
            obj.to_csv(output, sep='\t', index=index, header=header)
            if not six.PY2:
                contents = output.getvalue()
            else:
                contents = output.getvalue().decode('utf-8')
            output.close()
        clipboard = QApplication.clipboard()
        clipboard.setText(contents)