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
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"""
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 ----------------
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
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)