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