Total Complexity | 91 |
Total Lines | 400 |
Duplicated Lines | 0 % |
Complex classes like Orange.widgets.utils.TableModel often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
1 | import pickle |
||
501 | class TableModel(QAbstractTableModel): |
||
502 | """ |
||
503 | An adapter for using Orange.data.Table within Qt's Item View Framework. |
||
504 | |||
505 | :param Orange.data.Table sourcedata: Source data table. |
||
506 | :param QObject parent: |
||
507 | """ |
||
508 | #: Orange.data.Value for the index. |
||
509 | ValueRole = gui.TableValueRole # next(gui.OrangeUserRole) |
||
510 | #: Orange.data.Value of the row's class. |
||
511 | ClassValueRole = gui.TableClassValueRole # next(gui.OrangeUserRole) |
||
512 | #: Orange.data.Variable of the column. |
||
513 | VariableRole = gui.TableVariable # next(gui.OrangeUserRole) |
||
514 | #: Basic statistics of the column |
||
515 | VariableStatsRole = next(gui.OrangeUserRole) |
||
516 | #: The column's role (position) in the domain. |
||
517 | #: One of Attribute, ClassVar or Meta |
||
518 | DomainRole = next(gui.OrangeUserRole) |
||
519 | |||
520 | #: Column domain roles |
||
521 | Attribute, ClassVar, Meta = range(3) |
||
522 | |||
523 | #: Default background color for domain roles |
||
524 | ColorForRole = { |
||
525 | Attribute: None, |
||
526 | ClassVar: QColor(160, 160, 160), |
||
527 | Meta: QColor(220, 220, 200) |
||
528 | } |
||
529 | |||
530 | #: Standard column descriptor |
||
531 | Column = namedtuple( |
||
532 | "Column", ["var", "role", "background", "format"]) |
||
533 | #: Basket column descriptor (i.e. sparse X/Y/metas/ compressed into |
||
534 | #: a single column). |
||
535 | Basket = namedtuple( |
||
536 | "Basket", ["vars", "role", "background", "density", "format"]) |
||
537 | |||
538 | def __init__(self, sourcedata, parent=None): |
||
539 | super().__init__(parent) |
||
540 | self.source = sourcedata |
||
541 | self.domain = domain = sourcedata.domain |
||
542 | |||
543 | self.X_density = sourcedata.X_density() |
||
544 | self.Y_density = sourcedata.Y_density() |
||
545 | self.M_density = sourcedata.metas_density() |
||
546 | |||
547 | def format_sparse(vars, datagetter, instance): |
||
548 | data = datagetter(instance) |
||
549 | return ", ".join("{}={}".format(vars[i].name, vars[i].repr_val(v)) |
||
550 | for i, v in zip(data.indices, data.data)) |
||
551 | |||
552 | def format_sparse_bool(vars, datagetter, instance): |
||
553 | data = datagetter(instance) |
||
554 | return ", ".join(vars[i].name for i in data.indices) |
||
555 | |||
556 | def format_dense(var, instance): |
||
557 | return str(instance[var]) |
||
558 | |||
559 | def make_basket_formater(vars, density, role): |
||
560 | formater = (format_sparse if density == Storage.SPARSE |
||
561 | else format_sparse_bool) |
||
562 | if role == TableModel.Attribute: |
||
563 | getter = operator.attrgetter("sparse_x") |
||
564 | elif role == TableModel.ClassVar: |
||
565 | getter = operator.attrgetter("sparse_y") |
||
566 | elif role == TableModel.Meta: |
||
567 | getter = operator.attrgetter("sparse_meta") |
||
568 | return partial(formater, vars, getter) |
||
569 | |||
570 | def make_basket(vars, density, role): |
||
571 | return TableModel.Basket( |
||
572 | vars, TableModel.Attribute, self.ColorForRole[role], density, |
||
573 | make_basket_formater(vars, density, role) |
||
574 | ) |
||
575 | |||
576 | def make_column(var, role): |
||
577 | return TableModel.Column( |
||
578 | var, role, self.ColorForRole[role], |
||
579 | partial(format_dense, var) |
||
580 | ) |
||
581 | |||
582 | columns = [] |
||
583 | |||
584 | if self.X_density != Storage.DENSE: |
||
585 | coldesc = make_basket(domain.attributes, self.X_density, |
||
586 | TableModel.Attribute) |
||
587 | columns.append(coldesc) |
||
588 | else: |
||
589 | columns += [make_column(var, TableModel.Attribute) |
||
590 | for var in domain.attributes] |
||
591 | |||
592 | if self.Y_density != Storage.DENSE: |
||
593 | coldesc = make_basket(domain.class_vars, self.Y_density, |
||
594 | TableModel.ClassVar) |
||
595 | columns.append(coldesc) |
||
596 | else: |
||
597 | columns += [make_column(var, TableModel.ClassVar) |
||
598 | for var in domain.class_vars] |
||
599 | |||
600 | if self.M_density != Storage.DENSE: |
||
601 | coldesc = make_basket(domain.metas, self.M_density, |
||
602 | TableModel.Meta) |
||
603 | columns.append(coldesc) |
||
604 | else: |
||
605 | columns += [make_column(var, TableModel.Meta) |
||
606 | for var in domain.metas] |
||
607 | |||
608 | #: list of all domain variables (attrs + class_vars + metas) |
||
609 | self.vars = domain.attributes + domain.class_vars + domain.metas |
||
610 | self.columns = columns |
||
611 | |||
612 | #: A list of all unique attribute labels (in all variables) |
||
613 | self._labels = sorted( |
||
614 | reduce(operator.ior, |
||
615 | [set(var.attributes) for var in self.vars], |
||
616 | set())) |
||
617 | |||
618 | #@lru_cache(maxsize=1000) |
||
619 | def row_instance(index): |
||
620 | return self.source[int(index)] |
||
621 | self._row_instance = row_instance |
||
622 | |||
623 | # column basic statistics (VariableStatsRole), computed when |
||
624 | # first needed. |
||
625 | self.__stats = None |
||
626 | self.__rowCount = sourcedata.approx_len() |
||
627 | self.__columnCount = len(self.columns) |
||
628 | |||
629 | if self.__rowCount > (2 ** 31 - 1): |
||
630 | raise ValueError("len(sourcedata) > 2 ** 31 - 1") |
||
631 | |||
632 | self.__sortColumn = -1 |
||
633 | self.__sortOrder = Qt.AscendingOrder |
||
634 | # Indices sorting the source table |
||
635 | self.__sortInd = None |
||
636 | # The inverse of __sortInd |
||
637 | self.__sortIndInv = None |
||
638 | |||
639 | def sort(self, column, order): |
||
640 | """ |
||
641 | Sort the data by `column` index into `order` |
||
642 | |||
643 | To reset the sort order pass -1 as the column. |
||
644 | |||
645 | :type column: int |
||
646 | :type order: Qt.SortOrder |
||
647 | |||
648 | Reimplemented from QAbstractItemModel.sort |
||
649 | |||
650 | .. note:: |
||
651 | This only affects the model's data presentation, the |
||
652 | underlying data table is left unmodified. |
||
653 | |||
654 | """ |
||
655 | self.layoutAboutToBeChanged.emit() |
||
656 | |||
657 | # Store persistent indices as well as their (actual) rows in the |
||
658 | # source data table. |
||
659 | persistent = self.persistentIndexList() |
||
660 | persistent_rows = numpy.array([ind.row() for ind in persistent], int) |
||
661 | if self.__sortInd is not None: |
||
662 | persistent_rows = self.__sortInd[persistent_rows] |
||
663 | |||
664 | self.__sortColumn = column |
||
665 | self.__sortOrder = order |
||
666 | |||
667 | if column < 0: |
||
668 | indices = None |
||
669 | else: |
||
670 | keydata = self.columnSortKeyData(column, TableModel.ValueRole) |
||
671 | if keydata is not None: |
||
672 | if keydata.dtype == object: |
||
673 | indices = sorted(range(self.__rowCount), |
||
674 | key=lambda i: str(keydata[i])) |
||
675 | indices = numpy.array(indices) |
||
676 | else: |
||
677 | indices = numpy.argsort(keydata, kind="mergesort") |
||
678 | else: |
||
679 | indices = numpy.arange(0, self.__rowCount) |
||
680 | |||
681 | if order == Qt.DescendingOrder: |
||
682 | indices = indices[::-1] |
||
683 | |||
684 | if self.__sortInd is not None: |
||
685 | indices = self.__sortInd[indices] |
||
686 | |||
687 | if indices is not None: |
||
688 | self.__sortInd = indices |
||
689 | self.__sortIndInv = numpy.argsort(indices) |
||
690 | else: |
||
691 | self.__sortInd = None |
||
692 | self.__sortIndInv = None |
||
693 | |||
694 | if self.__sortInd is not None: |
||
695 | persistent_rows = self.__sortIndInv[persistent_rows] |
||
696 | |||
697 | for pind, row in zip(persistent, persistent_rows): |
||
698 | self.changePersistentIndex(pind, self.index(row, pind.column())) |
||
699 | self.layoutChanged.emit() |
||
700 | |||
701 | def columnSortKeyData(self, column, role): |
||
702 | """ |
||
703 | Return a sequence of objects which can be used as `keys` for sorting. |
||
704 | |||
705 | :param int column: Sort column. |
||
706 | :param Qt.ItemRole role: Sort item role. |
||
707 | |||
708 | """ |
||
709 | coldesc = self.columns[column] |
||
710 | if isinstance(coldesc, TableModel.Column) \ |
||
711 | and role == TableModel.ValueRole: |
||
712 | col_view, _ = self.source.get_column_view(coldesc.var) |
||
713 | col_data = numpy.asarray(col_view) |
||
714 | if self.__sortInd is not None: |
||
715 | col_data = col_data[self.__sortInd] |
||
716 | return col_data |
||
717 | else: |
||
718 | if self.__sortInd is not None: |
||
719 | indices = self.__sortInd |
||
720 | else: |
||
721 | indices = range(self.rowCount()) |
||
722 | return numpy.asarray([self.index(i, column).data(role) |
||
723 | for i in indices]) |
||
724 | |||
725 | def sortColumn(self): |
||
726 | """ |
||
727 | The column currently used for sorting (-1 if no sorting is applied). |
||
728 | """ |
||
729 | return self.__sortColumn |
||
730 | |||
731 | def sortOrder(self): |
||
732 | """ |
||
733 | The current sort order. |
||
734 | """ |
||
735 | return self.__sortOrder |
||
736 | |||
737 | def mapToTableRows(self, modelrows): |
||
738 | """ |
||
739 | Return the row indices in the source table for the given model rows. |
||
740 | """ |
||
741 | if self.__sortColumn < 0: |
||
742 | return modelrows |
||
743 | else: |
||
744 | return self.__sortInd[modelrows].tolist() |
||
745 | |||
746 | def mapFromTableRows(self, tablerows): |
||
747 | """ |
||
748 | Return the row indices in the model for the given source table rows. |
||
749 | """ |
||
750 | if self.__sortColumn < 0: |
||
751 | return tablerows |
||
752 | else: |
||
753 | return self.__sortIndInv[tablerows].tolist() |
||
754 | |||
755 | def data(self, index, role, |
||
756 | # For optimizing out LOAD_GLOBAL byte code instructions in |
||
757 | # the item role tests. |
||
758 | _str=str, |
||
759 | _Qt_DisplayRole=Qt.DisplayRole, |
||
760 | _Qt_EditRole=Qt.EditRole, |
||
761 | _Qt_BackgroundRole=Qt.BackgroundRole, |
||
762 | _ValueRole=ValueRole, |
||
763 | _ClassValueRole=ClassValueRole, |
||
764 | _VariableRole=VariableRole, |
||
765 | _DomainRole=DomainRole, |
||
766 | _VariableStatsRole=VariableStatsRole, |
||
767 | # Some cached local precomputed values. |
||
768 | # All of the above roles we respond to |
||
769 | _recognizedRoles=set([Qt.DisplayRole, |
||
770 | Qt.EditRole, |
||
771 | Qt.BackgroundRole, |
||
772 | ValueRole, |
||
773 | ClassValueRole, |
||
774 | VariableRole, |
||
775 | DomainRole, |
||
776 | VariableStatsRole]), |
||
777 | ): |
||
778 | """ |
||
779 | Reimplemented from `QAbstractItemModel.data` |
||
780 | """ |
||
781 | if role not in _recognizedRoles: |
||
782 | return None |
||
783 | |||
784 | row, col = index.row(), index.column() |
||
785 | if not 0 <= row <= self.__rowCount: |
||
786 | return None |
||
787 | |||
788 | if self.__sortInd is not None: |
||
789 | row = self.__sortInd[row] |
||
790 | |||
791 | instance = self._row_instance(row) |
||
792 | coldesc = self.columns[col] |
||
793 | |||
794 | if role == _Qt_DisplayRole: |
||
795 | return coldesc.format(instance) |
||
796 | elif role == _Qt_EditRole and isinstance(coldesc, TableModel.Column): |
||
797 | return instance[coldesc.var] |
||
798 | elif role == _Qt_BackgroundRole: |
||
799 | return coldesc.background |
||
800 | return self.color_for_role[coldesc.role] |
||
801 | elif role == _ValueRole and isinstance(coldesc, TableModel.Column): |
||
802 | return instance[coldesc.var] |
||
803 | elif role == _ClassValueRole: |
||
804 | try: |
||
805 | return instance.get_class() |
||
806 | except TypeError: |
||
807 | return None |
||
808 | elif role == _VariableRole and isinstance(coldesc, TableModel.Column): |
||
809 | return coldesc.var |
||
810 | elif role == _DomainRole: |
||
811 | return coldesc.role |
||
812 | elif role == _VariableStatsRole: |
||
813 | return self._stats_for_column(col) |
||
814 | else: |
||
815 | return None |
||
816 | |||
817 | def setData(self, index, value, role): |
||
818 | row, col = self.__sortIndInv[index.row()], index.column() |
||
819 | if role == Qt.EditRole: |
||
820 | try: |
||
821 | self.source[row, col] = value |
||
822 | except (TypeError, IndexError): |
||
823 | return False |
||
824 | else: |
||
825 | self.dataChanged.emit(index, index) |
||
826 | return True |
||
827 | else: |
||
828 | return False |
||
829 | |||
830 | def parent(self, index): |
||
831 | """Reimplemented from `QAbstractTableModel.parent`.""" |
||
832 | return QModelIndex() |
||
833 | |||
834 | def rowCount(self, parent=QModelIndex()): |
||
835 | """Reimplemented from `QAbstractTableModel.rowCount`.""" |
||
836 | return 0 if parent.isValid() else self.__rowCount |
||
837 | |||
838 | def columnCount(self, parent=QModelIndex()): |
||
839 | """Reimplemented from `QAbstractTableModel.columnCount`.""" |
||
840 | return 0 if parent.isValid() else self.__columnCount |
||
841 | |||
842 | def headerData(self, section, orientation, role): |
||
843 | """Reimplemented from `QAbstractTableModel.headerData`.""" |
||
844 | if orientation == Qt.Vertical: |
||
845 | if role == Qt.DisplayRole: |
||
846 | if self.__sortInd is not None: |
||
847 | return int(self.__sortInd[section] + 1) |
||
848 | else: |
||
849 | return int(section + 1) |
||
850 | else: |
||
851 | return None |
||
852 | |||
853 | coldesc = self.columns[section] |
||
854 | if role == Qt.DisplayRole: |
||
855 | if isinstance(coldesc, TableModel.Basket): |
||
856 | return "{...}" |
||
857 | else: |
||
858 | return coldesc.var.name |
||
859 | elif role == Qt.ToolTipRole: |
||
860 | return self._tooltip(coldesc) |
||
861 | elif role == TableModel.VariableRole \ |
||
862 | and isinstance(coldesc, TableModel.Column): |
||
863 | return coldesc.var |
||
864 | elif role == TableModel.VariableStatsRole: |
||
865 | return self._stats_for_column(section) |
||
866 | elif role == TableModel.DomainRole: |
||
867 | return coldesc.role |
||
868 | else: |
||
869 | return None |
||
870 | |||
871 | def _tooltip(self, coldesc): |
||
872 | """ |
||
873 | Return an header tool tip text for an `column` descriptor. |
||
874 | """ |
||
875 | if isinstance(coldesc, TableModel.Basket): |
||
876 | return None |
||
877 | |||
878 | labels = self._labels |
||
879 | variable = coldesc.var |
||
880 | pairs = [(escape(key), escape(str(variable.attributes[key]))) |
||
881 | for key in labels if key in variable.attributes] |
||
882 | tip = "<b>%s</b>" % escape(variable.name) |
||
883 | tip = "<br/>".join([tip] + ["%s = %s" % pair for pair in pairs]) |
||
884 | return tip |
||
885 | |||
886 | def _stats_for_column(self, column): |
||
887 | """ |
||
888 | Return BasicStats for `column` index. |
||
889 | """ |
||
890 | coldesc = self.columns[column] |
||
891 | if isinstance(coldesc, TableModel.Basket): |
||
892 | return None |
||
893 | |||
894 | if self.__stats is None: |
||
895 | self.__stats = datacaching.getCached( |
||
896 | self.source, basic_stats.DomainBasicStats, |
||
897 | (self.source, True) |
||
898 | ) |
||
899 | |||
900 | return self.__stats[coldesc.var] |
||
901 |