From 6fca30079621f4bdd7e889212d9f76e108a7d14e Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Mon, 1 Jun 2020 11:10:06 +0100 Subject: [PATCH 01/52] First cut a data_view that can display successfully. --- examples/data_view/array_example.py | 42 ++ pyface/data_view/__init__.py | 0 pyface/data_view/abstract_data_model.py | 180 ++++++++ pyface/data_view/array_data_model.py | 218 +++++++++ pyface/data_view/i_data_view_widget.py | 63 +++ pyface/data_view/index_manager.py | 431 ++++++++++++++++++ pyface/data_view/tests/__init__.py | 0 .../data_view/tests/test_array_data_model.py | 155 +++++++ pyface/data_view/tests/test_index_manager.py | 183 ++++++++ pyface/ui/qt4/data_view/__init__.py | 0 .../ui/qt4/data_view/data_view_item_model.py | 193 ++++++++ pyface/ui/qt4/data_view/data_view_widget.py | 56 +++ pyface/ui/wx/data_view/__init__.py | 0 pyface/ui/wx/data_view/data_view_model.py | 81 ++++ pyface/ui/wx/data_view/data_view_widget.py | 65 +++ 15 files changed, 1667 insertions(+) create mode 100644 examples/data_view/array_example.py create mode 100644 pyface/data_view/__init__.py create mode 100644 pyface/data_view/abstract_data_model.py create mode 100644 pyface/data_view/array_data_model.py create mode 100644 pyface/data_view/i_data_view_widget.py create mode 100644 pyface/data_view/index_manager.py create mode 100644 pyface/data_view/tests/__init__.py create mode 100644 pyface/data_view/tests/test_array_data_model.py create mode 100644 pyface/data_view/tests/test_index_manager.py create mode 100644 pyface/ui/qt4/data_view/__init__.py create mode 100644 pyface/ui/qt4/data_view/data_view_item_model.py create mode 100644 pyface/ui/qt4/data_view/data_view_widget.py create mode 100644 pyface/ui/wx/data_view/__init__.py create mode 100644 pyface/ui/wx/data_view/data_view_model.py create mode 100644 pyface/ui/wx/data_view/data_view_widget.py diff --git a/examples/data_view/array_example.py b/examples/data_view/array_example.py new file mode 100644 index 000000000..4ea370beb --- /dev/null +++ b/examples/data_view/array_example.py @@ -0,0 +1,42 @@ +from traits.api import Array, Instance + +from pyface.api import ApplicationWindow, GUI +from pyface.data_view.array_data_model import ArrayDataModel +from pyface.data_view.i_data_view_widget import IDataViewWidget +from pyface.data_view.data_view_widget import DataViewWidget + + +class MainWindow(ApplicationWindow): + """ The main application window. """ + + data = Array + + data_view = Instance(IDataViewWidget) + + def _create_contents(self, parent): + """ Creates the left hand side or top depending on the style. """ + + self.data_view = DataViewWidget( + parent=parent, + data_model=ArrayDataModel(data=self.data), + #header_visible=False, + ) + self.data_view._create() + return self.data_view.control + + def _data_default(self): + import numpy + return numpy.random.uniform(size=(1000000, 10)) + + +# Application entry point. +if __name__ == "__main__": + # Create the GUI (this does NOT start the GUI event loop). + gui = GUI() + + # Create and open the main window. + window = MainWindow() + window.open() + + # Start the GUI event loop! + gui.start_event_loop() diff --git a/pyface/data_view/__init__.py b/pyface/data_view/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pyface/data_view/abstract_data_model.py b/pyface/data_view/abstract_data_model.py new file mode 100644 index 000000000..8bfedaac4 --- /dev/null +++ b/pyface/data_view/abstract_data_model.py @@ -0,0 +1,180 @@ +# (C) Copyright 2005-2020 Enthought, Inc., Austin, TX +# All rights reserved. +# +# This software is provided without warranty under the terms of the BSD +# license included in LICENSE.txt and may be redistributed only under +# the conditions described in the aforementioned license. The license +# is also available online at http://www.enthought.com/licenses/BSD.txt +# +# Thanks for using Enthought open source! +""" +Abstract Data Model +=================== + +This module provides an ABC for all data view data models. This specifies +the API that the data view widgets expect, and which the underlying +data is adapted to by the concrete implementations. Data models are intended +to be toolkit-independent +""" +from abc import abstractmethod + +from traits.api import ABCHasStrictTraits, Event, Instance + +from .index_manager import AbstractIndexManager + + +class AbstractDataModel(ABCHasStrictTraits): + """ Abstract base class for data models. """ + + #: The index manager that helps convert toolkit indices to data view + #: indices. + index_manager = Instance(AbstractIndexManager) + + #: Event fired when the structure of the data changes. + structure_changed = Event() + + #: Event fired when value changes without changes to structure. + values_changed = Event() + + # Data structure methods + + @abstractmethod + def get_column_count(self, row): + """ How many columns in the row of the data view model. + + The total number of columns in the table is given by the column + count of the Root row. + + Parameters + ---------- + row : sequence of int + The indices of the row as a sequence from root to leaf. + + Returns + ------- + column_count : non-negative int + The number of columns that the row provides. + """ + raise NotImplementedError + + @abstractmethod + def can_have_children(self, row): + """ Whether or not a row can have child rows. + + Parameters + ---------- + row : sequence of int + The indices of the row as a sequence from root to leaf. + + Returns + ------- + can_have_children : bool + Whether or not the row can ever have child rows. + """ + raise NotImplementedError + + @abstractmethod + def get_row_count(self, row): + """ How many child rows the row currently has. + + Parameters + ---------- + row : sequence of int + The indices of the row as a sequence from root to leaf. + + Returns + ------- + row_count : non-negative int + The number of child rows that the row has. + """ + raise NotImplementedError + + # Data value methods + + @abstractmethod + def get_value(self, row, column): + """ Return the Python value for the row and column. + + The values for column headers are returned by calling this method + with row as Root. + + Parameters + ---------- + row : sequence of int + The indices of the row as a sequence from root to leaf. + column : sequence of int + The indices of the column as a sequence of length 1. + + Returns + ------- + value : any + The value represented by the given row and column. + """ + raise NotImplementedError + + @abstractmethod + def set_value(self, row, column, value): + """ Set the Python value for the row and column. + + The values for column headers can be set by calling this method + with row as Root. + + Parameters + ---------- + row : sequence of int + The indices of the row as a sequence from root to leaf. + column : sequence of int + The indices of the column as a sequence of length 1. + value : any + The new value for the given row and column. + + Returns + ------- + success : bool + Whether or not the value was set successfully. + """ + raise NotImplementedError + + @abstractmethod + def get_text(self, row, column): + """ Return the text value for the row and column. + + The text for column headers are returned by calling this method + with row as Root. + + Parameters + ---------- + row : sequence of int + The indices of the row as a sequence from root to leaf. + column : sequence of int + The indices of the column as a sequence of length 1. + + Returns + ------- + text : str + The text to display in the given row and column. + """ + return str(self.get_value(row, column)) + + @abstractmethod + def set_text(self, row, column, text): + """ Set the text value for the row and column. + + The text for column headers can be set by calling this method + with row as Root. + + Parameters + ---------- + row : sequence of int + The indices of the row as a sequence from root to leaf. + column : sequence of int + The indices of the column as a sequence of length 1. + text : str + The new text value for the given row and column. + + Returns + ------- + success : bool + Whether or not the value was set successfully. + """ + raise NotImplementedError diff --git a/pyface/data_view/array_data_model.py b/pyface/data_view/array_data_model.py new file mode 100644 index 000000000..b8f6c52ba --- /dev/null +++ b/pyface/data_view/array_data_model.py @@ -0,0 +1,218 @@ +# (C) Copyright 2005-2020 Enthought, Inc., Austin, TX +# All rights reserved. +# +# This software is provided without warranty under the terms of the BSD +# license included in LICENSE.txt and may be redistributed only under +# the conditions described in the aforementioned license. The license +# is also available online at http://www.enthought.com/licenses/BSD.txt +# +# Thanks for using Enthought open source! +""" +Array Data Model +================ + +This module provides an a concrete implementation of a data model for a 2D +numpy array. +""" +from traits.api import Array, Instance, observe + +from .abstract_data_model import AbstractDataModel +from .index_manager import IntIndexManager + + +class ArrayDataModel(AbstractDataModel): + + #: The array being displayed. + data = Array(shape=(None, None)) + + #: The index manager that helps convert toolkit indices to data view + #: indices. + index_manager = Instance(IntIndexManager, ()) + + # Data structure methods + + def get_column_count(self, row): + """ How many columns in the row of the data view model. + + The total number of columns in the table is given by the column + count of the Root row. + + Parameters + ---------- + row : sequence of int + The indices of the row as a sequence from root to leaf. + + Returns + ------- + column_count : non-negative int + The number of columns that the row provides. + """ + return self.data.shape[-1] + + def can_have_children(self, row): + """ Whether or not a row can have child rows. + + Parameters + ---------- + row : sequence of int + The indices of the row as a sequence from root to leaf. + + Returns + ------- + can_have_children : bool + Whether or not the row can ever have child rows. + """ + if row == []: + return True + return False + + def get_row_count(self, row): + """ Whether or not the row currently has any child rows. + + Subclasses may override this to provide a more direct implementation. + + Parameters + ---------- + row : sequence of int + The indices of the row as a sequence from root to leaf. + + Returns + ------- + has_children : bool + Whether or not the row currently has child rows. + """ + if row == []: + return self.data.shape[0] + return 0 + + # Data value methods + + def get_value(self, row, column): + """ How many child rows the row currently has. + + Parameters + ---------- + row : sequence of int + The indices of the row as a sequence from root to leaf. + + Returns + ------- + row_count : non-negative int + The number of child rows that the row has. + """ + if row == []: + return column[0] + elif column == []: + # XXX not currently used + return row[0] + else: + return self.data[row[0], column[0]] + + def set_value(self, row, column, value): + """ Return the Python value for the row and column. + + The values for column headers are returned by calling this method + with row as Root. + + Parameters + ---------- + row : sequence of int + The indices of the row as a sequence from root to leaf. + column : sequence of int + The indices of the column as a sequence of length 1. + + Returns + ------- + value : any + The value represented by the given row and column. + """ + if row == []: + return False + elif column == []: + # XXX not used + return False + else: + self.data[row[0], column[0]] = value + self.values_changed = ((row, column), (row, column)) + return True + + def get_text(self, row, column): + """ Set the Python value for the row and column. + + The values for column headers can be set by calling this method + with row as Root. + + Parameters + ---------- + row : sequence of int + The indices of the row as a sequence from root to leaf. + column : sequence of int + The indices of the column as a sequence of length 1. + value : any + The new value for the given row and column. + + Returns + ------- + success : bool + Whether or not the value was set successfully. + """ + return str(self.get_value(row, column)) + + def set_text(self, row, column, text): + """ Return the text value for the row and column. + + The text for column headers are returned by calling this method + with row as Root. + + Parameters + ---------- + row : sequence of int + The indices of the row as a sequence from root to leaf. + column : sequence of int + The indices of the column as a sequence of length 1. + + Returns + ------- + text : str + The text to display in the given row and column. + """ + try: + value = self.data.dtype.type(text.strip()) + except ValueError: + return False + return self.set_value(row, column, value) + + def get_style(self, row, column): + """ Set the text value for the row and column. + + The text for column headers can be set by calling this method + with row as Root. + + Parameters + ---------- + row : sequence of int + The indices of the row as a sequence from root to leaf. + column : sequence of int + The indices of the column as a sequence of length 1. + text : str + The new text value for the given row and column. + + Returns + ------- + success : bool + Whether or not the value was set successfully. + """ + raise NotImplementedError + + # data update methods + + @observe('data', dispatch='ui') + def data_updated(self, event): + """ Handle the array being replaced with a new array. """ + if event.new.shape == event.old.shape: + self.values_changed = ( + ([0], [0]), + ([event.old.shape[0]], [event.old.shape[1]]), + ) + else: + self.structure_changed = True diff --git a/pyface/data_view/i_data_view_widget.py b/pyface/data_view/i_data_view_widget.py new file mode 100644 index 000000000..9849afede --- /dev/null +++ b/pyface/data_view/i_data_view_widget.py @@ -0,0 +1,63 @@ +# (C) Copyright 2005-2020 Enthought, Inc., Austin, TX +# All rights reserved. +# +# This software is provided without warranty under the terms of the BSD +# license included in LICENSE.txt and may be redistributed only under +# the conditions described in the aforementioned license. The license +# is also available online at http://www.enthought.com/licenses/BSD.txt +# +# Thanks for using Enthought open source! + +from traits.api import Bool, HasStrictTraits, Instance + +from pyface.data_view.abstract_data_model import AbstractDataModel +from pyface.i_widget import IWidget +from .data_view_item_model import DataViewItemModel + + +class IDataViewWidget(IWidget): + + #: Whether or not the column headers are visible. + header_visible = Bool(True) + + #: The data model for the data view. + data_model = Instance(AbstractDataModel, allow_none=False) + + +class MDataViewWidget(HasStrictTraits): + + #: Whether or not the column headers are visible. + header_visible = Bool(True) + + #: The data model for the data view. + data_model = Instance(AbstractDataModel, allow_none=False) + + def _add_event_listeners(self): + super()._add_event_listeners() + self.observe( + self._header_visible_updated, 'header_visible', dispatch='ui') + + def _remove_event_listeners(self): + self.observe( + self._header_visible_updated, + 'header_visible', + dispatch='ui', + remove=True, + ) + super()._remove_event_listeners() + + def _header_visible_updated(self, event): + """ Observer for header_visible trait. """ + if self.control is not None: + self._set_control_header_visible(event.new) + + def _get_control_header_visible(self): + """ Toolkit specific method to get the visibility of the header. """ + raise NotImplementedError() + + def _set_control_header_visible(self, tooltip): + """ Toolkit specific method to set the visibility of the header. """ + raise NotImplementedError() + + + diff --git a/pyface/data_view/index_manager.py b/pyface/data_view/index_manager.py new file mode 100644 index 000000000..1cd18494f --- /dev/null +++ b/pyface/data_view/index_manager.py @@ -0,0 +1,431 @@ +# (C) Copyright 2005-2020 Enthought, Inc., Austin, TX +# All rights reserved. +# +# This software is provided without warranty under the terms of the BSD +# license included in LICENSE.txt and may be redistributed only under +# the conditions described in the aforementioned license. The license +# is also available online at http://www.enthought.com/licenses/BSD.txt +# +# Thanks for using Enthought open source! +""" +Index Managers +============== + +This module provides a number of classes for efficiently managing the +mapping between different ways of representing indices. To do so, each +index manager provides an intermediate, opaque index object that is +suitable for use in these situations and is guaranteed to have a long +enough life that it will not change or be garbage collected while a C++ +object has a reference to it. + +The wx DataView classes expect to be given an integer id value that is +stable and can be used to return the parent reference id. + +And Qt's ModelView system expects to be given a pointer to an object +that is long-lived (in particular, it will not be garbage-collected +during the lifetime of a QModelIndex) and which can be used to find +the parent object of the current object. + +The default representation of an index from the point of view of the +data view infrastructure is a sequence of integers, giving the index at +each level of the heirarchy. DataViewModel classes can then use these +indices to identify objects in the underlying data model. + +There are three main classes defined in the module: AbstractIndexManager, +IntIndexManager, and TupleIndexManager. + +AbstractIndexManager + An ABC that defines the API + +IntIndexManager + An efficient index manager for non-heirarchical data, such as + lists and arrays. + +TupleIndexManager + An index manager that handles non-heirarchical data while trying + to be fast and memory efficient. + +The two concrete subclasses should be sufficient for most cases, but advanced +users may create their own if for some reason the provided managers do not +work well for a particular situation. Developers who implement this API +need to be mindful of the requirements on the lifetime and identity +constraints required by the various toolkit APIs. + +""" + +from abc import abstractmethod +import typing as t + +from traits.api import ABCHasStrictTraits, Dict, Int, Tuple + + +#: The singular root object for all index managers. +Root = () + + +class AbstractIndexManager(ABCHasStrictTraits): + """ Abstract base class for index managers. + """ + + @abstractmethod + def create_index(self, parent: t.Any, row: int) -> t.Any: + """ Given a parent index and a row number, create an index. + + The internal structure of the index should not matter to + consuming code. However obejcts returned from this method + should persist until the reset method is called. + + Parameters + ---------- + parent : index object + The parent index object. + row : int + The position of the resuling index in the parent's children. + + Returns + ------- + index : index object + The resulting opaque index object. + + Raises + ------ + IndexError + Negative row values raise an IndexError exception. + RuntimeError + If asked to create a persistent index for a parent and row + where that is not possible, a RuntimeError will be raised. + """ + raise NotImplementedError + + @abstractmethod + def get_parent_and_row(self, index: t.Any) -> t.Tuple[t.Any, int]: + """ Given an index object, return the parent index and row. + + Parameters + ---------- + index : index object + The opaque index object. + + Returns + ------- + parent : index object + The parent index object. + row : int + The position of the resuling index in the parent's children. + + Raises + ------ + IndexError + If the Root object is passed as the index, this method will + raise an IndexError, as it has no parent. + """ + raise NotImplementedError + + def from_sequence(self, indices: t.Sequence[int]) -> t.Any: + """ Given a sequence of indices, return the index object. + + The default implementation starts at the root and repeatedly calls + create_index() to find the index at each level, returning the final + value. + + Parameters + ---------- + indices : sequence of int + The row location at each level of the heirarchy. + + Returns + ------- + index : index object + The persistent index object associated with this sequence. + + Raises + ------ + RuntimeError + If asked to create a persistent index for a sequence of indices + where that is not possible, a RuntimeError will be raised. + """ + index = Root + for row in indices: + index = self.create_index(index, row) + return index + + def to_sequence(self, index: t.Any) -> t.List[int]: + """ Given an index, return the corresponding sequence of row values. + + The default implementation repeatedly calls get_parent_and_row() + to walk up the heirarchy and push the row values into the start + of the sequence. + + Parameters + ---------- + index : index object + The opaque index object. + + Returns + ------- + sequence : list of int + The row location at each level of the heirarchy. + """ + result: t.List[int] = [] + while index != Root: + index, row = self.get_parent_and_row(index) + result.insert(0, row) + return result + + @abstractmethod + def from_id(self, id: int) -> t.Any: + """ Given an integer id, return the corresponding index. + + Parameters + ---------- + id : int + An integer object id value. + + Returns + ------- + index : index object + The persistent index object associated with this id. + """ + raise NotImplementedError + + @abstractmethod + def id(self, index: t.Any) -> int: + """ Given an index, return the corresponding id. + + Parameters + ---------- + id : int + An integer object id value. + + Returns + ------- + index : index object + The persistent index object associated with this id. + """ + raise NotImplementedError + + def reset(self): + """ Reset any caches and other state. + + Resettable traits in subclasses are indicated by having + ``can_reset=True`` metadata. This is provided to allow + toolkit code to clear caches to prevent memory leaks when + working with very large tables. + + Care should be taken when calling this method, as Qt may + crash if a QModelIndex is referencing an index that no + longer has a reference in a cache. + + For some IndexManagers, particularly for those which are flat + or static, reset() may do nothing. + """ + resettable_traits = self.trait_names(can_reset=True) + self.reset_traits(resettable_traits) + + +class IntIndexManager(AbstractIndexManager): + """ Efficient IndexManager for non-heirarchical indexes. + + This is a simple index manager for flat data structures. The + index values returned are either the Root, or simple integers + that indicate the position of the index as a child of the root. + + While it cannot handle nested data, this index manager can + operate without having to perform any caching, and so is very + efficient. + """ + + def create_index(self, parent: t.Any, row: int) -> t.Any: + """ Given a parent index and a row number, create an index. + + This method always raises RuntimeError, as the only persistent + index for an IntIndexManager is the Root, which has no parent. + + Parameters + ---------- + parent : index object + The parent index object. + row : non-negative int + The position of the resulting index in the parent's children. + + Returns + ------- + index : index object + The resulting opaque index object. + + Raises + ------ + IndexError + Negative row values raise an IndexError exception. + RuntimeError + The only persistent index for an IntIndexManager is the + root, so this method always raises. + """ + if row < 0: + raise IndexError("Row must be non-negative. Got {}".format(row)) + if parent != Root: + raise RuntimeError( + "{} cannot create persistent index value for {}.".format( + self.__class__.__name__, + (parent, row) + ) + ) + return row + + def get_parent_and_row(self, index: t.Any) -> t.Tuple[t.Any, int]: + """ Given an index object, return the parent index and row. + + Parameters + ---------- + index : index object + The opaque index object. + + Returns + ------- + parent : index object + The parent index object. + row : int + The position of the resuling index in the parent's children. + + Raises + ------ + IndexError + If the Root object is passed as the index, this method will + raise an IndexError, as it has no parent. + """ + if index == Root: + raise IndexError("Root index has no parent.") + return Root, int(index) + + def from_id(self, id: int) -> t.Any: + """ Given an integer id, return the corresponding index. + + Parameters + ---------- + id : int + An integer object id value. + + Returns + ------- + index : index object + The persistent index object associated with this id. + """ + if id == 0: + return Root + return int(id) - 1 + + def id(self, index: t.Any) -> int: + """ Given an index, return the corresponding id. + + Parameters + ---------- + id : int + An integer object id value. + + Returns + ------- + index : index object + The persistent index object associated with this id. + """ + if index == Root: + return 0 + return int(index) + 1 + + +class TupleIndexManager(AbstractIndexManager): + + #: A dictionary that maps tuples to the canonical version of the tuple. + _cache = Dict(Tuple, Tuple, {Root: Root}, can_reset=True) + + #: A dictionary that maps ids to the canonical version of the tuple. + _id_cache = Dict(Int, Tuple, {id(Root): Root}, can_reset=True) + + def create_index(self, parent: t.Any, row: int) -> t.Any: + """ Given a parent index and a row number, create an index. + + This method always raises RuntimeError, as the only persistent + index for an IntIndexManager is the Root, which has no parent. + + Parameters + ---------- + parent : index object + The parent index object. + row : non-negative int + The position of the resulting index in the parent's children. + + Returns + ------- + index : index object + The resulting opaque index object. + + Raises + ------ + IndexError + Negative row values raise an IndexError exception. + RuntimeError + The only persistent index for an IntIndexManager is the + root, so this method always raises. + """ + if row < 0: + raise IndexError("Row must be non-negative. Got {}".format(row)) + + index = (parent, row) + canonical_index = self._cache.setdefault(index, index) + self._id_cache[self.id(canonical_index)] = canonical_index + return canonical_index + + def get_parent_and_row(self, index: t.Any) -> t.Tuple[t.Any, int]: + """ Given an index object, return the parent index and row. + + Parameters + ---------- + index : index object + The opaque index object. + + Returns + ------- + parent : index object + The parent index object. + row : int + The position of the resuling index in the parent's children. + + Raises + ------ + IndexError + If the Root object is passed as the index, this method will + raise an IndexError, as it has no parent. + """ + if index == Root: + raise IndexError("Root index has no parent.") + return index + + def from_id(self, id: int) -> t.Any: + """ Given an integer id, return the corresponding index. + + Parameters + ---------- + id : int + An integer object id value. + + Returns + ------- + index : index object + The persistent index object associated with this id. + """ + return self._id_cache[id] + + def id(self, index: t.Any) -> int: + """ Given an index, return the corresponding id. + + Parameters + ---------- + id : int + An integer object id value. + + Returns + ------- + index : index object + The persistent index object associated with this id. + """ + canonical_index = self._cache.setdefault(index, index) + return id(canonical_index) diff --git a/pyface/data_view/tests/__init__.py b/pyface/data_view/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pyface/data_view/tests/test_array_data_model.py b/pyface/data_view/tests/test_array_data_model.py new file mode 100644 index 000000000..9fb1b192d --- /dev/null +++ b/pyface/data_view/tests/test_array_data_model.py @@ -0,0 +1,155 @@ +# (C) Copyright 2005-2020 Enthought, Inc., Austin, TX +# All rights reserved. +# +# This software is provided without warranty under the terms of the BSD +# license included in LICENSE.txt and may be redistributed only under +# the conditions described in the aforementioned license. The license +# is also available online at http://www.enthought.com/licenses/BSD.txt +# +# Thanks for using Enthought open source! + +from unittest import TestCase + +from traits.testing.unittest_tools import UnittestTools +from traits.testing.optional_dependencies import numpy as np, requires_numpy + +from ..array_data_model import ArrayDataModel + + +@requires_numpy +class TestArrayDataModel(UnittestTools, TestCase): + + def setUp(self): + super().setUp() + self.array = np.arange(15.0).reshape(5, 3) + self.model = ArrayDataModel(data=self.array) + self.values_changed_event = None + self.structure_changed_event = None + self.model.observe(self.model_values_changed, 'values_changed') + self.model.observe(self.model_structure_changed, 'structure_changed') + + def tearDown(self): + self.model.observe( + self.model_values_changed, 'values_changed', remove=True) + self.model.observe( + self.model_structure_changed, 'structure_changed', remove=True) + self.values_changed_event = None + self.structure_changed_event = None + super().tearDown() + + def model_values_changed(self, event): + self.values_changed_event = event + + def model_structure_changed(self, event): + self.structure_changed_event = event + + def test_get_column_count(self): + for row in range(5): + with self.subTest(row=row): + result = self.model.get_column_count([row]) + self.assertEqual(result, 3) + + def test_get_column_count_root(self): + result = self.model.get_column_count([]) + self.assertEqual(result, 3) + + def test_can_have_children(self): + for row in range(5): + with self.subTest(row=row): + result = self.model.can_have_children([row]) + self.assertEqual(result, False) + + def test_can_have_children_root(self): + result = self.model.can_have_children([]) + self.assertEqual(result, True) + + def test_get_row_count(self): + for row in range(5): + with self.subTest(row=row): + result = self.model.get_row_count([row]) + self.assertEqual(result, 0) + + def test_get_row_count_root(self): + result = self.model.get_row_count([]) + self.assertEqual(result, 5) + + def test_get_value(self): + for row in range(5): + for column in range(3): + with self.subTest(row=row, column=column): + result = self.model.get_value([row], [column]) + self.assertEqual(result, self.array[row, column]) + + def test_get_value_root(self): + for column in range(3): + with self.subTest(column=column): + result = self.model.get_value([], [column]) + self.assertEqual(result, column) + + def test_set_value_float(self): + for row in range(5): + for column in range(3): + with self.subTest(row=row, column=column): + value = 6.0 * row + 2 * column + with self.assertTraitChanges(self.model, "values_changed"): + result = self.model.set_value([row], [column], value) + self.assertTrue(result, self.array[row, column]) + self.assertEqual(self.array[row, column], value) + self.assertEqual( + self.values_changed_event.new, + (([row], [column]), ([row], [column])) + ) + + def test_set_value_root(self): + for column in range(3): + with self.subTest(column=column): + result = self.model.set_value([], [column], column+1) + self.assertEqual(result, False) + + def test_get_text(self): + for row in range(5): + for column in range(3): + with self.subTest(row=row, column=column): + result = self.model.get_text([row], [column]) + self.assertEqual(result, str(self.array[row, column])) + + def test_set_value_root(self): + for column in range(3): + with self.subTest(column=column): + result = self.model.set_text([], [column], str(column+1)) + self.assertEqual(result, False) + + def test_set_text_root(self): + for row in range(5): + for column in range(3): + with self.subTest(row=row, column=column): + result = self.model.get_text([row], [column]) + self.assertEqual(result, str(self.array[row, column])) + + def test_set_text_float(self): + for row in range(5): + for column in range(3): + with self.subTest(row=row, column=column): + value = 6.0 * row + 2 * column + text = str(value) + with self.assertTraitChanges(self.model, "values_changed"): + result = self.model.set_text([row], [column], text) + self.assertTrue(result, self.array[row, column]) + self.assertEqual(self.array[row, column], value) + self.assertEqual( + self.values_changed_event.new, + (([row], [column]), ([row], [column])) + ) + + def test_data_updated(self): + with self.assertTraitChanges(self.model, "values_changed"): + self.model.data = 2*self.array + self.assertEqual( + self.values_changed_event.new, + (([0], [0]), ([5], [3])) + ) + + def test_data_updated_new_shape(self): + with self.assertTraitChanges(self.model, "structure_changed"): + self.model.data = 2*self.array.T + self.assertTrue(self.structure_changed_event.new) diff --git a/pyface/data_view/tests/test_index_manager.py b/pyface/data_view/tests/test_index_manager.py new file mode 100644 index 000000000..f3ce400fa --- /dev/null +++ b/pyface/data_view/tests/test_index_manager.py @@ -0,0 +1,183 @@ +# (C) Copyright 2005-2020 Enthought, Inc., Austin, TX +# All rights reserved. +# +# This software is provided without warranty under the terms of the BSD +# license included in LICENSE.txt and may be redistributed only under +# the conditions described in the aforementioned license. The license +# is also available online at http://www.enthought.com/licenses/BSD.txt +# +# Thanks for using Enthought open source! + +from unittest import TestCase + +from ..index_manager import ( + AbstractIndexManager, IntIndexManager, Root, TupleIndexManager, +) + + +class IndexManagerMixin: + + index_manager: AbstractIndexManager + + def test_root_has_no_parent(self): + with self.assertRaises(IndexError): + self.index_manager.get_parent_and_row(Root) + + def test_root_to_sequence(self): + result = self.index_manager.to_sequence(Root) + + self.assertEqual(result, []) + + def test_root_from_sequence(self): + result = self.index_manager.from_sequence([]) + + self.assertIs(result, Root) + + def test_root_id_round_trip(self): + root_id = self.index_manager.id(Root) + result = self.index_manager.from_id(root_id) + + self.assertIs(result, Root) + + def test_simple_sequence_round_trip(self): + sequence = [5] + index = self.index_manager.from_sequence(sequence) + result = self.index_manager.to_sequence(index) + + self.assertEqual(result, sequence) + + def test_simple_sequence_invalid(self): + sequence = [-5] + with self.assertRaises(IndexError): + self.index_manager.from_sequence(sequence) + + def test_simple_sequence_to_parent_row(self): + sequence = [5] + index = self.index_manager.from_sequence(sequence) + result = self.index_manager.get_parent_and_row(index) + + self.assertEqual(result, (Root, 5)) + + def test_simple_row_round_trip(self): + index = self.index_manager.create_index(Root, 5) + result = self.index_manager.get_parent_and_row(index) + + self.assertEqual(result, (Root, 5)) + + def test_simple_row_invalid(self): + with self.assertRaises(IndexError): + self.index_manager.create_index(Root, -5) + + def test_simple_row_to_sequence(self): + index = self.index_manager.create_index(Root, 5) + result = self.index_manager.to_sequence(index) + + self.assertEqual(result, [5]) + + def test_simple_id_round_trip(self): + index = self.index_manager.create_index(Root, 5) + id = self.index_manager.id(index) + result = self.index_manager.from_id(id) + + self.assertEqual(result, index) + + +class TestIntIndexManager(IndexManagerMixin, TestCase): + + def setUp(self): + super().setUp() + self.index_manager = IntIndexManager() + + def tearDown(self): + self.index_manager.reset() + + +class TestTupleIndexManager(IndexManagerMixin, TestCase): + + def setUp(self): + super().setUp() + self.index_manager = TupleIndexManager() + + def tearDown(self): + self.index_manager.reset() + + def test_complex_sequence_round_trip(self): + sequence = [5, 6, 7, 8, 9, 10] + index = self.index_manager.from_sequence(sequence) + result = self.index_manager.to_sequence(index) + + self.assertEqual(result, sequence) + + def test_complex_sequence_identical_index(self): + sequence = [5, 6, 7, 8, 9, 10] + index_1 = self.index_manager.from_sequence(sequence[:]) + index_2 = self.index_manager.from_sequence(sequence[:]) + + self.assertIs(index_1, index_2) + + def test_complex_sequence_to_parent_row(self): + sequence = [5, 6, 7, 8, 9, 10] + index = self.index_manager.from_sequence(sequence) + + parent, row = self.index_manager.get_parent_and_row(index) + + self.assertEqual(row, 10) + self.assertIs( + parent, + self.index_manager.from_sequence([5, 6, 7, 8, 9]) + ) + + def test_complex_index_round_trip(self): + sequence = [5, 6, 7, 8, 9, 10] + + parent = Root + for depth, row in enumerate(sequence): + with self.subTest(depth=depth): + index = self.index_manager.create_index(parent, row) + result = self.index_manager.get_parent_and_row(index) + self.assertIs(result[0], parent) + self.assertEqual(result[1], row) + parent = index + + def test_complex_index_create_index_identical(self): + sequence = [5, 6, 7, 8, 9, 10] + + parent = Root + for depth, row in enumerate(sequence): + with self.subTest(depth=depth): + index_1 = self.index_manager.create_index(parent, row) + index_2 = self.index_manager.create_index(parent, row) + self.assertIs(index_1, index_2) + parent = index_1 + + def test_complex_index_to_sequence(self): + sequence = [5, 6, 7, 8, 9, 10] + parent = Root + for depth, row in enumerate(sequence): + with self.subTest(depth=depth): + index = self.index_manager.create_index(parent, row) + result = self.index_manager.to_sequence(index) + self.assertEquals(result, sequence[:depth+1]) + parent = index + + def test_complex_index_sequence_round_trip(self): + parent = Root + for depth, row in enumerate([5, 6, 7, 8, 9, 10]): + with self.subTest(depth=depth): + index = self.index_manager.create_index(parent, row) + sequence = self.index_manager.to_sequence(index) + result = self.index_manager.from_sequence(sequence) + self.assertIs(result, index) + parent = index + + def test_complex_index_id_round_trip(self): + sequence = [5, 6, 7, 8, 9, 10] + parent = Root + for depth, row in enumerate(sequence): + with self.subTest(depth=depth): + index = self.index_manager.create_index(parent, row) + id = self.index_manager.id(index) + self.assertIsInstance(id, int) + result = self.index_manager.from_id(id) + self.assertIs(result, index) + parent = index diff --git a/pyface/ui/qt4/data_view/__init__.py b/pyface/ui/qt4/data_view/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pyface/ui/qt4/data_view/data_view_item_model.py b/pyface/ui/qt4/data_view/data_view_item_model.py new file mode 100644 index 000000000..5a38a71f6 --- /dev/null +++ b/pyface/ui/qt4/data_view/data_view_item_model.py @@ -0,0 +1,193 @@ +# (C) Copyright 2005-2020 Enthought, Inc., Austin, TX +# All rights reserved. +# +# This software is provided without warranty under the terms of the BSD +# license included in LICENSE.txt and may be redistributed only under +# the conditions described in the aforementioned license. The license +# is also available online at http://www.enthought.com/licenses/BSD.txt +# +# Thanks for using Enthought open source! + +import logging + +from pyface.qt.QtCore import QAbstractItemModel, QModelIndex, Qt +from pyface.data_view.index_manager import Root +from pyface.data_view.abstract_data_model import AbstractDataModel + + +logger = logging.getLogger(__name__) + +# XXX This file is scaffolding and may need to be rewritten + + +class DataViewItemModel(QAbstractItemModel): + + def __init__(self, model, parent=None): + super().__init__(parent) + self.model = model + + @property + def model(self): + return self._model + + @model.setter + def model(self, model: AbstractDataModel): + if hasattr(self, '_model'): + # disconnect trait listeners + self._model.observe( + self.on_structure_changed, + 'structure_changed', + remove=True, + ) + self._model.observe( + self.on_values_changed, + 'values_changed', + remove=True, + ) + + self.beginResetModel() + self._model = model + self.endResetModel() + else: + # model is being initialized + self._model = model + + # hook up trait listeners + self._model.observe( + self.on_structure_changed, + 'structure_changed', + ) + self._model.observe( + self.on_values_changed, + 'values_changed', + ) + + # model event listeners + + def on_structure_changed(self, event): + self.beginResetModel() + self.endResetModel() + + def on_values_changed(self, event): + if event.top == [] and event.bottom == []: + # this is a column header change + self.headerDataChanged(event.left[0], event.right[0]) + elif event.left == [] and event.right == []: + # this is a row header change + # XXX this is currently not supported and not needed + pass + else: + top = [] + bottom = [] + for top_row, bottom_row in zip(event.new.top, event.new.bottom): + top.append(top_row) + bottom.append(bottom_row) + if top_row != bottom_row: + break + + top_left = self._to_model_index(top, event.left) + bottom_right = self._to_model_index(bottom, event.right) + self.dataChanged(top_left, bottom_right) + + # Structure methods + + def parent(self, index): + if not index.isValid(): + return QModelIndex() + parent = index.internalPointer() + if parent == Root: + return QModelIndex() + + grandparent, row = self.model.index_manager.get_parent_and_row(parent) + return self.createIndex(row, 0, grandparent) + + def index(self, row, column, parent): + if parent.isValid(): + parent_index = self.model.index_manager.create_index( + parent.internalPointer(), + parent.row(), + ) + else: + parent_index = Root + index = self.createIndex(row, column, parent_index) + return index + + def rowCount(self, index): + row_index = self._to_row_index(index) + try: + if self.model.can_have_children(row_index): + return self.model.get_row_count(row_index) + except Exception: + logger.exception("Error in rowCount") + + return 0 + + def columnCount(self, index): + row_index = self._to_row_index(index) + try: + return self.model.get_column_count(row_index) + except Exception: + logger.exception("Error in columnCount") + + # Data methods + + def flags(self, index): + row = self._to_row_index(index) + column = self._to_column_index(index) + + flags = Qt.ItemIsEnabled + if not self.model.can_have_children(row): + flags |= Qt.ItemNeverHasChildren + + return flags + + def data(self, index, role=Qt.DisplayRole): + row = self._to_row_index(index) + column = self._to_column_index(index) + + if role == Qt.DisplayRole: + return self.model.get_text(row, column) + + return None + + def headerData(self, section, orientation, role=Qt.DisplayRole): + if orientation == Qt.Horizontal: + row = [] + column = [section] + else: + # XXX not currently used, but here for symmetry and completeness + row = [section] + column = [] + if role == Qt.DisplayRole: + return self.model.get_text(row, column) + + + # Private utility methods + + def _to_row_index(self, index): + if not index.isValid(): + row_index = [] + else: + parent = index.internalPointer() + if parent == Root: + row_index = [] + else: + row_index = self.model.index_manager.to_sequence(parent) + row_index.append(index.row()) + return row_index + + def _to_column_index(self, index): + if not index.isValid(): + return [] + else: + return [index.column()] + + def _to_model_index(self, row_index, column_index): + if row_index == Root: + return QModelIndex() + index = self.model.index_manager.from_sequence(row_index[:-1]) + row = row_index[-1] + column = column_index[0] + + model_index = self.createIndex(row, column, index) + diff --git a/pyface/ui/qt4/data_view/data_view_widget.py b/pyface/ui/qt4/data_view/data_view_widget.py new file mode 100644 index 000000000..8f75443df --- /dev/null +++ b/pyface/ui/qt4/data_view/data_view_widget.py @@ -0,0 +1,56 @@ +# (C) Copyright 2005-2020 Enthought, Inc., Austin, TX +# All rights reserved. +# +# This software is provided without warranty under the terms of the BSD +# license included in LICENSE.txt and may be redistributed only under +# the conditions described in the aforementioned license. The license +# is also available online at http://www.enthought.com/licenses/BSD.txt +# +# Thanks for using Enthought open source! + +from traits.api import Bool, Instance, observe, provides + +from pyface.qt.QtCore import QAbstractItemModel +from pyface.qt.QtGui import QTreeView +from pyface.data_view.abstract_data_model import AbstractDataModel +from pyface.data_view.i_data_view_widget import ( + IDataViewWidget, MDataViewWidget +) +from pyface.ui.qt4.widget import Widget +from .data_view_item_model import DataViewItemModel + +# XXX This file is scaffolding and may need to be rewritten + + +@provides(IDataViewWidget) +class DataViewWidget(MDataViewWidget, Widget): + + _item_model = Instance(QAbstractItemModel) + + def _create_control(self, parent): + self._create_item_model() + + control = QTreeView(parent) + control.setUniformRowHeights(True) + control.setModel(self._item_model) + control.setHeaderHidden(not self.header_visible) + return control + + def _create_item_model(self): + self._item_model = DataViewItemModel(self.data_model) + + def _get_control_header_visible(self): + """ Toolkit specific method to get the control's tooltip. """ + return not self.control.isHeaderHidden() + + def _set_control_header_visible(self, tooltip): + """ Toolkit specific method to set the control's tooltip. """ + self.control.setHeaderHidden(not tooltip) + + @observe('data_model', dispatch='ui') + def update_item_model(self, event): + if self._item_model is not None: + self._item_model.model = event.new + + + diff --git a/pyface/ui/wx/data_view/__init__.py b/pyface/ui/wx/data_view/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pyface/ui/wx/data_view/data_view_model.py b/pyface/ui/wx/data_view/data_view_model.py new file mode 100644 index 000000000..d1c5b872d --- /dev/null +++ b/pyface/ui/wx/data_view/data_view_model.py @@ -0,0 +1,81 @@ + + +from pyface.data_view.index_manager import Root +from wx.dataview import DataViewItem, DataViewModel as wxDataViewModel + + +# XXX This file is scaffolding and may need to be rewritten or expanded + +class DataViewModel(wxDataViewModel): + + def __init__(self, model): + super().__init__() + self._model = model + + @property + def model(self): + return self._model + + def GetParent(self, item): + index = self._to_index(item) + if index == Root: + return None + parent, row = self.model.index_manager.get_parent_and_row(index) + parent_id = self.model.index_manager.id(parent) + if parent_id == 0: + return None + return DataViewItem(parent_id) + + def GetChildren(self, item, children): + index = self._to_index(item) + row_index = self.model.index_manager.to_sequence(index) + n_children = self.model.get_row_count(row_index) + for i in range(n_children): + child_index = self.model.index_manager.create_index(index, i) + child_id = self.model.index_manager.id(child_index) + children.append(DataViewItem(child_id)) + return n_children + + def IsContainer(self, item): + row_index = self._to_row_index(item) + return self.model.can_have_children(row_index) + + def HasContainerColumns(self, item): + return item.GetID() is not None + + def HasChildren(self, item): + row_index = self._to_row_index(item) + return self.model.has_child_rows(row_index) + + def GetValue(self, item, column): + row_index = self._to_row_index(item) + column_index = [column] + value = self.model.get_text(row_index, column_index) + return value + + def SetValue(self, value, item, column): + row_index = self._to_row_index(item) + column_index = [column] + try: + self.model.set_text(row_index, column_index, value) + except Exception as exc: + print(exc) + # XXX log it + return False + return True + + def GetColumnCount(self): + return self.model.get_column_count([]) + + def _to_row_index(self, item): + id = item.GetID() + if id is None: + id = -1 + index = self.model.index_manager.from_id(id) + return self.model.index_manager.to_sequence(index) + + def _to_index(self, item): + id = item.GetID() + if id is None: + id = 0 + return self.model.index_manager.from_id(id) diff --git a/pyface/ui/wx/data_view/data_view_widget.py b/pyface/ui/wx/data_view/data_view_widget.py new file mode 100644 index 000000000..a5ae046f4 --- /dev/null +++ b/pyface/ui/wx/data_view/data_view_widget.py @@ -0,0 +1,65 @@ +# (C) Copyright 2005-2020 Enthought, Inc., Austin, TX +# All rights reserved. +# +# This software is provided without warranty under the terms of the BSD +# license included in LICENSE.txt and may be redistributed only under +# the conditions described in the aforementioned license. The license +# is also available online at http://www.enthought.com/licenses/BSD.txt +# +# Thanks for using Enthought open source! + +from traits.api import Bool, Instance, observe, provides + +from wx.dataview import ( + DataViewCtrl, DataViewModel as wxDataViewModel, DATAVIEW_CELL_EDITABLE +) +from pyface.data_view.abstract_data_model import AbstractDataModel +from pyface.data_view.i_data_view_widget import ( + IDataViewWidget, MDataViewWidget +) +from pyface.ui.wx.widget import Widget +from .data_view_model import DataViewModel + + +# XXX This file is scaffolding and may need to be rewritten + +@provides(IDataViewWidget) +class DataViewWidget(MDataViewWidget, Widget): + + _item_model = Instance(wxDataViewModel) + + def _create_control(self, parent): + self._create_item_model() + + control = DataViewCtrl(parent) + control.AssociateModel(self._item_model) + # required for wxPython refcounting system + self._item_model.DecRef() + + # create columns for view + for column in range(self._item_model.GetColumnCount()): + control.AppendTextColumn( + self._item_model.model.get_text([], [column]), + column, + mode=DATAVIEW_CELL_EDITABLE, + ) + return control + + def _create_item_model(self): + self._item_model = DataViewModel(self.data_model) + + def _get_control_header_visible(self): + """ Toolkit specific method to get the control's tooltip. """ + #return not self.control.isHeaderHidden() + + def _set_control_header_visible(self, tooltip): + """ Toolkit specific method to set the control's tooltip. """ + #self.control.setHeaderHidden(not tooltip) + + @observe('data_model', dispatch='ui') + def update_item_model(self, event): + if self._item_model is not None: + self._item_model.model = event.new + + + From 70ba5f0ac7867037fcd59a3b29973b3c0d095e61 Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Thu, 4 Jun 2020 09:46:30 +0100 Subject: [PATCH 02/52] Add a column-based data model and corresponding example. --- examples/data_view/array_example.py | 2 +- examples/data_view/object_column_example.py | 296 ++++++++++++++++++++ pyface/data_view/abstract_data_model.py | 104 +++++++ pyface/data_view/column_data_model.py | 137 +++++++++ pyface/data_view/index_manager.py | 8 +- pyface/ui/wx/data_view/data_view_model.py | 10 +- 6 files changed, 548 insertions(+), 9 deletions(-) create mode 100644 examples/data_view/object_column_example.py create mode 100644 pyface/data_view/column_data_model.py diff --git a/examples/data_view/array_example.py b/examples/data_view/array_example.py index 4ea370beb..d7ea8dcff 100644 --- a/examples/data_view/array_example.py +++ b/examples/data_view/array_example.py @@ -26,7 +26,7 @@ def _create_contents(self, parent): def _data_default(self): import numpy - return numpy.random.uniform(size=(1000000, 10)) + return numpy.random.uniform(size=(100000, 10)) # Application entry point. diff --git a/examples/data_view/object_column_example.py b/examples/data_view/object_column_example.py new file mode 100644 index 000000000..a10f60222 --- /dev/null +++ b/examples/data_view/object_column_example.py @@ -0,0 +1,296 @@ +from random import choice, randint + +from traits.api import HasStrictTraits, Instance, Int, Str, List + +from pyface.api import ApplicationWindow, GUI +from pyface.data_view.column_data_model import ( + AbstractRowInfo, ColumnDataModel, ObjectRowInfo +) +from pyface.data_view.i_data_view_widget import IDataViewWidget +from pyface.data_view.data_view_widget import DataViewWidget + + +class Address(HasStrictTraits): + + street = Str + + city = Str + + country = Str + + +class Person(HasStrictTraits): + + name = Str + + age = Int + + address = Instance(Address) + + +row_info = ObjectRowInfo( + title='People', + value='name', + rows=[ + ObjectRowInfo(title="Age", value="age"), + ObjectRowInfo( + title="Address", + rows=[ + ObjectRowInfo(title="Street", value="address.street"), + ObjectRowInfo(title="City", value="address.city"), + ObjectRowInfo(title="Country", value="address.country"), + ], + ), + ], +) + + +class MainWindow(ApplicationWindow): + """ The main application window. """ + + data = List(Instance(Person)) + + row_info = Instance(AbstractRowInfo) + + data_view = Instance(IDataViewWidget) + + def _create_contents(self, parent): + """ Creates the left hand side or top depending on the style. """ + + self.data_view = DataViewWidget( + parent=parent, + data_model=ColumnDataModel( + data=self.data, + row_info=self.row_info + ), + #header_visible=False, + ) + self.data_view._create() + return self.data_view.control + + def _data_default(self): + import numpy + return numpy.random.uniform(size=(100000, 10)) + +male_names = [ + 'Michael', + 'Edward', + 'Timothy', + 'James', + 'George', + 'Ralph', + 'David', + 'Martin', + 'Bryce', + 'Richard', + 'Eric', + 'Travis', + 'Robert', + 'Bryan', + 'Alan', + 'Harold', + 'John', + 'Stephen', + 'Gael', + 'Frederic', + 'Eli', + 'Scott', + 'Samuel', + 'Alexander', + 'Tobias', + 'Sven', + 'Peter', + 'Albert', + 'Thomas', + 'Horatio', + 'Julius', + 'Henry', + 'Walter', + 'Woodrow', + 'Dylan', + 'Elmer'] + +female_names = [ + 'Leah', + 'Jaya', + 'Katrina', + 'Vibha', + 'Diane', + 'Lisa', + 'Jean', + 'Alice', + 'Rebecca', + 'Delia', + 'Christine', + 'Marie', + 'Dorothy', + 'Ellen', + 'Victoria', + 'Elizabeth', + 'Margaret', + 'Joyce', + 'Sally', + 'Ethel', + 'Esther', + 'Suzanne', + 'Monica', + 'Hortense', + 'Samantha', + 'Tabitha', + 'Judith', + 'Ariel', + 'Helen', + 'Mary', + 'Jane', + 'Janet', + 'Jennifer', + 'Rita', + 'Rena', + 'Rianna'] + +all_names = male_names + female_names + +male_name = lambda: choice(male_names) +female_name = lambda: choice(female_names) +any_name = lambda: choice(all_names) +age = lambda: randint(15, 72) + +family_name = lambda: choice(['Jones', + 'Smith', + 'Thompson', + 'Hayes', + 'Thomas', + 'Boyle', + "O'Reilly", + 'Lebowski', + 'Lennon', + 'Starr', + 'McCartney', + 'Harrison', + 'Harrelson', + 'Steinbeck', + 'Rand', + 'Hemingway', + 'Zhivago', + 'Clemens', + 'Heinlien', + 'Farmer', + 'Niven', + 'Van Vogt', + 'Sturbridge', + 'Washington', + 'Adams', + 'Bush', + 'Kennedy', + 'Ford', + 'Lincoln', + 'Jackson', + 'Johnson', + 'Eisenhower', + 'Truman', + 'Roosevelt', + 'Wilson', + 'Coolidge', + 'Mack', + 'Moon', + 'Monroe', + 'Springsteen', + 'Rigby', + "O'Neil", + 'Philips', + 'Clinton', + 'Clapton', + 'Santana', + 'Midler', + 'Flack', + 'Conner', + 'Bond', + 'Seinfeld', + 'Costanza', + 'Kramer', + 'Falk', + 'Moore', + 'Cramdon', + 'Baird', + 'Baer', + 'Spears', + 'Simmons', + 'Roberts', + 'Michaels', + 'Stuart', + 'Montague', + 'Miller']) + +street = lambda: '%d %s %s' % (randint(11, + 999), + choice(['Spring', + 'Summer', + 'Moonlight', + 'Winding', + 'Windy', + 'Whispering', + 'Falling', + 'Roaring', + 'Hummingbird', + 'Mockingbird', + 'Bluebird', + 'Robin', + 'Babbling', + 'Cedar', + 'Pine', + 'Ash', + 'Maple', + 'Oak', + 'Birch', + 'Cherry', + 'Blossom', + 'Rosewood', + 'Apple', + 'Peach', + 'Blackberry', + 'Strawberry', + 'Starlight', + 'Wilderness', + 'Dappled', + 'Beaver', + 'Acorn', + 'Pecan', + 'Pheasant', + 'Owl']), + choice(['Way', + 'Lane', + 'Boulevard', + 'Street', + 'Drive', + 'Circle', + 'Avenue', + 'Trail'])) + +city = lambda: choice(['Boston', 'Cambridge', ]) +country = lambda: choice(['USA', 'UK']) + +people = [ + Person( + name='%s %s' % (any_name(), family_name()), + age=age(), + address=Address( + street=street(), + city=city(), + country=country(), + ), + ) + for i in range(100) +] + + +# Application entry point. +if __name__ == "__main__": + # Create the GUI (this does NOT start the GUI event loop). + gui = GUI() + + # Create and open the main window. + window = MainWindow(data=people, row_info=row_info) + window.open() + + # Start the GUI event loop! + gui.start_event_loop() diff --git a/pyface/data_view/abstract_data_model.py b/pyface/data_view/abstract_data_model.py index 8bfedaac4..8541465b1 100644 --- a/pyface/data_view/abstract_data_model.py +++ b/pyface/data_view/abstract_data_model.py @@ -36,6 +36,9 @@ class AbstractDataModel(ABCHasStrictTraits): #: Event fired when value changes without changes to structure. values_changed = Event() + #: Event fired when selection changes. + selection = Event() + # Data structure methods @abstractmethod @@ -135,6 +138,8 @@ def set_value(self, row, column, value): """ raise NotImplementedError + # Data channels + @abstractmethod def get_text(self, row, column): """ Return the text value for the row and column. @@ -178,3 +183,102 @@ def set_text(self, row, column, text): Whether or not the value was set successfully. """ raise NotImplementedError + +''' + @abstractmethod + def get_checked(self, row, column): + return None + + @abstractmethod + def set_checked(self, row, column, checked): + return None + + @abstractmethod + def get_color(self, row, column): + return None + + @abstractmethod + def get_image(self, row, column): + return None + + @abstractmethod + def get_description(self, row, column): + return None + + # interaction methods + + # XXX these should perhaps live in a separate class + + def get_enabled(self, row, column): + """ Whether or not the given cell is enabled for user interaction + + Note that if the entire control is disabled then individual cells will + still be disabled independent of the value returned by this method. + + Parameters + ---------- + row : sequence of int + The indices of the row as a sequence from root to leaf. + column : sequence of int + The indices of the column as a sequence of length 1. + + Returns + ------- + enabled : bool + Whether the cell allows user interaction. + """ + return True + + def get_editable(self, row, column): + return False + + @abstractmethod + def get_column_editor(self, column): + """ Return editor information for a column + """ + # XXX needs to be a richer object + # eg should have bounds for spinbox, choices for Combo etc. + return "text" + + @abstractmethod + def get_cell_editor(self, row, column): + """ Return editor information for the row and column. + """ + return self.get_column_type(column) + + def get_can_check(self, row, column): + return False + + def get_can_drag(self, row, column): + return False + + def get_can_drop(self, row, column): + return False + + def get_can_select(self, row, column): + return True + + def get_selection(self): + pass + + def set_selection(self, items, ignore_missing=False): + """ Set the current selection to the given items. + + If ``ignore_missing`` is ``True``, items that are not available in the + selection provider are silently ignored. If it is ``False`` (default), + an :class:`~.ValueError` should be raised. + + Parameters + ---------- + items : list + List of items to be selected. + + ignore_missing : bool + If ``False`` (default), the provider raises an exception if any + of the items in ``items`` is not available to be selected. + Otherwise, missing elements are silently ignored, and the rest + is selected. + """ + pass + +''' \ No newline at end of file diff --git a/pyface/data_view/column_data_model.py b/pyface/data_view/column_data_model.py new file mode 100644 index 000000000..27cd193a7 --- /dev/null +++ b/pyface/data_view/column_data_model.py @@ -0,0 +1,137 @@ +from abc import abstractmethod + +from traits.api import ( + ABCHasStrictTraits, Callable, HasTraits, Instance, List, Str +) +from traits.trait_base import xgetattr, xsetattr + +from .abstract_data_model import AbstractDataModel +from .index_manager import TupleIndexManager + + +def id(obj): + return obj + + +class AbstractRowInfo(ABCHasStrictTraits): + """ Configuration for a data row in a ColumnDataModel. + """ + + #: The text to display in the first column. + title = Str + + #: The child rows of this row, if any. + rows = List(Instance('AbstractRowInfo')) + + #: The method to format the value as a string. + format = Callable(str) + + #: The method to evaluate a string into a value. + evaluate = Callable(id) + + @abstractmethod + def get_value(self, obj): + raise NotImplementedError + + def set_value(self, obj): + return False + + def get_text(self, obj): + value = self.get_value(obj) + if value is None: + return '' + return self.format(value) + + def set_text(self, obj, text): + return self.set_value(obj, self.evaluate(text)) + + +class ObjectRowInfo(AbstractRowInfo): + + #: The extended trait name of the trait holding the value. + value = Str + + def get_value(self, obj): + return xgetattr(obj, self.value, None) + + def set_value(self, obj, value): + xsetattr(obj, self.value, value) + return True + + +class DictRowInfo(AbstractRowInfo): + + #: The extended trait name of the dictionary holding the values. + value = Str + + #: The key holding the value. + key = Str + + def get_value(self, obj): + data = xgetattr(obj, self.value, None) + return data.get(self.key, None) + + def set_value(self, obj, value): + data = xgetattr(obj, self.value, None) + data[self.key] = value + return True + + +class ColumnDataModel(AbstractDataModel): + + #: A list of objects to display in columns. + data = List(Instance(HasTraits)) + + #: An object which describes how to map data for each row. + row_info = Instance(AbstractRowInfo) + + #: The index manager that helps convert toolkit indices to data view + #: indices. + index_manager = Instance(TupleIndexManager, ()) + + def get_column_count(self, row): + return len(self.data) + 1 + + def can_have_children(self, row): + if not row: + return True + row_info = self._row_info_object(row) + return len(row_info.rows) != 0 + + def get_row_count(self, row): + row_info = self._row_info_object(row) + return len(row_info.rows) + + def get_value(self, row, column): + row_info = self._row_info_object(row) + if column[0] == 0: + return row_info.title + obj = self.data[column[0]-1] + return row_info.get_value(obj) + + def set_value(self, row, column, value): + row_info = self._row_info_object(row) + if column[0] == 0: + return False + obj = self.data[column[0]-1] + return row_info.set_value(obj, value) + + def get_text(self, row, column): + row_info = self._row_info_object(row) + if column[0] == 0: + return row_info.title + obj = self.data[column[0]-1] + return row_info.get_text(obj) + + def set_text(self, row, column, text): + row_info = self._row_info_object(row) + if column[0] == 0: + return False + obj = self.data[column[0]-1] + return row_info.set_value(obj, text) + + def _row_info_object(self, row): + row_info = self.row_info + for index in row: + row_info = row_info.rows[index] + return row_info diff --git a/pyface/data_view/index_manager.py b/pyface/data_view/index_manager.py index 1cd18494f..e8fcdd9a8 100644 --- a/pyface/data_view/index_manager.py +++ b/pyface/data_view/index_manager.py @@ -312,7 +312,7 @@ def from_id(self, id: int) -> t.Any: """ if id == 0: return Root - return int(id) - 1 + return id - 1 def id(self, index: t.Any) -> int: """ Given an index, return the corresponding id. @@ -329,7 +329,7 @@ def id(self, index: t.Any) -> int: """ if index == Root: return 0 - return int(index) + 1 + return index + 1 class TupleIndexManager(AbstractIndexManager): @@ -338,7 +338,7 @@ class TupleIndexManager(AbstractIndexManager): _cache = Dict(Tuple, Tuple, {Root: Root}, can_reset=True) #: A dictionary that maps ids to the canonical version of the tuple. - _id_cache = Dict(Int, Tuple, {id(Root): Root}, can_reset=True) + _id_cache = Dict(Int, Tuple, {0: Root}, can_reset=True) def create_index(self, parent: t.Any, row: int) -> t.Any: """ Given a parent index and a row number, create an index. @@ -427,5 +427,7 @@ def id(self, index: t.Any) -> int: index : index object The persistent index object associated with this id. """ + if index == Root: + return 0 canonical_index = self._cache.setdefault(index, index) return id(canonical_index) diff --git a/pyface/ui/wx/data_view/data_view_model.py b/pyface/ui/wx/data_view/data_view_model.py index d1c5b872d..5bb51305e 100644 --- a/pyface/ui/wx/data_view/data_view_model.py +++ b/pyface/ui/wx/data_view/data_view_model.py @@ -50,8 +50,8 @@ def HasChildren(self, item): def GetValue(self, item, column): row_index = self._to_row_index(item) column_index = [column] - value = self.model.get_text(row_index, column_index) - return value + text = self.model.get_text(row_index, column_index) + return text def SetValue(self, value, item, column): row_index = self._to_row_index(item) @@ -70,12 +70,12 @@ def GetColumnCount(self): def _to_row_index(self, item): id = item.GetID() if id is None: - id = -1 - index = self.model.index_manager.from_id(id) + id = 0 + index = self.model.index_manager.from_id(int(id)) return self.model.index_manager.to_sequence(index) def _to_index(self, item): id = item.GetID() if id is None: id = 0 - return self.model.index_manager.from_id(id) + return self.model.index_manager.from_id(int(id)) From fa3c8924af450feea4b7b29debef5b8a78ef6699 Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Thu, 4 Jun 2020 09:49:04 +0100 Subject: [PATCH 03/52] Add missing file. --- pyface/data_view/data_view_widget.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 pyface/data_view/data_view_widget.py diff --git a/pyface/data_view/data_view_widget.py b/pyface/data_view/data_view_widget.py new file mode 100644 index 000000000..434ca7827 --- /dev/null +++ b/pyface/data_view/data_view_widget.py @@ -0,0 +1,13 @@ +# (C) Copyright 2005-2020 Enthought, Inc., Austin, TX +# All rights reserved. +# +# This software is provided without warranty under the terms of the BSD +# license included in LICENSE.txt and may be redistributed only under +# the conditions described in the aforementioned license. The license +# is also available online at http://www.enthought.com/licenses/BSD.txt +# +# Thanks for using Enthought open source! + +from pyface.toolkit import toolkit_object + +DataViewWidget = toolkit_object('data_view.data_view_widget:DataViewWidget') \ No newline at end of file From 3bfaaaad8a76aefae21d88a16d3db02d043fe46a Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Tue, 16 Jun 2020 09:37:26 +0100 Subject: [PATCH 04/52] Target code for basic data view implementation. --- examples/data_view/array_example.py | 4 +- ...ct_column_example.py => column_example.py} | 39 +++- pyface/data_view/abstract_data_model.py | 145 +++---------- pyface/data_view/abstract_value_type.py | 104 +++++++++ pyface/data_view/column_data_model.py | 137 ------------ pyface/data_view/data_models/__init__.py | 0 pyface/data_view/data_models/api.py | 2 + .../{ => data_models}/array_data_model.py | 170 ++++++++------- .../data_models/column_data_model.py | 204 ++++++++++++++++++ .../data_view/data_models/tests/__init__.py | 0 .../tests/test_array_data_model.py | 157 ++++++++++++++ .../data_view/tests/test_array_data_model.py | 155 ------------- pyface/data_view/tests/test_index_manager.py | 4 +- pyface/data_view/value_types/__init__.py | 0 pyface/data_view/value_types/api.py | 4 + pyface/data_view/value_types/numeric_value.py | 78 +++++++ pyface/data_view/value_types/testing.py | 0 .../data_view/value_types/tests/__init__.py | 0 .../value_types/tests/test_numeric_value.py | 96 +++++++++ .../value_types/tests/test_text_value.py | 46 ++++ pyface/data_view/value_types/text_value.py | 11 + .../ui/qt4/data_view/data_view_item_model.py | 78 +++++-- pyface/ui/wx/data_view/data_view_model.py | 98 ++++++++- pyface/ui/wx/data_view/data_view_widget.py | 27 ++- 24 files changed, 1028 insertions(+), 531 deletions(-) rename examples/data_view/{object_column_example.py => column_example.py} (87%) create mode 100644 pyface/data_view/abstract_value_type.py delete mode 100644 pyface/data_view/column_data_model.py create mode 100644 pyface/data_view/data_models/__init__.py create mode 100644 pyface/data_view/data_models/api.py rename pyface/data_view/{ => data_models}/array_data_model.py (55%) create mode 100644 pyface/data_view/data_models/column_data_model.py create mode 100644 pyface/data_view/data_models/tests/__init__.py create mode 100644 pyface/data_view/data_models/tests/test_array_data_model.py delete mode 100644 pyface/data_view/tests/test_array_data_model.py create mode 100644 pyface/data_view/value_types/__init__.py create mode 100644 pyface/data_view/value_types/api.py create mode 100644 pyface/data_view/value_types/numeric_value.py create mode 100644 pyface/data_view/value_types/testing.py create mode 100644 pyface/data_view/value_types/tests/__init__.py create mode 100644 pyface/data_view/value_types/tests/test_numeric_value.py create mode 100644 pyface/data_view/value_types/tests/test_text_value.py create mode 100644 pyface/data_view/value_types/text_value.py diff --git a/examples/data_view/array_example.py b/examples/data_view/array_example.py index d7ea8dcff..14eda9b80 100644 --- a/examples/data_view/array_example.py +++ b/examples/data_view/array_example.py @@ -1,7 +1,7 @@ from traits.api import Array, Instance from pyface.api import ApplicationWindow, GUI -from pyface.data_view.array_data_model import ArrayDataModel +from pyface.data_view.data_models.array_data_model import ArrayDataModel from pyface.data_view.i_data_view_widget import IDataViewWidget from pyface.data_view.data_view_widget import DataViewWidget @@ -26,7 +26,7 @@ def _create_contents(self, parent): def _data_default(self): import numpy - return numpy.random.uniform(size=(100000, 10)) + return numpy.random.uniform(size=(10000, 10, 10))*1000000 # Application entry point. diff --git a/examples/data_view/object_column_example.py b/examples/data_view/column_example.py similarity index 87% rename from examples/data_view/object_column_example.py rename to examples/data_view/column_example.py index a10f60222..c32c3552e 100644 --- a/examples/data_view/object_column_example.py +++ b/examples/data_view/column_example.py @@ -3,11 +3,13 @@ from traits.api import HasStrictTraits, Instance, Int, Str, List from pyface.api import ApplicationWindow, GUI -from pyface.data_view.column_data_model import ( - AbstractRowInfo, ColumnDataModel, ObjectRowInfo +from pyface.data_view.abstract_value_type import AbstractValueType, none_value +from pyface.data_view.data_models.column_data_model import ( + AbstractRowInfo, ColumnDataModel, HasTraitsRowInfo ) from pyface.data_view.i_data_view_widget import IDataViewWidget from pyface.data_view.data_view_widget import DataViewWidget +from pyface.data_view.value_types.api import IntValue, TextValue class Address(HasStrictTraits): @@ -28,17 +30,36 @@ class Person(HasStrictTraits): address = Instance(Address) -row_info = ObjectRowInfo( +row_info = HasTraitsRowInfo( title='People', value='name', + value_type=TextValue(), rows=[ - ObjectRowInfo(title="Age", value="age"), - ObjectRowInfo( + HasTraitsRowInfo( + title="Age", + value="age", + value_type=IntValue(minimum=0), + ), + HasTraitsRowInfo( title="Address", + value_type=none_value, + value='address', rows=[ - ObjectRowInfo(title="Street", value="address.street"), - ObjectRowInfo(title="City", value="address.city"), - ObjectRowInfo(title="Country", value="address.country"), + HasTraitsRowInfo( + title="Street", + value="address.street", + value_type=TextValue(), + ), + HasTraitsRowInfo( + title="City", + value="address.city", + value_type=TextValue(), + ), + HasTraitsRowInfo( + title="Country", + value="address.country", + value_type=TextValue(), + ), ], ), ], @@ -61,7 +82,7 @@ def _create_contents(self, parent): parent=parent, data_model=ColumnDataModel( data=self.data, - row_info=self.row_info + row_info=self.row_info, ), #header_visible=False, ) diff --git a/pyface/data_view/abstract_data_model.py b/pyface/data_view/abstract_data_model.py index 8541465b1..d66233d22 100644 --- a/pyface/data_view/abstract_data_model.py +++ b/pyface/data_view/abstract_data_model.py @@ -141,10 +141,10 @@ def set_value(self, row, column, value): # Data channels @abstractmethod - def get_text(self, row, column): + def get_value_type(self, row, column): """ Return the text value for the row and column. - The text for column headers are returned by calling this method + The value type for column headers are returned by calling this method with row as Root. Parameters @@ -159,126 +159,45 @@ def get_text(self, row, column): text : str The text to display in the given row and column. """ - return str(self.get_value(row, column)) - - @abstractmethod - def set_text(self, row, column, text): - """ Set the text value for the row and column. - - The text for column headers can be set by calling this method - with row as Root. - - Parameters - ---------- - row : sequence of int - The indices of the row as a sequence from root to leaf. - column : sequence of int - The indices of the column as a sequence of length 1. - text : str - The new text value for the given row and column. - - Returns - ------- - success : bool - Whether or not the value was set successfully. - """ raise NotImplementedError -''' - @abstractmethod - def get_checked(self, row, column): - return None - - @abstractmethod - def set_checked(self, row, column, checked): - return None - - @abstractmethod - def get_color(self, row, column): - return None - - @abstractmethod - def get_image(self, row, column): - return None - - @abstractmethod - def get_description(self, row, column): - return None - - # interaction methods - - # XXX these should perhaps live in a separate class - - def get_enabled(self, row, column): - """ Whether or not the given cell is enabled for user interaction - - Note that if the entire control is disabled then individual cells will - still be disabled independent of the value returned by this method. + def iter_rows(self, start=[]): + """ Iterator that yields rows in preorder. Parameters ---------- - row : sequence of int - The indices of the row as a sequence from root to leaf. - column : sequence of int - The indices of the column as a sequence of length 1. - - Returns - ------- - enabled : bool - Whether the cell allows user interaction. + start : row index + The row to start at. The iterator will yeild the row and all + child rows. + + Yields + ------ + row_index + The current row index. """ - return True + yield start + if self.can_have_children(start): + for row in range(self.get_row_count(start)): + yield from self.iter_rows(start + [row]) - def get_editable(self, row, column): - return False + def iter_items(self, start_row=[]): + """ Iterator that yields rows and columns in preorder. - @abstractmethod - def get_column_editor(self, column): - """ Return editor information for a column - """ - # XXX needs to be a richer object - # eg should have bounds for spinbox, choices for Combo etc. - return "text" - - @abstractmethod - def get_cell_editor(self, row, column): - """ Return editor information for the row and column. - """ - return self.get_column_type(column) - - def get_can_check(self, row, column): - return False - - def get_can_drag(self, row, column): - return False - - def get_can_drop(self, row, column): - return False - - def get_can_select(self, row, column): - return True - - def get_selection(self): - pass - - def set_selection(self, items, ignore_missing=False): - """ Set the current selection to the given items. - - If ``ignore_missing`` is ``True``, items that are not available in the - selection provider are silently ignored. If it is ``False`` (default), - an :class:`~.ValueError` should be raised. + Columns are iterated in order. Parameters ---------- - items : list - List of items to be selected. - - ignore_missing : bool - If ``False`` (default), the provider raises an exception if any - of the items in ``items`` is not available to be selected. - Otherwise, missing elements are silently ignored, and the rest - is selected. + start : row index + The row to start at. The iterator will yeild the row and all + child rows. + + Yields + ------ + row_index, column_index + The current row and column indices. """ - pass - -''' \ No newline at end of file + for row in self.iter_rows(): + if row != []: + yield row, [] + for column in range(self.get_column_count(row)): + yield row, [column] diff --git a/pyface/data_view/abstract_value_type.py b/pyface/data_view/abstract_value_type.py new file mode 100644 index 000000000..ec671d6f2 --- /dev/null +++ b/pyface/data_view/abstract_value_type.py @@ -0,0 +1,104 @@ +from abc import abstractmethod +import locale +from math import inf +import sys + +from traits.api import ( + ABCHasStrictTraits, Any, Bool, Callable, Enum, Event, Int, Str, Float, + observe +) +from pyface.ui_traits import Image + + +default_max = sys.maxsize +default_min = -sys.maxsize + 1 + + +class AbstractValueType(ABCHasStrictTraits): + """ A value type converts raw data into data channels. + + The data channels are editor value, text, color, image, and description. + The data channels are used by other parts of the code to produce the actual + display. + """ + + #: Fired when a change occurs that requires updating values. + updated = Event + + def is_valid(self, model, row, column, value): + return True + + def get_is_editable(self, model, row, column): + return False + + def get_editable(self, model, row, column): + return model.get_value(row, column) + + def set_editable(self, model, row, column, value): + return model.set_value(row, column, value) + + def has_text(self, model, row, column): + return self.get_text(model, row, column) != "" + + def get_text(self, model, row, column): + return str(model.get_value(row, column)) + + def set_text(self, model, row, column, text): + """ Default behaviour does not allow setting the text. """ + return False + + @observe('+update') + def update_value_type(self, event=None): + """ Fire update event when marked traits change. """ + self.updated = True + + +class BaseValueType(AbstractValueType): + + #: Whether or not there is an editable value. + is_editable = Bool(True, update=True) + + def get_is_editable(self, model, row, column): + return self.is_editable + + def get_editable(self, model, row, column): + return model.get_value(row, column) + + def set_editable(self, model, row, column, value): + return model.set_value(row, column, value) + + def has_text(self, model, row, column): + return True + + def get_text(self, model, row, column): + return self.text + + def set_text(self, model, row, column, text): + """ Default behaviour does not allow setting the text. """ + return False + + +class NoneValue(AbstractValueType): + + def is_valid(self, model, row, column, value): + return True + + def get_is_editable(self, model, row, column): + return False + + def has_text(self, model, row, column): + return False + + +none_value = NoneValue() + +class ConstantValueType(AbstractValueType): + + text = Str(update=True) + + def has_text(self, model, row, column): + return self.text != "" + + def get_text(self, model, row, column): + return self.text + diff --git a/pyface/data_view/column_data_model.py b/pyface/data_view/column_data_model.py deleted file mode 100644 index 27cd193a7..000000000 --- a/pyface/data_view/column_data_model.py +++ /dev/null @@ -1,137 +0,0 @@ -from abc import abstractmethod - -from traits.api import ( - ABCHasStrictTraits, Callable, HasTraits, Instance, List, Str -) -from traits.trait_base import xgetattr, xsetattr - -from .abstract_data_model import AbstractDataModel -from .index_manager import TupleIndexManager - - -def id(obj): - return obj - - -class AbstractRowInfo(ABCHasStrictTraits): - """ Configuration for a data row in a ColumnDataModel. - """ - - #: The text to display in the first column. - title = Str - - #: The child rows of this row, if any. - rows = List(Instance('AbstractRowInfo')) - - #: The method to format the value as a string. - format = Callable(str) - - #: The method to evaluate a string into a value. - evaluate = Callable(id) - - @abstractmethod - def get_value(self, obj): - raise NotImplementedError - - def set_value(self, obj): - return False - - def get_text(self, obj): - value = self.get_value(obj) - if value is None: - return '' - return self.format(value) - - def set_text(self, obj, text): - return self.set_value(obj, self.evaluate(text)) - - -class ObjectRowInfo(AbstractRowInfo): - - #: The extended trait name of the trait holding the value. - value = Str - - def get_value(self, obj): - return xgetattr(obj, self.value, None) - - def set_value(self, obj, value): - xsetattr(obj, self.value, value) - return True - - -class DictRowInfo(AbstractRowInfo): - - #: The extended trait name of the dictionary holding the values. - value = Str - - #: The key holding the value. - key = Str - - def get_value(self, obj): - data = xgetattr(obj, self.value, None) - return data.get(self.key, None) - - def set_value(self, obj, value): - data = xgetattr(obj, self.value, None) - data[self.key] = value - return True - - -class ColumnDataModel(AbstractDataModel): - - #: A list of objects to display in columns. - data = List(Instance(HasTraits)) - - #: An object which describes how to map data for each row. - row_info = Instance(AbstractRowInfo) - - #: The index manager that helps convert toolkit indices to data view - #: indices. - index_manager = Instance(TupleIndexManager, ()) - - def get_column_count(self, row): - return len(self.data) + 1 - - def can_have_children(self, row): - if not row: - return True - row_info = self._row_info_object(row) - return len(row_info.rows) != 0 - - def get_row_count(self, row): - row_info = self._row_info_object(row) - return len(row_info.rows) - - def get_value(self, row, column): - row_info = self._row_info_object(row) - if column[0] == 0: - return row_info.title - obj = self.data[column[0]-1] - return row_info.get_value(obj) - - def set_value(self, row, column, value): - row_info = self._row_info_object(row) - if column[0] == 0: - return False - obj = self.data[column[0]-1] - return row_info.set_value(obj, value) - - def get_text(self, row, column): - row_info = self._row_info_object(row) - if column[0] == 0: - return row_info.title - obj = self.data[column[0]-1] - return row_info.get_text(obj) - - def set_text(self, row, column, text): - row_info = self._row_info_object(row) - if column[0] == 0: - return False - obj = self.data[column[0]-1] - return row_info.set_value(obj, text) - - def _row_info_object(self, row): - row_info = self.row_info - for index in row: - row_info = row_info.rows[index] - return row_info diff --git a/pyface/data_view/data_models/__init__.py b/pyface/data_view/data_models/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pyface/data_view/data_models/api.py b/pyface/data_view/data_models/api.py new file mode 100644 index 000000000..9104a74f0 --- /dev/null +++ b/pyface/data_view/data_models/api.py @@ -0,0 +1,2 @@ +from .array_data_model import ArrayDataModel +from .column_data_model import ColumnDataModel \ No newline at end of file diff --git a/pyface/data_view/array_data_model.py b/pyface/data_view/data_models/array_data_model.py similarity index 55% rename from pyface/data_view/array_data_model.py rename to pyface/data_view/data_models/array_data_model.py index b8f6c52ba..9f9a811c2 100644 --- a/pyface/data_view/array_data_model.py +++ b/pyface/data_view/data_models/array_data_model.py @@ -16,18 +16,46 @@ """ from traits.api import Array, Instance, observe -from .abstract_data_model import AbstractDataModel -from .index_manager import IntIndexManager +from pyface.data_view.abstract_data_model import AbstractDataModel +from pyface.data_view.abstract_value_type import ( + AbstractValueType, ConstantValueType, none_value +) +from pyface.data_view.value_types.api import FloatValue, IntValue, TextValue +from pyface.data_view.index_manager import TupleIndexManager class ArrayDataModel(AbstractDataModel): #: The array being displayed. - data = Array(shape=(None, None)) + data = Array() #: The index manager that helps convert toolkit indices to data view #: indices. - index_manager = Instance(IntIndexManager, ()) + index_manager = Instance(TupleIndexManager, args=()) + + #: The value type of the column titles. + header_label_type = Instance( + AbstractValueType, + factory=ConstantValueType, + kw={'text': "Index"}, + ) + + #: The value type of the column titles. + column_header_type = Instance( + AbstractValueType, + factory=IntValue, + kw={'is_editable': False}, + ) + + #: The value type of the column titles. + row_header_type = Instance( + AbstractValueType, + factory=IntValue, + kw={'is_editable': False}, + ) + + #: The type of value being displayed in the data model. + value_type = Instance(AbstractValueType) # Data structure methods @@ -62,7 +90,7 @@ def can_have_children(self, row): can_have_children : bool Whether or not the row can ever have child rows. """ - if row == []: + if len(row) < len(self.data.shape) - 1: return True return False @@ -81,8 +109,8 @@ def get_row_count(self, row): has_children : bool Whether or not the row currently has child rows. """ - if row == []: - return self.data.shape[0] + if len(row) < len(self.data.shape) - 1: + return self.data.shape[len(row)] return 0 # Data value methods @@ -104,9 +132,12 @@ def get_value(self, row, column): return column[0] elif column == []: # XXX not currently used - return row[0] + return row[-1] else: - return self.data[row[0], column[0]] + index = tuple(row + column) + if len(index) != len(self.data.shape): + return 0 + return self.data[index] def set_value(self, row, column, value): """ Return the Python value for the row and column. @@ -129,80 +160,25 @@ def set_value(self, row, column, value): if row == []: return False elif column == []: - # XXX not used return False else: - self.data[row[0], column[0]] = value - self.values_changed = ((row, column), (row, column)) + index = tuple(row + column) + self.data[index] = value + self.values_changed = (row, column, row, column) return True - def get_text(self, row, column): - """ Set the Python value for the row and column. - - The values for column headers can be set by calling this method - with row as Root. - - Parameters - ---------- - row : sequence of int - The indices of the row as a sequence from root to leaf. - column : sequence of int - The indices of the column as a sequence of length 1. - value : any - The new value for the given row and column. - - Returns - ------- - success : bool - Whether or not the value was set successfully. - """ - return str(self.get_value(row, column)) - - def set_text(self, row, column, text): - """ Return the text value for the row and column. - - The text for column headers are returned by calling this method - with row as Root. - - Parameters - ---------- - row : sequence of int - The indices of the row as a sequence from root to leaf. - column : sequence of int - The indices of the column as a sequence of length 1. - - Returns - ------- - text : str - The text to display in the given row and column. - """ - try: - value = self.data.dtype.type(text.strip()) - except ValueError: - return False - return self.set_value(row, column, value) - - def get_style(self, row, column): - """ Set the text value for the row and column. - - The text for column headers can be set by calling this method - with row as Root. - - Parameters - ---------- - row : sequence of int - The indices of the row as a sequence from root to leaf. - column : sequence of int - The indices of the column as a sequence of length 1. - text : str - The new text value for the given row and column. - - Returns - ------- - success : bool - Whether or not the value was set successfully. - """ - raise NotImplementedError + def get_value_type(self, row, column): + if row == []: + if column == []: + return self.header_label_type + return self.column_header_type + elif column == []: + # XXX not currently used + return self.row_header_type + elif len(row) < len(self.data.shape) - 1: + return none_value + else: + return self.value_type # data update methods @@ -211,8 +187,40 @@ def data_updated(self, event): """ Handle the array being replaced with a new array. """ if event.new.shape == event.old.shape: self.values_changed = ( - ([0], [0]), - ([event.old.shape[0]], [event.old.shape[1]]), + ([0], [0], [event.old.shape[0]], [event.old.shape[1]]) ) else: self.structure_changed = True + + @observe('value_type.updated', dispatch='ui') + def value_type_updated(self, event): + """ Handle the value type being updated. """ + self.values_changed = ( + ([0], [0], [self.data.shape[0]], [self.data.shape[1]]) + ) + + @observe('column_header_type.updated', dispatch='ui') + def column_header_type_updated(self, event): + """ Handle the header type being updated. """ + self.values_changed = ( + ([], [0], [], [self.data.shape[1]]) + ) + + @observe('row_header_type.updated', dispatch='ui') + def value_header_type_updated(self, event): + """ Handle the header type being updated. """ + self.values_changed = ( + ([0], [], [self.data.shape[0]], []) + ) + + def _value_type_default(self): + import numpy as np + scalar_type = self.data.dtype + if np.issubdtype(scalar_type, np.integer): + return IntValue() + elif np.issubdtype(scalar_type, np.floating): + return FloatValue() + elif np.issubdtype(scalar_type, np.character): + return TextValue() + + return TextValue(is_editable=False) \ No newline at end of file diff --git a/pyface/data_view/data_models/column_data_model.py b/pyface/data_view/data_models/column_data_model.py new file mode 100644 index 000000000..65edc82d1 --- /dev/null +++ b/pyface/data_view/data_models/column_data_model.py @@ -0,0 +1,204 @@ +from abc import abstractmethod + +from traits.api import ( + ABCHasStrictTraits, Callable, ComparisonMode, Event, HasTraits, Instance, + List, Str, Tuple, observe +) +from traits.trait_base import xgetattr, xsetattr + +from pyface.data_view.abstract_data_model import AbstractDataModel +from pyface.data_view.abstract_value_type import AbstractValueType +from pyface.data_view.index_manager import TupleIndexManager +from pyface.data_view.value_types.api import TextValue + + +def id(obj): + return obj + + +class AbstractRowInfo(ABCHasStrictTraits): + """ Configuration for a data row in a ColumnDataModel. + """ + + #: The text to display in the first column. + title = Str() + + #: The child rows of this row, if any. + rows = List( + Instance('AbstractRowInfo'), + comparison_mode=ComparisonMode.identity, + ) + + #: The value type of the data stored in this row. + title_type = Instance( + AbstractValueType, + factory=TextValue, + kw={'is_editable': False}, + ) + + #: The value type of the data stored in this row. + value_type = Instance(AbstractValueType) + + #: An event fired when the row or its children update. The payload is + #: a tuple of whether the title or value changed (or both), and the + #: row_index affected. + updated = Event + + def __iter__(self): + yield self + for row in self.rows: + yield from row + + @abstractmethod + def get_value(self, obj): + raise NotImplementedError + + @abstractmethod + def can_set_value(self, obj): + raise NotImplementedError + + def set_value(self, obj): + return False + + @abstractmethod + def get_observable(self, obj): + raise NotImplementedError + + # trait observers + + @observe('title,title_type.updated', dispatch='ui') + def title_updated(self, event): + self.updated = (self, 'title', []) + + @observe('value_type.updated', dispatch='ui') + def value_type_updated(self, event): + self.updated = (self, 'value', []) + + @observe('rows.items', dispatch='ui') + def rows_updated(self, event): + self.updated = (self, 'rows', []) + + @observe('rows:items:updated', dispatch='ui') + def row_item_updated(self, event): + row = event.object + row_info, part, row_index = event.new + row_index = [self.rows.index(row)] + row_index + self.updated = (row_info, part, row_index) + + +class HasTraitsRowInfo(AbstractRowInfo): + """ RowInfo that presents a named trait value. + """ + + #: The extended trait name of the trait holding the value. + value = Str() + + def get_value(self, obj): + return xgetattr(obj, self.value, None) + + def can_set_value(self, obj): + return self.value != '' + + def set_value(self, obj, value): + if not self.value: + return False + xsetattr(obj, self.value, value) + return True + + def get_observable(self): + return self.value + + @observe('value', dispatch='ui') + def value_type_updated(self, event): + self.updated = (self, 'value', []) + + +class DictRowInfo(AbstractRowInfo): + """ RowInfo that presents an item in a dictionary. + + The attribute ``value`` should reference a dictionary trait on a + has traits object. + """ + + #: The extended trait name of the dictionary holding the values. + value = Str() + + #: The key holding the value. + key = Str() + + def get_value(self, obj): + data = xgetattr(obj, self.value, None) + return data.get(self.key, None) + + def can_set_value(self, obj): + return self.value != '' + + def set_value(self, obj, value): + data = xgetattr(obj, self.value, None) + data[self.key] = value + return True + + def get_observable(self): + return self.value + '.items' + + @observe('value,key', dispatch='ui') + def value_type_updated(self, event): + self.updated = (self, 'value', []) + + +class ColumnDataModel(AbstractDataModel): + """ A data model that presents a list of objects as columns. + """ + + #: A list of objects to display in columns. + data = List( + Instance(HasTraits), + comparison_mode=ComparisonMode.identity, + ) + + #: An object which describes how to map data for each row. + row_info = Instance(AbstractRowInfo) + + #: The index manager that helps convert toolkit indices to data view + #: indices. + index_manager = Instance(TupleIndexManager, args=()) + + def get_column_count(self, row): + return len(self.data) + + def can_have_children(self, row): + if len(row) == 0: + return True + row_info = self._row_info_object(row) + return len(row_info.rows) != 0 + + def get_row_count(self, row): + row_info = self._row_info_object(row) + return len(row_info.rows) + + def get_value(self, row, column): + row_info = self._row_info_object(row) + if len(column) == 0: + return row_info.title + obj = self.data[column[0]] + return row_info.get_value(obj) + + def set_value(self, row, column, value): + row_info = self._row_info_object(row) + if len(column) == 0: + return False + obj = self.data[column[0]] + return row_info.set_value(obj, value) + + def get_value_type(self, row, column): + row_info = self._row_info_object(row) + if len(column) == 0: + return row_info.title_type + else: + return row_info.value_type + + def _row_info_object(self, row): + row_info = self.row_info + for index in row: + row_info = row_info.rows[index] + return row_info diff --git a/pyface/data_view/data_models/tests/__init__.py b/pyface/data_view/data_models/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pyface/data_view/data_models/tests/test_array_data_model.py b/pyface/data_view/data_models/tests/test_array_data_model.py new file mode 100644 index 000000000..ee9c48ad1 --- /dev/null +++ b/pyface/data_view/data_models/tests/test_array_data_model.py @@ -0,0 +1,157 @@ +# (C) Copyright 2005-2020 Enthought, Inc., Austin, TX +# All rights reserved. +# +# This software is provided without warranty under the terms of the BSD +# license included in LICENSE.txt and may be redistributed only under +# the conditions described in the aforementioned license. The license +# is also available online at http://www.enthought.com/licenses/BSD.txt +# +# Thanks for using Enthought open source! + +from unittest import TestCase, expectedFailure + +from traits.testing.unittest_tools import UnittestTools +from traits.testing.optional_dependencies import numpy as np, requires_numpy + +from pyface.data_view.abstract_value_type import AbstractValueType +from pyface.data_view.value_types.api import FloatValue, IntValue, TextValue +from ..array_data_model import ArrayDataModel + + +@requires_numpy +class TestArrayDataModel(UnittestTools, TestCase): + + def setUp(self): + super().setUp() + self.array = np.arange(15.0).reshape(5, 3) + self.model = ArrayDataModel(data=self.array) + self.values_changed_event = None + self.structure_changed_event = None + self.model.observe(self.model_values_changed, 'values_changed') + self.model.observe(self.model_structure_changed, 'structure_changed') + + def tearDown(self): + self.model.observe( + self.model_values_changed, 'values_changed', remove=True) + self.model.observe( + self.model_structure_changed, 'structure_changed', remove=True) + self.values_changed_event = None + self.structure_changed_event = None + super().tearDown() + + def model_values_changed(self, event): + self.values_changed_event = event + + def model_structure_changed(self, event): + self.structure_changed_event = event + + def test_get_column_count(self): + for row in self.model.iter_rows(): + with self.subTest(row=row): + result = self.model.get_column_count(row) + self.assertEqual(result, 3) + + def test_can_have_children(self): + for row in self.model.iter_rows(): + with self.subTest(row=row): + result = self.model.can_have_children(row) + if len(row) == 0: + self.assertEqual(result, True) + else: + self.assertEqual(result, False) + + def test_get_row_count(self): + for row in self.model.iter_rows(): + with self.subTest(row=row): + result = self.model.get_row_count(row) + if len(row) == 0: + self.assertEqual(result, 5) + else: + self.assertEqual(result, 0) + + def test_get_value(self): + for row, column in self.model.iter_items(): + with self.subTest(row=row, column=column): + result = self.model.get_value(row, column) + if row == []: + self.assertEqual(result, column[0]) + elif column == []: + self.assertEqual(result, row[0]) + else: + self.assertEqual(result, self.array[row[0], column[0]]) + + def test_set_value(self): + for row, column in self.model.iter_items(): + with self.subTest(row=row, column=column): + if row == []: + result = self.model.set_value(row, column, column[0] + 1) + self.assertFalse(result) + elif column == []: + result = self.model.set_value(row, column, row[0] + 1) + self.assertFalse(result) + else: + value = 6.0 * row[0] + 2 * column[0] + with self.assertTraitChanges(self.model, "values_changed"): + result = self.model.set_value(row, column, value) + self.assertTrue(result) + self.assertEqual(self.array[row[0], column[0]], value) + self.assertEqual( + self.values_changed_event.new, + (row, column, row, column) + ) + + def test_get_value_type(self): + for row, column in self.model.iter_items(): + with self.subTest(row=row, column=column): + result = self.model.get_value_type(row, column) + if row == []: + self.assertIsInstance(result, AbstractValueType) + self.assertIs(result, self.model.column_header_type) + elif column == []: + self.assertIsInstance(result, AbstractValueType) + self.assertIs(result, self.model.row_header_type) + else: + self.assertIsInstance(result, AbstractValueType) + self.assertIs(result, self.model.value_type) + + def test_data_updated(self): + with self.assertTraitChanges(self.model, "values_changed"): + self.model.data = 2 * self.array + self.assertEqual( + self.values_changed_event.new, + ([0], [0], [5], [3]) + ) + + def test_data_updated_new_shape(self): + with self.assertTraitChanges(self.model, "structure_changed"): + self.model.data = 2 * self.array.T + self.assertTrue(self.structure_changed_event.new) + + def test_type_updated(self): + with self.assertTraitChanges(self.model, "values_changed"): + self.model.value_type = IntValue() + self.assertEqual( + self.values_changed_event.new, + ([0], [0], [5], [3]) + ) + + def test_type_attribute_updated(self): + with self.assertTraitChanges(self.model, "values_changed"): + self.model.value_type.is_editable = False + self.assertEqual( + self.values_changed_event.new, + ([0], [0], [5], [3]) + ) + + def test_default_value_type(self): + data = np.arange(15).reshape(5, 3) + model = ArrayDataModel(data=data) + self.assertIsInstance(model.value_type, IntValue) + + data = np.arange(15.0).reshape(5, 3) + model = ArrayDataModel(data=data) + self.assertIsInstance(model.value_type, FloatValue) + + data = np.array([['a', 'b', 'c'], ['e', 'f', 'g']]) + model = ArrayDataModel(data=data) + self.assertIsInstance(model.value_type, TextValue) diff --git a/pyface/data_view/tests/test_array_data_model.py b/pyface/data_view/tests/test_array_data_model.py deleted file mode 100644 index 9fb1b192d..000000000 --- a/pyface/data_view/tests/test_array_data_model.py +++ /dev/null @@ -1,155 +0,0 @@ -# (C) Copyright 2005-2020 Enthought, Inc., Austin, TX -# All rights reserved. -# -# This software is provided without warranty under the terms of the BSD -# license included in LICENSE.txt and may be redistributed only under -# the conditions described in the aforementioned license. The license -# is also available online at http://www.enthought.com/licenses/BSD.txt -# -# Thanks for using Enthought open source! - -from unittest import TestCase - -from traits.testing.unittest_tools import UnittestTools -from traits.testing.optional_dependencies import numpy as np, requires_numpy - -from ..array_data_model import ArrayDataModel - - -@requires_numpy -class TestArrayDataModel(UnittestTools, TestCase): - - def setUp(self): - super().setUp() - self.array = np.arange(15.0).reshape(5, 3) - self.model = ArrayDataModel(data=self.array) - self.values_changed_event = None - self.structure_changed_event = None - self.model.observe(self.model_values_changed, 'values_changed') - self.model.observe(self.model_structure_changed, 'structure_changed') - - def tearDown(self): - self.model.observe( - self.model_values_changed, 'values_changed', remove=True) - self.model.observe( - self.model_structure_changed, 'structure_changed', remove=True) - self.values_changed_event = None - self.structure_changed_event = None - super().tearDown() - - def model_values_changed(self, event): - self.values_changed_event = event - - def model_structure_changed(self, event): - self.structure_changed_event = event - - def test_get_column_count(self): - for row in range(5): - with self.subTest(row=row): - result = self.model.get_column_count([row]) - self.assertEqual(result, 3) - - def test_get_column_count_root(self): - result = self.model.get_column_count([]) - self.assertEqual(result, 3) - - def test_can_have_children(self): - for row in range(5): - with self.subTest(row=row): - result = self.model.can_have_children([row]) - self.assertEqual(result, False) - - def test_can_have_children_root(self): - result = self.model.can_have_children([]) - self.assertEqual(result, True) - - def test_get_row_count(self): - for row in range(5): - with self.subTest(row=row): - result = self.model.get_row_count([row]) - self.assertEqual(result, 0) - - def test_get_row_count_root(self): - result = self.model.get_row_count([]) - self.assertEqual(result, 5) - - def test_get_value(self): - for row in range(5): - for column in range(3): - with self.subTest(row=row, column=column): - result = self.model.get_value([row], [column]) - self.assertEqual(result, self.array[row, column]) - - def test_get_value_root(self): - for column in range(3): - with self.subTest(column=column): - result = self.model.get_value([], [column]) - self.assertEqual(result, column) - - def test_set_value_float(self): - for row in range(5): - for column in range(3): - with self.subTest(row=row, column=column): - value = 6.0 * row + 2 * column - with self.assertTraitChanges(self.model, "values_changed"): - result = self.model.set_value([row], [column], value) - self.assertTrue(result, self.array[row, column]) - self.assertEqual(self.array[row, column], value) - self.assertEqual( - self.values_changed_event.new, - (([row], [column]), ([row], [column])) - ) - - def test_set_value_root(self): - for column in range(3): - with self.subTest(column=column): - result = self.model.set_value([], [column], column+1) - self.assertEqual(result, False) - - def test_get_text(self): - for row in range(5): - for column in range(3): - with self.subTest(row=row, column=column): - result = self.model.get_text([row], [column]) - self.assertEqual(result, str(self.array[row, column])) - - def test_set_value_root(self): - for column in range(3): - with self.subTest(column=column): - result = self.model.set_text([], [column], str(column+1)) - self.assertEqual(result, False) - - def test_set_text_root(self): - for row in range(5): - for column in range(3): - with self.subTest(row=row, column=column): - result = self.model.get_text([row], [column]) - self.assertEqual(result, str(self.array[row, column])) - - def test_set_text_float(self): - for row in range(5): - for column in range(3): - with self.subTest(row=row, column=column): - value = 6.0 * row + 2 * column - text = str(value) - with self.assertTraitChanges(self.model, "values_changed"): - result = self.model.set_text([row], [column], text) - self.assertTrue(result, self.array[row, column]) - self.assertEqual(self.array[row, column], value) - self.assertEqual( - self.values_changed_event.new, - (([row], [column]), ([row], [column])) - ) - - def test_data_updated(self): - with self.assertTraitChanges(self.model, "values_changed"): - self.model.data = 2*self.array - self.assertEqual( - self.values_changed_event.new, - (([0], [0]), ([5], [3])) - ) - - def test_data_updated_new_shape(self): - with self.assertTraitChanges(self.model, "structure_changed"): - self.model.data = 2*self.array.T - self.assertTrue(self.structure_changed_event.new) diff --git a/pyface/data_view/tests/test_index_manager.py b/pyface/data_view/tests/test_index_manager.py index f3ce400fa..bf82cd091 100644 --- a/pyface/data_view/tests/test_index_manager.py +++ b/pyface/data_view/tests/test_index_manager.py @@ -17,8 +17,6 @@ class IndexManagerMixin: - index_manager: AbstractIndexManager - def test_root_has_no_parent(self): with self.assertRaises(IndexError): self.index_manager.get_parent_and_row(Root) @@ -157,7 +155,7 @@ def test_complex_index_to_sequence(self): with self.subTest(depth=depth): index = self.index_manager.create_index(parent, row) result = self.index_manager.to_sequence(index) - self.assertEquals(result, sequence[:depth+1]) + self.assertEqual(result, sequence[:depth+1]) parent = index def test_complex_index_sequence_round_trip(self): diff --git a/pyface/data_view/value_types/__init__.py b/pyface/data_view/value_types/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pyface/data_view/value_types/api.py b/pyface/data_view/value_types/api.py new file mode 100644 index 000000000..60c3ab0df --- /dev/null +++ b/pyface/data_view/value_types/api.py @@ -0,0 +1,4 @@ + + +from .numeric_value import FloatValue, IntValue, NumericValue +from .text_value import TextValue diff --git a/pyface/data_view/value_types/numeric_value.py b/pyface/data_view/value_types/numeric_value.py new file mode 100644 index 000000000..a6d11b304 --- /dev/null +++ b/pyface/data_view/value_types/numeric_value.py @@ -0,0 +1,78 @@ +import locale +from math import inf + +from traits.api import ( + Bool, Callable, Enum, Event, Int, Str, Float, + observe +) + +from pyface.data_view.abstract_value_type import BaseValueType + + +def format_locale(value): + return "{:n}".format(value) + + +class NumericValue(BaseValueType): + """ Data channels for a numeric value. + """ + + #: The minimum value for the numeric value. + minimum = Float(-inf) + + #: The maximum value for the numeric value. + maximum = Float(inf) + + #: A function that converts to the a numeric type. + evaluate = Callable() + + #: A function that converts the required type to a string for display. + format = Callable(format_locale, update=True) + + #: A function that converts the required type from a display string. + unformat = Callable(locale.delocalize) + + def is_valid(self, model, row, column, value): + try: + return self.minimum <= value <= self.maximum + except Exception: + return False + + def get_editable(self, model, row, column): + # evaluate is needed to convert numpy types to python types so + # Qt recognises them + return self.evaluate(model.get_value(row, column)) + + def set_editable(self, model, row, column, value): + if not self.is_valid(model, row, column, value): + return False + return model.set_value(row, column, value) + + def get_text(self, model, row, column): + return self.format(model.get_value(row, column)) + + def set_text(self, model, row, column, text): + try: + value = self.evaluate(self.unformat(text)) + except ValueError: + return False + return self.set_editable(model, row, column, value) + + +class IntValue(NumericValue): + + evaluate = Callable(int) + + +class FloatValue(NumericValue): + + evaluate = Callable(float) + + +class ProportionValue(NumericValue): + + minimum = 0.0 + + maximum = 1.0 + + evaluate = Callable(float) diff --git a/pyface/data_view/value_types/testing.py b/pyface/data_view/value_types/testing.py new file mode 100644 index 000000000..e69de29bb diff --git a/pyface/data_view/value_types/tests/__init__.py b/pyface/data_view/value_types/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pyface/data_view/value_types/tests/test_numeric_value.py b/pyface/data_view/value_types/tests/test_numeric_value.py new file mode 100644 index 000000000..11f19656b --- /dev/null +++ b/pyface/data_view/value_types/tests/test_numeric_value.py @@ -0,0 +1,96 @@ +from unittest import TestCase +from unittest.mock import Mock + +from ..numeric_value import FloatValue, IntValue, NumericValue, format_locale + + +class TestNumericValue(TestCase): + + def setUp(self): + self.model = Mock() + self.model.get_value = Mock(return_value=1.0) + self.model.set_value = Mock(return_value=True) + + def test_defaults(self): + value = NumericValue() + self.assertIsNone(value.evaluate) + + def test_is_valid(self): + value = NumericValue() + self.assertTrue(value.is_valid(None, [0], [0], 0.0)) + + def test_is_valid_false(self): + value = NumericValue(minimum=0.0, maximum=1.0) + self.assertFalse(value.is_valid(None, [0], [0], -1.0)) + + def test_is_valid_error(self): + value = NumericValue() + self.assertFalse(value.is_valid(None, [0], [0], 'invalid')) + + def test_get_editable(self): + value = NumericValue(evaluate=float) + editable = value.get_editable(self.model, [0], [0]) + + self.assertEqual(editable, 1.0) + + def test_set_editable(self): + value = NumericValue(evaluate=float) + success = value.set_editable(self.model, [0], [0], 1.0) + + self.assertTrue(success) + self.model.set_value.assert_called_once_with([0], [0], 1.0) + + def test_set_editable_invalid(self): + value = NumericValue(minimum=0.0, maximum=1.0) + success = value.set_editable(self.model, [0], [0], -1.0) + + self.assertFalse(success) + self.model.set_value.assert_not_called() + + def test_set_editable_error(self): + value = NumericValue(minimum=0.0, maximum=1.0) + success = value.set_editable(self.model, [0], [0], 'invalid') + + self.assertFalse(success) + self.model.set_value.assert_not_called() + + def test_get_text(self): + value = NumericValue() + text = value.get_text(self.model, [0], [0]) + + self.assertEqual(text, format_locale(1.0)) + + def test_set_text(self): + value = NumericValue(evaluate=float) + success = value.set_text(self.model, [0], [0], format_locale(1.1)) + + self.assertTrue(success) + self.model.set_value.assert_called_once_with([0], [0], 1.1) + + def test_set_text_invalid(self): + value = NumericValue(evaluate=float, minimum=0.0, maximum=1.0) + success = value.set_text(self.model, [0], [0], format_locale(1.1)) + + self.assertFalse(success) + self.model.set_value.assert_not_called() + + def test_set_text_error(self): + value = NumericValue(evaluate=float) + success = value.set_text(self.model, [0], [0], "invalid") + + self.assertFalse(success) + self.model.set_value.assert_not_called() + + +class TestIntValue(TestCase): + + def test_defaults(self): + value = IntValue() + self.assertIs(value.evaluate, int) + + +class TestFloatValue(TestCase): + + def test_defaults(self): + value = FloatValue() + self.assertIs(value.evaluate, float) diff --git a/pyface/data_view/value_types/tests/test_text_value.py b/pyface/data_view/value_types/tests/test_text_value.py new file mode 100644 index 000000000..c4f02b0b5 --- /dev/null +++ b/pyface/data_view/value_types/tests/test_text_value.py @@ -0,0 +1,46 @@ +from unittest import TestCase +from unittest.mock import Mock + +from ..text_value import TextValue + + +class TestTextValue(TestCase): + + def setUp(self): + self.model = Mock() + self.model.get_value = Mock(return_value="test") + self.model.set_value = Mock(return_value=True) + + def test_defaults(self): + value = TextValue() + + def test_is_valid(self): + value = TextValue() + self.assertTrue(value.is_valid(None, [0], [0], "test")) + + def test_get_editable(self): + value = TextValue() + editable = value.get_editable(self.model, [0], [0]) + + self.assertEqual(editable, "test") + + def test_set_editable(self): + value = TextValue() + success = value.set_editable(self.model, [0], [0], "test") + + self.assertTrue(success) + self.model.set_value.assert_called_once_with([0], [0], "test") + + def test_get_text(self): + value = TextValue() + editable = value.get_text(self.model, [0], [0]) + + self.assertEqual(editable, "test") + + def test_set_text(self): + value = TextValue() + success = value.set_text(self.model, [0], [0], "test") + + self.assertTrue(success) + self.model.set_value.assert_called_once_with([0], [0], "test") + diff --git a/pyface/data_view/value_types/text_value.py b/pyface/data_view/value_types/text_value.py new file mode 100644 index 000000000..daa7d79a9 --- /dev/null +++ b/pyface/data_view/value_types/text_value.py @@ -0,0 +1,11 @@ + +from pyface.data_view.abstract_value_type import BaseValueType + + +class TextValue(BaseValueType): + + def get_text(self, model, row, column): + return str(model.get_value(row, column)) + + def set_text(self, model, row, column, text): + return model.set_value(row, column, text) diff --git a/pyface/ui/qt4/data_view/data_view_item_model.py b/pyface/ui/qt4/data_view/data_view_item_model.py index 5a38a71f6..6056a3c0a 100644 --- a/pyface/ui/qt4/data_view/data_view_item_model.py +++ b/pyface/ui/qt4/data_view/data_view_item_model.py @@ -25,6 +25,7 @@ class DataViewItemModel(QAbstractItemModel): def __init__(self, model, parent=None): super().__init__(parent) self.model = model + self.showRowHeader = True @property def model(self): @@ -69,25 +70,24 @@ def on_structure_changed(self, event): self.endResetModel() def on_values_changed(self, event): - if event.top == [] and event.bottom == []: + top, left, bottom, right = event.new + if top == [] and bottom == []: # this is a column header change - self.headerDataChanged(event.left[0], event.right[0]) - elif event.left == [] and event.right == []: + self.headerDataChanged.emit(left[0], right[0]) + elif left == [] and right == []: # this is a row header change # XXX this is currently not supported and not needed pass else: - top = [] - bottom = [] - for top_row, bottom_row in zip(event.new.top, event.new.bottom): - top.append(top_row) - bottom.append(bottom_row) + for i, (top_row, bottom_row) in enumerate(zip(top, bottom)): if top_row != bottom_row: break + top = top[:i+1] + bottom = bottom[:i+1] - top_left = self._to_model_index(top, event.left) - bottom_right = self._to_model_index(bottom, event.right) - self.dataChanged(top_left, bottom_right) + top_left = self._to_model_index(top, left) + bottom_right = self._to_model_index(bottom, right) + self.dataChanged.emit(top_left, bottom_right) # Structure methods @@ -125,7 +125,7 @@ def rowCount(self, index): def columnCount(self, index): row_index = self._to_row_index(index) try: - return self.model.get_column_count(row_index) + return self.model.get_column_count(row_index) + 1 except Exception: logger.exception("Error in columnCount") @@ -134,33 +134,62 @@ def columnCount(self, index): def flags(self, index): row = self._to_row_index(index) column = self._to_column_index(index) + value_type = self.model.get_value_type(row, column) - flags = Qt.ItemIsEnabled + flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable if not self.model.can_have_children(row): flags |= Qt.ItemNeverHasChildren + if value_type.get_is_editable(self.model, row, column): + flags |= Qt.ItemIsEditable + return flags def data(self, index, role=Qt.DisplayRole): row = self._to_row_index(index) column = self._to_column_index(index) + value_type = self.model.get_value_type(row, column) if role == Qt.DisplayRole: - return self.model.get_text(row, column) + if value_type.has_text(self.model, row, column): + return value_type.get_text(self.model, row, column) + elif role == Qt.EditRole: + if value_type.get_is_editable(self.model, row, column): + return value_type.get_editable(self.model, row, column) return None + def setData(self, index, value, role=Qt.EditRole): + row = self._to_row_index(index) + column = self._to_column_index(index) + value_type = self.model.get_value_type(row, column) + + if role == Qt.EditRole: + if value_type.get_is_editable(self.model, row, column): + return value_type.set_editable(self.model, row, column, value) + elif role == Qt.TextRole: + if value_type.has_text(self.model, row, column): + return value_type.set_text(self.model, row, column, value) + + return False + def headerData(self, section, orientation, role=Qt.DisplayRole): if orientation == Qt.Horizontal: row = [] - column = [section] + if section == 0: + column = [] + else: + column = [section - 1] else: # XXX not currently used, but here for symmetry and completeness row = [section] column = [] - if role == Qt.DisplayRole: - return self.model.get_text(row, column) + value_type = self.model.get_value_type(row, column) + + if role == Qt.DisplayRole: + if value_type.has_text(self.model, row, column): + return value_type.get_text(self.model, row, column) # Private utility methods @@ -180,14 +209,21 @@ def _to_column_index(self, index): if not index.isValid(): return [] else: - return [index.column()] + column = index.column() + if column == 0: + return [] + else: + return [column - 1] def _to_model_index(self, row_index, column_index): - if row_index == Root: + if len(row_index) == 0: return QModelIndex() index = self.model.index_manager.from_sequence(row_index[:-1]) row = row_index[-1] - column = column_index[0] + if len(column_index) == 0: + column = 0 + else: + column = column_index[0] + 1 - model_index = self.createIndex(row, column, index) + return self.createIndex(row, column, index) diff --git a/pyface/ui/wx/data_view/data_view_model.py b/pyface/ui/wx/data_view/data_view_model.py index 5bb51305e..a25bb0350 100644 --- a/pyface/ui/wx/data_view/data_view_model.py +++ b/pyface/ui/wx/data_view/data_view_model.py @@ -4,26 +4,96 @@ from wx.dataview import DataViewItem, DataViewModel as wxDataViewModel +type_hint_to_variant = { + 'str': "string", + 'int': "longlong", + 'float': "double", + 'bool': "bool", + 'datetime': "datetime", + 'container': "list", + 'object': "void*", +} + + # XXX This file is scaffolding and may need to be rewritten or expanded class DataViewModel(wxDataViewModel): def __init__(self, model): super().__init__() - self._model = model + self.model = model @property def model(self): return self._model + @model.setter + def model(self, model): + if hasattr(self, '_model'): + # disconnect trait listeners + self._model.observe( + self.on_structure_changed, + 'structure_changed', + remove=True, + ) + self._model.observe( + self.on_values_changed, + 'values_changed', + remove=True, + ) + self._model = model + else: + # model is being initialized + self._model = model + + # hook up trait listeners + self._model.observe( + self.on_structure_changed, + 'structure_changed', + ) + self._model.observe( + self.on_values_changed, + 'values_changed', + ) + + def on_structure_changed(self, event): + self.Cleared() + + def on_values_changed(self, event): + top, left, bottom, right = event.new + if top == [] and bottom == []: + # this is a column header change, reset everything + self.Cleared() + elif left == [] and right == []: + # this is a row header change + # XXX this is currently not supported and not needed + pass + else: + for i, (top_row, bottom_row) in enumerate(zip(top, bottom)): + if top_row != bottom_row: + break + top = top[:i+1] + bottom = bottom[:i+1] + + if top == bottom and left == right: + # single value change + self.ValueChanged(self._to_item(top), left[0]) + elif top == bottom: + # single item change + self.ItemChanged(self._to_item(top)) + else: + # multiple item change + items = [self._to_item(top[:i] + [row]) for row in range(top[i], bottom[i]+1)] + self.ItemsChanged(items) + def GetParent(self, item): index = self._to_index(item) if index == Root: - return None + return DataViewItem() parent, row = self.model.index_manager.get_parent_and_row(index) parent_id = self.model.index_manager.id(parent) if parent_id == 0: - return None + return DataViewItem() return DataViewItem(parent_id) def GetChildren(self, item, children): @@ -40,8 +110,8 @@ def IsContainer(self, item): row_index = self._to_row_index(item) return self.model.can_have_children(row_index) - def HasContainerColumns(self, item): - return item.GetID() is not None + def HasValue(self, item, column): + return True def HasChildren(self, item): row_index = self._to_row_index(item) @@ -50,23 +120,26 @@ def HasChildren(self, item): def GetValue(self, item, column): row_index = self._to_row_index(item) column_index = [column] - text = self.model.get_text(row_index, column_index) - return text + return self.model.get_text(row_index, column_index) def SetValue(self, value, item, column): row_index = self._to_row_index(item) column_index = [column] try: - self.model.set_text(row_index, column_index, value) + result = self.model.set_text(row_index, column_index, value) except Exception as exc: print(exc) # XXX log it return False - return True + return result def GetColumnCount(self): return self.model.get_column_count([]) + def GetColumnType(self, column): + value_type = self.model.get_column_value_type([column]) + return type_hint_to_variant.get(value_type.type_hint, "string") + def _to_row_index(self, item): id = item.GetID() if id is None: @@ -74,6 +147,13 @@ def _to_row_index(self, item): index = self.model.index_manager.from_id(int(id)) return self.model.index_manager.to_sequence(index) + def _to_item(self, row_index): + if len(row_index) == 0: + return DataViewItem() + index = self.model.index_manager.from_sequence(row_index) + id = self.model.index_manager.id(index) + return DataViewItem(id) + def _to_index(self, item): id = item.GetID() if id is None: diff --git a/pyface/ui/wx/data_view/data_view_widget.py b/pyface/ui/wx/data_view/data_view_widget.py index a5ae046f4..1945d4a17 100644 --- a/pyface/ui/wx/data_view/data_view_widget.py +++ b/pyface/ui/wx/data_view/data_view_widget.py @@ -11,7 +11,8 @@ from traits.api import Bool, Instance, observe, provides from wx.dataview import ( - DataViewCtrl, DataViewModel as wxDataViewModel, DATAVIEW_CELL_EDITABLE + DataViewCtrl, DataViewModel as wxDataViewModel, DATAVIEW_CELL_EDITABLE, + EVT_DATAVIEW_ITEM_ACTIVATED ) from pyface.data_view.abstract_data_model import AbstractDataModel from pyface.data_view.i_data_view_widget import ( @@ -48,6 +49,30 @@ def _create_control(self, parent): def _create_item_model(self): self._item_model = DataViewModel(self.data_model) + def _add_event_listeners(self): + """ Set up toolkit-specific bindings for events """ + super()._add_event_listeners() + self.control.Bind(EVT_DATAVIEW_ITEM_ACTIVATED, self.activated) + + def _remove_event_listeners(self): + """ Remove toolkit-specific bindings for events """ + self.control.Unbind(EVT_DATAVIEW_ITEM_ACTIVATED, self.activated) + super()._remove_event_listeners() + + def destroy(self): + if self.control is not None: + # unhook things here + self._item_model = None + super().destroy() + + def activated(self, event): + print('activated') + if self.control is not None: + print(event.GetPosition()) + column = self.control.GetColumns()[event.GetColumn()] + print(event.GetColumn()) + #self.control.EditItem(event.GetItem(), column) + def _get_control_header_visible(self): """ Toolkit specific method to get the control's tooltip. """ #return not self.control.isHeaderHidden() From 02c7af7bb2086245cac05d47c51563a32b0421bc Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Tue, 16 Jun 2020 10:53:24 +0100 Subject: [PATCH 05/52] Update abstract data model docstrings. --- pyface/data_view/abstract_data_model.py | 80 ++++++++++++++++++------- 1 file changed, 60 insertions(+), 20 deletions(-) diff --git a/pyface/data_view/abstract_data_model.py b/pyface/data_view/abstract_data_model.py index d66233d22..67edf8cb2 100644 --- a/pyface/data_view/abstract_data_model.py +++ b/pyface/data_view/abstract_data_model.py @@ -7,14 +7,14 @@ # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! -""" -Abstract Data Model -=================== + +""" Provides an AbstractDataModel ABC for Pyface data models. This module provides an ABC for all data view data models. This specifies the API that the data view widgets expect, and which the underlying data is adapted to by the concrete implementations. Data models are intended -to be toolkit-independent +to be toolkit-independent, and be able to adapt any approximately tabular or +nested data structure to what the data view system expects. """ from abc import abstractmethod @@ -24,10 +24,44 @@ class AbstractDataModel(ABCHasStrictTraits): - """ Abstract base class for data models. """ + """ Abstract base class for Pyface data models. + + The data model API is intended to provide a common API for heirarchical + and tabular data. This class is concerned with the structure, type and + values provided by the data, but not with how the data is presented. + + Row and column indices are represented by sequences (usually lists) of + integers, specifying the index at each level of the heirarchy. The root + row and column are represented by empty lists. + + Subclasses need to implement the ``get_column_count``, + ``can_have_children`` and ``get_row_count`` methods to return the number + of columns in a particular row, as well as the heirarchical structure of + the rows. Appropriate observers should be set up on the underlaying data + so that the ``structure_changed`` event is fired when the values returned + by these methods would change. + + Subclasses also have to implement the ``get_value`` and ``get_value_type`` + methods. These expect a row and column index, with root values treated + specially: the root row corresponds to the values which will be displayed + in the column headers of the view, and the root column corresponds to the + values which will be displayed in the row headers of the view. + The ``get_value`` returns an arbitrary Python object corresponding to the + cell being viewed, and the ``get_value_type`` should return an instance of + an ``AbstractValueType`` that adapts the raw value to the data channels + that the data view expects (eg. text, color, icons, editable value, etc.). + Implementations should ensure that the ``values_changed`` event fires + whenever the data, or the way the data is presented, is updated. + + If the data is to be editable then the subclass should override the + ``set_data`` method. It should attempt to change the underlying data as a + side-effect, and return True on success and False on failure (for example, + setting an invalid value). + """ #: The index manager that helps convert toolkit indices to data view - #: indices. + #: indices. This should be an IntIndexManager for non-hierarchical data + #: or a TupleIndexManager for hierarchical data. index_manager = Instance(AbstractIndexManager) #: Event fired when the structure of the data changes. @@ -36,9 +70,6 @@ class AbstractDataModel(ABCHasStrictTraits): #: Event fired when value changes without changes to structure. values_changed = Event() - #: Event fired when selection changes. - selection = Event() - # Data structure methods @abstractmethod @@ -46,7 +77,7 @@ def get_column_count(self, row): """ How many columns in the row of the data view model. The total number of columns in the table is given by the column - count of the Root row. + count of the root row. Parameters ---------- @@ -64,6 +95,8 @@ def get_column_count(self, row): def can_have_children(self, row): """ Whether or not a row can have child rows. + The root row should always return True. + Parameters ---------- row : sequence of int @@ -98,15 +131,16 @@ def get_row_count(self, row): def get_value(self, row, column): """ Return the Python value for the row and column. - The values for column headers are returned by calling this method - with row as Root. + The values for column headers are returned by calling this method with + row equal to []. The values for row headers are returned by calling + this method with column equal to []. Parameters ---------- row : sequence of int The indices of the row as a sequence from root to leaf. column : sequence of int - The indices of the column as a sequence of length 1. + The indices of the column as a sequence of length 0 or 1. Returns ------- @@ -115,19 +149,22 @@ def get_value(self, row, column): """ raise NotImplementedError - @abstractmethod def set_value(self, row, column, value): """ Set the Python value for the row and column. - The values for column headers can be set by calling this method - with row as Root. + The default method assumes the data is read-only and always + returns False. + + The values for column headers can be set by calling this method with + row equal to []. The values for row headers can be set by calling + this method with column equal to []. Parameters ---------- row : sequence of int The indices of the row as a sequence from root to leaf. column : sequence of int - The indices of the column as a sequence of length 1. + The indices of the column as a sequence of length 0 or 1. value : any The new value for the given row and column. @@ -136,7 +173,7 @@ def set_value(self, row, column, value): success : bool Whether or not the value was set successfully. """ - raise NotImplementedError + return False # Data channels @@ -145,14 +182,15 @@ def get_value_type(self, row, column): """ Return the text value for the row and column. The value type for column headers are returned by calling this method - with row as Root. + with row equal to []. The value typess for row headers are returned + by calling this method with column equal to []. Parameters ---------- row : sequence of int The indices of the row as a sequence from root to leaf. column : sequence of int - The indices of the column as a sequence of length 1. + The indices of the column as a sequence of length 0 or 1. Returns ------- @@ -161,6 +199,8 @@ def get_value_type(self, row, column): """ raise NotImplementedError + # Convenience iterator methods + def iter_rows(self, start=[]): """ Iterator that yields rows in preorder. From 5f1de116e22d3e6c408fa9216b8b7fd62debda05 Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Tue, 16 Jun 2020 17:23:53 +0100 Subject: [PATCH 06/52] Lots more docstrings, some additional clean-up and API clarification. --- examples/data_view/array_example.py | 10 + examples/data_view/column_example.py | 10 + pyface/data_view/abstract_data_model.py | 41 +++- pyface/data_view/abstract_value_type.py | 210 ++++++++++++------ .../data_view/data_models/array_data_model.py | 77 +++++-- .../data_models/column_data_model.py | 32 +++ pyface/data_view/value_types/api.py | 3 + .../data_view/value_types/constant_value.py | 32 +++ .../data_view/value_types/editable_value.py | 99 +++++++++ pyface/data_view/value_types/no_value.py | 25 +++ pyface/data_view/value_types/numeric_value.py | 24 +- pyface/data_view/value_types/testing.py | 0 .../value_types/tests/test_constant_value.py | 48 ++++ .../value_types/tests/test_numeric_value.py | 1 + pyface/data_view/value_types/text_value.py | 36 ++- .../ui/qt4/data_view/data_view_item_model.py | 10 +- 16 files changed, 545 insertions(+), 113 deletions(-) create mode 100644 pyface/data_view/value_types/constant_value.py create mode 100644 pyface/data_view/value_types/editable_value.py create mode 100644 pyface/data_view/value_types/no_value.py delete mode 100644 pyface/data_view/value_types/testing.py create mode 100644 pyface/data_view/value_types/tests/test_constant_value.py diff --git a/examples/data_view/array_example.py b/examples/data_view/array_example.py index 14eda9b80..a03cafa66 100644 --- a/examples/data_view/array_example.py +++ b/examples/data_view/array_example.py @@ -1,3 +1,13 @@ +# (C) Copyright 2005-2020 Enthought, Inc., Austin, TX +# All rights reserved. +# +# This software is provided without warranty under the terms of the BSD +# license included in LICENSE.txt and may be redistributed only under +# the conditions described in the aforementioned license. The license +# is also available online at http://www.enthought.com/licenses/BSD.txt +# +# Thanks for using Enthought open source! + from traits.api import Array, Instance from pyface.api import ApplicationWindow, GUI diff --git a/examples/data_view/column_example.py b/examples/data_view/column_example.py index c32c3552e..a03617a4a 100644 --- a/examples/data_view/column_example.py +++ b/examples/data_view/column_example.py @@ -1,3 +1,13 @@ +# (C) Copyright 2005-2020 Enthought, Inc., Austin, TX +# All rights reserved. +# +# This software is provided without warranty under the terms of the BSD +# license included in LICENSE.txt and may be redistributed only under +# the conditions described in the aforementioned license. The license +# is also available online at http://www.enthought.com/licenses/BSD.txt +# +# Thanks for using Enthought open source! + from random import choice, randint from traits.api import HasStrictTraits, Instance, Int, Str, List diff --git a/pyface/data_view/abstract_data_model.py b/pyface/data_view/abstract_data_model.py index 67edf8cb2..4d566b2e8 100644 --- a/pyface/data_view/abstract_data_model.py +++ b/pyface/data_view/abstract_data_model.py @@ -53,10 +53,13 @@ class AbstractDataModel(ABCHasStrictTraits): Implementations should ensure that the ``values_changed`` event fires whenever the data, or the way the data is presented, is updated. + If the data is to be editable then the subclass should override the ``set_data`` method. It should attempt to change the underlying data as a side-effect, and return True on success and False on failure (for example, - setting an invalid value). + setting an invalid value). If the underlying data structure cannot be + listened to internally (such as a numpy array or Pandas data frame), this + method should also fire the values changed event with appropriate values. """ #: The index manager that helps convert toolkit indices to data view @@ -67,7 +70,10 @@ class AbstractDataModel(ABCHasStrictTraits): #: Event fired when the structure of the data changes. structure_changed = Event() - #: Event fired when value changes without changes to structure. + #: Event fired when value changes without changes to structure. This + #: should be set to a 4-tuple of (start_row_index, start_column_index, + #: end_row_index, end_column_index) indicated the subset of data which + #: changed. values_changed = Event() # Data structure methods @@ -149,6 +155,30 @@ def get_value(self, row, column): """ raise NotImplementedError + def can_set_value(self, row, column): + """ Whether the value in the indicated row and column can be set. + + The default method assumes the data is read-only and always + returns False. + + Whether or a column header can be set is returned by calling this + method with row equal to []. Whether or a row header can be set + is returned by calling this method with column equal to []. + + Parameters + ---------- + row : sequence of int + The indices of the row as a sequence from root to leaf. + column : sequence of int + The indices of the column as a sequence of length 0 or 1. + + Returns + ------- + can_set_value : bool + Whether or not the value can be set. + """ + return False + def set_value(self, row, column, value): """ Set the Python value for the row and column. @@ -175,8 +205,6 @@ def set_value(self, row, column, value): """ return False - # Data channels - @abstractmethod def get_value_type(self, row, column): """ Return the text value for the row and column. @@ -194,8 +222,9 @@ def get_value_type(self, row, column): Returns ------- - text : str - The text to display in the given row and column. + value_type : AbstractValueType or None + The value type of the given row and column, or None if no value + should be displayed. """ raise NotImplementedError diff --git a/pyface/data_view/abstract_value_type.py b/pyface/data_view/abstract_value_type.py index ec671d6f2..9f19a7220 100644 --- a/pyface/data_view/abstract_value_type.py +++ b/pyface/data_view/abstract_value_type.py @@ -1,17 +1,25 @@ -from abc import abstractmethod -import locale -from math import inf -import sys +# (C) Copyright 2005-2020 Enthought, Inc., Austin, TX +# All rights reserved. +# +# This software is provided without warranty under the terms of the BSD +# license included in LICENSE.txt and may be redistributed only under +# the conditions described in the aforementioned license. The license +# is also available online at http://www.enthought.com/licenses/BSD.txt +# +# Thanks for using Enthought open source! -from traits.api import ( - ABCHasStrictTraits, Any, Bool, Callable, Enum, Event, Int, Str, Float, - observe -) -from pyface.ui_traits import Image +""" Provides an AbstractValueType ABC for Pyface data models. +This module provides an ABC for data view value types, which are responsible +for adapting raw data values as used by the data model's ``get_value`` and +``set_value`` methods to the data channels that the data view expects, such +as text, color, icons, etc. -default_max = sys.maxsize -default_min = -sys.maxsize + 1 +It is up to the data view to take this standardized data and determine what +and how to actually display it. +""" + +from traits.api import ABCHasStrictTraits, Event, Str, observe class AbstractValueType(ABCHasStrictTraits): @@ -20,85 +28,153 @@ class AbstractValueType(ABCHasStrictTraits): The data channels are editor value, text, color, image, and description. The data channels are used by other parts of the code to produce the actual display. + + Subclasses should mark traits that potentially affect the display of values + with ``update=True`` metdadata, or alternativelym fire the ``updated`` + event when the state of the value type changes. """ #: Fired when a change occurs that requires updating values. updated = Event - def is_valid(self, model, row, column, value): - return True + def can_edit(self, model, row, column): + """ Return whether or not the value can be edited. - def get_is_editable(self, model, row, column): - return False + The default implementation is that cells that can be set are + editable. + + Parameters + ---------- + model : AbstractDataModel + The data model holding the data. + row : sequence of int + The row in the data model being queried. + column : sequence of int + The column in the data model being queried. + + Returns + ------- + can_edit : bool + Whether or not the value is editable. + """ + return model.can_set_value(row, column) def get_editable(self, model, row, column): + """ Return a value suitable for editing. + + The default implementation is to return the underlying data value + directly from the data model. + + Parameters + ---------- + model : AbstractDataModel + The data model holding the data. + row : sequence of int + The row in the data model being queried. + column : sequence of int + The column in the data model being queried. + + Returns + ------- + value : any + The value to edit. + """ return model.get_value(row, column) def set_editable(self, model, row, column, value): + """ Return a value suitable for editing. + + The default implementation is to return the underlying data value + directly from the data model. + + Parameters + ---------- + model : AbstractDataModel + The data model holding the data. + row : sequence of int + The row in the data model being queried. + column : sequence of int + The column in the data model being queried. + + Returns + ------- + value : any + The value to edit. + """ + if not self.can_edit(model, row, column): + return False return model.set_value(row, column, value) def has_text(self, model, row, column): + """ Whether or not the value has a textual representation. + + The default implementation returns True if ``get_text`` + returns a non-empty value. + + Parameters + ---------- + model : AbstractDataModel + The data model holding the data. + row : sequence of int + The row in the data model being queried. + column : sequence of int + The column in the data model being queried. + + Returns + ------- + value : any + The value to edit. + """ return self.get_text(model, row, column) != "" def get_text(self, model, row, column): + """ The textual representation of the underlying value. + + The default implementation calls str() on the underlying value. + + Parameters + ---------- + model : AbstractDataModel + The data model holding the data. + row : sequence of int + The row in the data model being queried. + column : sequence of int + The column in the data model being queried. + + Returns + ------- + text : str + The value to edit. + """ return str(model.get_value(row, column)) def set_text(self, model, row, column, text): - """ Default behaviour does not allow setting the text. """ + """ Set the textual representation of the underlying value. + + This is provided primarily for backends which may not permit + non-text editing of values, in which case this provides an + alternative route to setting the value. The default implementation + does not allow setting the text. + + Parameters + ---------- + model : AbstractDataModel + The data model holding the data. + row : sequence of int + The row in the data model being queried. + column : sequence of int + The column in the data model being queried. + text : str + The text to set. + + Returns + ------- + success : bool + Whether or not the value was successfully set. + """ return False @observe('+update') def update_value_type(self, event=None): """ Fire update event when marked traits change. """ self.updated = True - - -class BaseValueType(AbstractValueType): - - #: Whether or not there is an editable value. - is_editable = Bool(True, update=True) - - def get_is_editable(self, model, row, column): - return self.is_editable - - def get_editable(self, model, row, column): - return model.get_value(row, column) - - def set_editable(self, model, row, column, value): - return model.set_value(row, column, value) - - def has_text(self, model, row, column): - return True - - def get_text(self, model, row, column): - return self.text - - def set_text(self, model, row, column, text): - """ Default behaviour does not allow setting the text. """ - return False - - -class NoneValue(AbstractValueType): - - def is_valid(self, model, row, column, value): - return True - - def get_is_editable(self, model, row, column): - return False - - def has_text(self, model, row, column): - return False - - -none_value = NoneValue() - -class ConstantValueType(AbstractValueType): - - text = Str(update=True) - - def has_text(self, model, row, column): - return self.text != "" - - def get_text(self, model, row, column): - return self.text - diff --git a/pyface/data_view/data_models/array_data_model.py b/pyface/data_view/data_models/array_data_model.py index 9f9a811c2..c06f874d2 100644 --- a/pyface/data_view/data_models/array_data_model.py +++ b/pyface/data_view/data_models/array_data_model.py @@ -7,20 +7,18 @@ # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! -""" -Array Data Model -================ +""" Provides an N-dimensional array data model implementation. -This module provides an a concrete implementation of a data model for a 2D -numpy array. +This module provides an a concrete implementation of a data model for an +n-dim numpy array. """ from traits.api import Array, Instance, observe from pyface.data_view.abstract_data_model import AbstractDataModel -from pyface.data_view.abstract_value_type import ( - AbstractValueType, ConstantValueType, none_value +from pyface.data_view.abstract_value_type import AbstractValueType +from pyface.data_view.value_types.api import ( + ConstantValue, FloatValue, IntValue, TextValue, no_value ) -from pyface.data_view.value_types.api import FloatValue, IntValue, TextValue from pyface.data_view.index_manager import TupleIndexManager @@ -36,7 +34,7 @@ class ArrayDataModel(AbstractDataModel): #: The value type of the column titles. header_label_type = Instance( AbstractValueType, - factory=ConstantValueType, + factory=ConstantValue, kw={'text': "Index"}, ) @@ -116,7 +114,7 @@ def get_row_count(self, row): # Data value methods def get_value(self, row, column): - """ How many child rows the row currently has. + """ Return the Python value for the row and column. Parameters ---------- @@ -131,14 +129,35 @@ def get_value(self, row, column): if row == []: return column[0] elif column == []: - # XXX not currently used return row[-1] else: index = tuple(row + column) if len(index) != len(self.data.shape): - return 0 + return None return self.data[index] + def can_set_value(self, row, column): + """ Whether the value in the indicated row and column can be set. + + This returns False for row and column headers, but True for all + array values. + + Parameters + ---------- + row : sequence of int + The indices of the row as a sequence from root to leaf. + column : sequence of int + The indices of the column as a sequence of length 0 or 1. + + Returns + ------- + can_set_value : bool + Whether or not the value can be set. + """ + # can only set values when we have the full index + index = tuple(row + column) + return len(index) == self.data.ndim + def set_value(self, row, column, value): """ Return the Python value for the row and column. @@ -157,17 +176,33 @@ def set_value(self, row, column, value): value : any The value represented by the given row and column. """ - if row == []: + index = tuple(row + column) + if len(index) < self.data.ndim: return False - elif column == []: - return False - else: - index = tuple(row + column) - self.data[index] = value - self.values_changed = (row, column, row, column) - return True + + self.data[index] = value + self.values_changed = (row, column, row, column) + return True def get_value_type(self, row, column): + """ Return the text value for the row and column. + + The value type for column headers are returned by calling this method + with row equal to []. The value typess for row headers are returned + by calling this method with column equal to []. + + Parameters + ---------- + row : sequence of int + The indices of the row as a sequence from root to leaf. + column : sequence of int + The indices of the column as a sequence of length 0 or 1. + + Returns + ------- + text : str + The text to display in the given row and column. + """ if row == []: if column == []: return self.header_label_type @@ -176,7 +211,7 @@ def get_value_type(self, row, column): # XXX not currently used return self.row_header_type elif len(row) < len(self.data.shape) - 1: - return none_value + return no_value else: return self.value_type diff --git a/pyface/data_view/data_models/column_data_model.py b/pyface/data_view/data_models/column_data_model.py index 65edc82d1..fc3df8a51 100644 --- a/pyface/data_view/data_models/column_data_model.py +++ b/pyface/data_view/data_models/column_data_model.py @@ -1,3 +1,13 @@ +# (C) Copyright 2005-2020 Enthought, Inc., Austin, TX +# All rights reserved. +# +# This software is provided without warranty under the terms of the BSD +# license included in LICENSE.txt and may be redistributed only under +# the conditions described in the aforementioned license. The license +# is also available online at http://www.enthought.com/licenses/BSD.txt +# +# Thanks for using Enthought open source! + from abc import abstractmethod from traits.api import ( @@ -183,6 +193,28 @@ def get_value(self, row, column): obj = self.data[column[0]] return row_info.get_value(obj) + def can_set_value(self, row, column): + """ Whether the value in the indicated row and column can be set. + + This returns False for row headers, but True for all other values. + + Parameters + ---------- + row : sequence of int + The indices of the row as a sequence from root to leaf. + column : sequence of int + The indices of the column as a sequence of length 0 or 1. + + Returns + ------- + can_set_value : bool + Whether or not the value can be set. + """ + if column == []: + return False + else: + return True + def set_value(self, row, column, value): row_info = self._row_info_object(row) if len(column) == 0: diff --git a/pyface/data_view/value_types/api.py b/pyface/data_view/value_types/api.py index 60c3ab0df..26221fa97 100644 --- a/pyface/data_view/value_types/api.py +++ b/pyface/data_view/value_types/api.py @@ -1,4 +1,7 @@ +from .constant_value import ConstantValue +from .editable_value import EditableValue +from .no_value import NoValue, no_value from .numeric_value import FloatValue, IntValue, NumericValue from .text_value import TextValue diff --git a/pyface/data_view/value_types/constant_value.py b/pyface/data_view/value_types/constant_value.py new file mode 100644 index 000000000..5b62d5406 --- /dev/null +++ b/pyface/data_view/value_types/constant_value.py @@ -0,0 +1,32 @@ +# (C) Copyright 2005-2020 Enthought, Inc., Austin, TX +# All rights reserved. +# +# This software is provided without warranty under the terms of the BSD +# license included in LICENSE.txt and may be redistributed only under +# the conditions described in the aforementioned license. The license +# is also available online at http://www.enthought.com/licenses/BSD.txt +# +# Thanks for using Enthought open source! + +from traits.api import Str + +from pyface.data_view.abstract_value_type import AbstractValueType + + +class ConstantValue(AbstractValueType): + """ A value type that does not depend on the underlying data model. + + This value type is not editable, but the other data channels it + provides can be modified by changing the appropriate trait on the + value type. + """ + + #: The text value to display. + text = Str(update=True) + + def can_edit(self, model, row, column): + return False + + def get_text(self, model, row, column): + return self.text + diff --git a/pyface/data_view/value_types/editable_value.py b/pyface/data_view/value_types/editable_value.py new file mode 100644 index 000000000..1fad240b5 --- /dev/null +++ b/pyface/data_view/value_types/editable_value.py @@ -0,0 +1,99 @@ +# (C) Copyright 2005-2020 Enthought, Inc., Austin, TX +# All rights reserved. +# +# This software is provided without warranty under the terms of the BSD +# license included in LICENSE.txt and may be redistributed only under +# the conditions described in the aforementioned license. The license +# is also available online at http://www.enthought.com/licenses/BSD.txt +# +# Thanks for using Enthought open source! + +from traits.api import Bool + +from pyface.data_view.abstract_value_type import AbstractValueType + + +class EditableValue(AbstractValueType): + """ A base class for editable values. + + This class provides two things beyond the base AbstractValueType: + a trait ``is_editable`` which allows toggling editing state on and + off, and an ``is_valid` method that is used for validation before + setting a value. + """ + + #: Whether or not the value is editable, assuming the underlying data can + #: be set. + is_editable = Bool(True, update=True) + + def is_valid(self, model, row, column, value): + """ Whether or not the value is valid for the data item specified. + + The default implementation returns True for all values. + + Parameters + ---------- + model : AbstractDataModel + The data model holding the data. + row : sequence of int + The row in the data model being queried. + column : sequence of int + The column in the data model being queried. + value : any + The value to validate. + + Returns + ------- + is_valid : bool + Whether or not the value is valid. + """ + return True + + # AbstractValueType Interface -------------------------------------------- + + def can_edit(self, model, row, column): + """ Return whether or not the value can be edited. + + A cell is editable if the underlying data can be set, and the + ``is_editable`` flag is set to True + + Parameters + ---------- + model : AbstractDataModel + The data model holding the data. + row : sequence of int + The row in the data model being queried. + column : sequence of int + The column in the data model being queried. + + Returns + ------- + can_edit : bool + Whether or not the value is editable. + """ + return model.can_set_value(row, column) and self.is_editable + + def set_editable(self, model, row, column, value): + """ Return whether or not the value can be edited. + + A cell is editable if the underlying data can be set, and the + ``is_editable`` flag is set to True + + Parameters + ---------- + model : AbstractDataModel + The data model holding the data. + row : sequence of int + The row in the data model being queried. + column : sequence of int + The column in the data model being queried. + + Returns + ------- + can_edit : bool + Whether or not the value is editable. + """ + if not (self.can_edit(model, row, column) + and self.is_valid(model, row, column, value)): + return False + return model.set_value(row, column, value) diff --git a/pyface/data_view/value_types/no_value.py b/pyface/data_view/value_types/no_value.py new file mode 100644 index 000000000..9172afdf7 --- /dev/null +++ b/pyface/data_view/value_types/no_value.py @@ -0,0 +1,25 @@ +# (C) Copyright 2005-2020 Enthought, Inc., Austin, TX +# All rights reserved. +# +# This software is provided without warranty under the terms of the BSD +# license included in LICENSE.txt and may be redistributed only under +# the conditions described in the aforementioned license. The license +# is also available online at http://www.enthought.com/licenses/BSD.txt +# +# Thanks for using Enthought open source! + +from pyface.data_view.abstract_value_type import AbstractValueType + + +class NoValue(AbstractValueType): + """ A ValueType that has no data in any channel. """ + + def can_edit(self, model, row, column): + return False + + def has_text(self, model, row, column): + return False + + +#: Standard instance of the NoValue class, since it has no state. +no_value = NoValue() diff --git a/pyface/data_view/value_types/numeric_value.py b/pyface/data_view/value_types/numeric_value.py index a6d11b304..1dedf7e1e 100644 --- a/pyface/data_view/value_types/numeric_value.py +++ b/pyface/data_view/value_types/numeric_value.py @@ -1,19 +1,26 @@ +# (C) Copyright 2005-2020 Enthought, Inc., Austin, TX +# All rights reserved. +# +# This software is provided without warranty under the terms of the BSD +# license included in LICENSE.txt and may be redistributed only under +# the conditions described in the aforementioned license. The license +# is also available online at http://www.enthought.com/licenses/BSD.txt +# +# Thanks for using Enthought open source! + import locale from math import inf -from traits.api import ( - Bool, Callable, Enum, Event, Int, Str, Float, - observe -) +from traits.api import Callable, Float -from pyface.data_view.abstract_value_type import BaseValueType +from .editable_value import EditableValue def format_locale(value): return "{:n}".format(value) -class NumericValue(BaseValueType): +class NumericValue(EditableValue): """ Data channels for a numeric value. """ @@ -43,11 +50,6 @@ def get_editable(self, model, row, column): # Qt recognises them return self.evaluate(model.get_value(row, column)) - def set_editable(self, model, row, column, value): - if not self.is_valid(model, row, column, value): - return False - return model.set_value(row, column, value) - def get_text(self, model, row, column): return self.format(model.get_value(row, column)) diff --git a/pyface/data_view/value_types/testing.py b/pyface/data_view/value_types/testing.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/pyface/data_view/value_types/tests/test_constant_value.py b/pyface/data_view/value_types/tests/test_constant_value.py new file mode 100644 index 000000000..3e5562884 --- /dev/null +++ b/pyface/data_view/value_types/tests/test_constant_value.py @@ -0,0 +1,48 @@ +# (C) Copyright 2005-2020 Enthought, Inc., Austin, TX +# All rights reserved. +# +# This software is provided without warranty under the terms of the BSD +# license included in LICENSE.txt and may be redistributed only under +# the conditions described in the aforementioned license. The license +# is also available online at http://www.enthought.com/licenses/BSD.txt +# +# Thanks for using Enthought open source! + +from unittest import TestCase +from unittest.mock import Mock + +from traits.testing.unittest_tools import UnittestTools + +from ..constant_value import ConstantValue + + +class TestConstantValue(UnittestTools, TestCase): + + def setUp(self): + self.model = Mock() + + def test_defaults(self): + value_type = ConstantValue() + self.assertEqual(value_type.text, "") + + def test_can_edit(self): + value_type = ConstantValue() + self.assertFalse(value_type.can_edit(self.model, [0], [0])) + + def test_has_text(self): + value_type = ConstantValue() + self.assertFalse(value_type.has_text(self.model, [0], [0])) + + def test_has_text_true(self): + value_type = ConstantValue(text="something") + self.assertTrue(value_type.has_text(self.model, [0], [0])) + + def test_get_text(self): + value_type = ConstantValue(text="something") + self.assertEqual(value_type.get_text(self.model, [0], [0]), "something") + + def test_text_changed(self): + value_type = ConstantValue() + with self.assertTraitChanges(value_type, 'updated'): + value_type.text = 'something' + self.assertEqual(value_type.text, 'something') diff --git a/pyface/data_view/value_types/tests/test_numeric_value.py b/pyface/data_view/value_types/tests/test_numeric_value.py index 11f19656b..16c6c7bd3 100644 --- a/pyface/data_view/value_types/tests/test_numeric_value.py +++ b/pyface/data_view/value_types/tests/test_numeric_value.py @@ -9,6 +9,7 @@ class TestNumericValue(TestCase): def setUp(self): self.model = Mock() self.model.get_value = Mock(return_value=1.0) + self.model.can_set_value = Mock(return_value=True) self.model.set_value = Mock(return_value=True) def test_defaults(self): diff --git a/pyface/data_view/value_types/text_value.py b/pyface/data_view/value_types/text_value.py index daa7d79a9..aeee39425 100644 --- a/pyface/data_view/value_types/text_value.py +++ b/pyface/data_view/value_types/text_value.py @@ -1,11 +1,37 @@ +# (C) Copyright 2005-2020 Enthought, Inc., Austin, TX +# All rights reserved. +# +# This software is provided without warranty under the terms of the BSD +# license included in LICENSE.txt and may be redistributed only under +# the conditions described in the aforementioned license. The license +# is also available online at http://www.enthought.com/licenses/BSD.txt +# +# Thanks for using Enthought open source! -from pyface.data_view.abstract_value_type import BaseValueType +from .editable_value import EditableValue -class TextValue(BaseValueType): - - def get_text(self, model, row, column): - return str(model.get_value(row, column)) +class TextValue(EditableValue): + """ Editable value that presents a string value. + """ def set_text(self, model, row, column, text): + """ Set the textual representation of the underlying value. + + Parameters + ---------- + model : AbstractDataModel + The data model holding the data. + row : sequence of int + The row in the data model being queried. + column : sequence of int + The column in the data model being queried. + text : str + The text to set. + + Returns + ------- + success : bool + Whether or not the value was successfully set. + """ return model.set_value(row, column, text) diff --git a/pyface/ui/qt4/data_view/data_view_item_model.py b/pyface/ui/qt4/data_view/data_view_item_model.py index 6056a3c0a..c1a27e947 100644 --- a/pyface/ui/qt4/data_view/data_view_item_model.py +++ b/pyface/ui/qt4/data_view/data_view_item_model.py @@ -140,7 +140,7 @@ def flags(self, index): if not self.model.can_have_children(row): flags |= Qt.ItemNeverHasChildren - if value_type.get_is_editable(self.model, row, column): + if value_type and value_type.can_edit(self.model, row, column): flags |= Qt.ItemIsEditable return flags @@ -149,12 +149,14 @@ def data(self, index, role=Qt.DisplayRole): row = self._to_row_index(index) column = self._to_column_index(index) value_type = self.model.get_value_type(row, column) + if not value_type: + return None if role == Qt.DisplayRole: if value_type.has_text(self.model, row, column): return value_type.get_text(self.model, row, column) elif role == Qt.EditRole: - if value_type.get_is_editable(self.model, row, column): + if value_type.can_edit(self.model, row, column): return value_type.get_editable(self.model, row, column) return None @@ -163,9 +165,11 @@ def setData(self, index, value, role=Qt.EditRole): row = self._to_row_index(index) column = self._to_column_index(index) value_type = self.model.get_value_type(row, column) + if not value_type: + return False if role == Qt.EditRole: - if value_type.get_is_editable(self.model, row, column): + if value_type.can_edit(self.model, row, column): return value_type.set_editable(self.model, row, column, value) elif role == Qt.TextRole: if value_type.has_text(self.model, row, column): From 5b4b43a040d0c46d6228f275c5d6fb421dcdb5ec Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Tue, 16 Jun 2020 17:29:12 +0100 Subject: [PATCH 07/52] More and better tests. --- .../value_types/tests/test_no_value.py | 28 +++++++++++++++++++ .../value_types/tests/test_text_value.py | 2 ++ 2 files changed, 30 insertions(+) create mode 100644 pyface/data_view/value_types/tests/test_no_value.py diff --git a/pyface/data_view/value_types/tests/test_no_value.py b/pyface/data_view/value_types/tests/test_no_value.py new file mode 100644 index 000000000..1a2db8a64 --- /dev/null +++ b/pyface/data_view/value_types/tests/test_no_value.py @@ -0,0 +1,28 @@ +# (C) Copyright 2005-2020 Enthought, Inc., Austin, TX +# All rights reserved. +# +# This software is provided without warranty under the terms of the BSD +# license included in LICENSE.txt and may be redistributed only under +# the conditions described in the aforementioned license. The license +# is also available online at http://www.enthought.com/licenses/BSD.txt +# +# Thanks for using Enthought open source! + +from unittest import TestCase +from unittest.mock import Mock + +from ..no_value import NoValue + + +class TestNoValue(TestCase): + + def setUp(self): + self.model = Mock() + + def test_can_edit(self): + value_type = NoValue() + self.assertFalse(value_type.can_edit(self.model, [0], [0])) + + def test_has_text(self): + value_type = NoValue() + self.assertFalse(value_type.has_text(self.model, [0], [0])) diff --git a/pyface/data_view/value_types/tests/test_text_value.py b/pyface/data_view/value_types/tests/test_text_value.py index c4f02b0b5..a875b9559 100644 --- a/pyface/data_view/value_types/tests/test_text_value.py +++ b/pyface/data_view/value_types/tests/test_text_value.py @@ -9,10 +9,12 @@ class TestTextValue(TestCase): def setUp(self): self.model = Mock() self.model.get_value = Mock(return_value="test") + self.model.can_set_value = Mock(return_value=True) self.model.set_value = Mock(return_value=True) def test_defaults(self): value = TextValue() + self.assertTrue(value.is_editable) def test_is_valid(self): value = TextValue() From 15554b200f7ea53285e1f121dcf25a6402ed6186 Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Wed, 17 Jun 2020 09:05:56 +0100 Subject: [PATCH 08/52] Fix up wx widget scaffolding. --- pyface/ui/wx/data_view/data_view_model.py | 19 ++++++++--- pyface/ui/wx/data_view/data_view_widget.py | 39 +++++++++------------- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/pyface/ui/wx/data_view/data_view_model.py b/pyface/ui/wx/data_view/data_view_model.py index a25bb0350..e563bc136 100644 --- a/pyface/ui/wx/data_view/data_view_model.py +++ b/pyface/ui/wx/data_view/data_view_model.py @@ -119,12 +119,21 @@ def HasChildren(self, item): def GetValue(self, item, column): row_index = self._to_row_index(item) - column_index = [column] - return self.model.get_text(row_index, column_index) + if column == 0: + column_index = [] + else: + column_index = [column - 1] + value_type = self.model.get_value_type(row_index, column_index) + if value_type.has_text(self.model, row_index, column_index): + return value_type.get_text(self.model, row_index, column_index) + return '' def SetValue(self, value, item, column): row_index = self._to_row_index(item) - column_index = [column] + if column == 0: + column_index = [] + else: + column_index = [column - 1] try: result = self.model.set_text(row_index, column_index, value) except Exception as exc: @@ -134,10 +143,10 @@ def SetValue(self, value, item, column): return result def GetColumnCount(self): - return self.model.get_column_count([]) + return self.model.get_column_count([]) + 1 def GetColumnType(self, column): - value_type = self.model.get_column_value_type([column]) + value_type = self.model.get_value_type([], [column-1]) return type_hint_to_variant.get(value_type.type_hint, "string") def _to_row_index(self, item): diff --git a/pyface/ui/wx/data_view/data_view_widget.py b/pyface/ui/wx/data_view/data_view_widget.py index 1945d4a17..dad413f78 100644 --- a/pyface/ui/wx/data_view/data_view_widget.py +++ b/pyface/ui/wx/data_view/data_view_widget.py @@ -12,7 +12,7 @@ from wx.dataview import ( DataViewCtrl, DataViewModel as wxDataViewModel, DATAVIEW_CELL_EDITABLE, - EVT_DATAVIEW_ITEM_ACTIVATED + DATAVIEW_CELL_ACTIVATABLE, EVT_DATAVIEW_ITEM_ACTIVATED ) from pyface.data_view.abstract_data_model import AbstractDataModel from pyface.data_view.i_data_view_widget import ( @@ -38,10 +38,17 @@ def _create_control(self, parent): self._item_model.DecRef() # create columns for view - for column in range(self._item_model.GetColumnCount()): + value_type = self._item_model.model.get_value_type([], []) + control.AppendTextColumn( + value_type.get_text(self._item_model.model, [], []), + 0, + mode=DATAVIEW_CELL_ACTIVATABLE, + ) + for column in range(self._item_model.GetColumnCount()-1): + value_type = self._item_model.model.get_value_type([], [column]) control.AppendTextColumn( - self._item_model.model.get_text([], [column]), - column, + value_type.get_text(self._item_model.model, [], [column]), + column+1, mode=DATAVIEW_CELL_EDITABLE, ) return control @@ -49,37 +56,21 @@ def _create_control(self, parent): def _create_item_model(self): self._item_model = DataViewModel(self.data_model) - def _add_event_listeners(self): - """ Set up toolkit-specific bindings for events """ - super()._add_event_listeners() - self.control.Bind(EVT_DATAVIEW_ITEM_ACTIVATED, self.activated) - - def _remove_event_listeners(self): - """ Remove toolkit-specific bindings for events """ - self.control.Unbind(EVT_DATAVIEW_ITEM_ACTIVATED, self.activated) - super()._remove_event_listeners() - def destroy(self): if self.control is not None: # unhook things here self._item_model = None super().destroy() - def activated(self, event): - print('activated') - if self.control is not None: - print(event.GetPosition()) - column = self.control.GetColumns()[event.GetColumn()] - print(event.GetColumn()) - #self.control.EditItem(event.GetItem(), column) - def _get_control_header_visible(self): """ Toolkit specific method to get the control's tooltip. """ - #return not self.control.isHeaderHidden() + # need to read DV_NO_HEADER + pass def _set_control_header_visible(self, tooltip): """ Toolkit specific method to set the control's tooltip. """ - #self.control.setHeaderHidden(not tooltip) + # need to toggle DV_NO_HEADER + pass @observe('data_model', dispatch='ui') def update_item_model(self, event): From f06bd98517d4578497c472c1a295c8a58c1ae73b Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Wed, 17 Jun 2020 15:21:53 +0100 Subject: [PATCH 09/52] Add documentation for the DataModel with an example. --- docs/source/data_view.rst | 157 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 docs/source/data_view.rst diff --git a/docs/source/data_view.rst b/docs/source/data_view.rst new file mode 100644 index 000000000..0270a88c7 --- /dev/null +++ b/docs/source/data_view.rst @@ -0,0 +1,157 @@ +Pyface DataViews +================= + +The Pyface DataView API is allows visualization of heirarchical and +non-heirarchical tabular data. + +Data Models +----------- + +Data to be viewed needs to be exposed to the DataView infrastructure by +creating a data model for it. This is a class that implements the +interface of |AbstractDataModel|. + +A data model for a dictionary could be implemented like this:: + + class DictDataModel(AbstractDataModel): + + data = Dict + + index_manager = Instance(IntIndexManager, ()) + +The index manager is an ``IntIndexManager`` because the data is +non-heirarchical and this is more memory-efficient than the alternative +``TupleIndexManager``. + +Data Structure +~~~~~~~~~~~~~~ + +The keys are displayed in the row headers, so each row has one column +displaying the value:: + + def get_column_count(self, row): + return 1 + +The data is non-heirarchical, so on the root has children and the number +of child rows of the root is the length of the dictionary:: + + def can_have_children(self, row): + if len(row) == 0: + return True + return False + + def get_row_count(self, row): + if len(row) == 0: + return len(self.data) + return False + +Data Values +~~~~~~~~~~~ + +To get the values of the data model, we need to find the apprpriate value +from the dictionary:: + + keys_header = Str("Keys") + values_header = Str("Values") + + def get_value(self, row, column): + if len(row) == 0: + # this is a column header + if len(row) == 0: + # title of the row headers + return self.keys_header + else: + return self.values_header + else: + row_index = row[0] + key, value = list(self.data.items())[row_index] + if len(column) == 0: + # the is a row header, so get the key + return key + else: + return value + +In this case, all of the values are text, and read-only, so we can have a +trait holding the value type, and return that for every item:: + + header_value_type = Instance(AbstractValueType) + key_value_type = Instance(AbstractValueType) + value_type = Instance(AbstractValueType) + + def _default_header_value_type(self): + return TextValue(is_editable=False) + + def _default_key_value_type(self): + return TextValue(is_editable=False) + + def _default_value_type(self): + return TextValue(is_editable=False) + + def get_value_type(self, row, column): + if len(row) == 0: + return self.header_value_type + elif len(column) == 0: + return self.key_value_type + else: + return self.value_type + +The default assumes that values representable as text, but if the values were +ints, for example then the class could be instantiated with:: + + model = DictDataModel(value_type=IntValue(is_editable=False)) + +The ``is_editable`` arguments are not strictly needed, as the default +implementation of |can_set_value| returns False, but if we wanted to make the +data model read-write we would need to write an implementation of +|can_set_value| which returns True for editable items, and an implementation +of |set_value| that updates the data in-place. This would look something like:: + + def can_set_value(self, row, column): + return len(row) != 0 and len(column) != 0: + + def set_value(self, row, column, value): + if len(row) == 0 or len(column) == 0: + return False + row_index = row[0] + key = list(self.data)[row_index] + self.data[key] = value + return True + +Update Events +------------- + +Finally, when the underlying data changes, the DataView infrastructure expects +certain event traits to be fired. If a value is changed, or the value type is +updated, but the number of rows and columns is unaffected, then the +``values_changed`` trait should be fired with a tuple of ``(start_row_index, +start_column_index, end_row_index, end_column_index)``. If a major change has +occurred, or if the size, shape or layout of the data has changed, then +the ``structure_changed`` event should be fired with a simple ``True`` value. + +So for example, if the value types change, only the displayed values need to be +updated:: + + @observe('header_value_type.updated') + def header_values_updated(self, event): + self.values_changed = ([], [], [], [0]) + + @observe('key_value_type.updated') + def key_values_updated(self, event): + self.values_changed = ([0], [], [len(self.data) - 1], []) + + @observe('value_type.updated') + def values_updated(self, event): + self.values_changed = ([0], [0], [len(self.data) - 1], [0]) + +On the other hand, if the dictionary or its items change, then it is simplest +to just indicate that the entire view needs updating:: + + @observe('data.items') + def data_updated(self, event): + self.structure_changed = True + + + +.. substitute:: AbstractDataModel :py:class:`~pyface.data_view.abstract_data_model.AbstractDataModel` +.. substitute:: can_set_value :py:class:`~pyface.data_view.abstract_data_model.AbstractDataModel.can_set_value` +.. substitute:: set_value :py:class:`~pyface.data_view.abstract_data_model.AbstractDataModel.set_value` From bc36909be753e5246a9e56667c7841a7e1c43caa Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Wed, 17 Jun 2020 17:44:34 +0100 Subject: [PATCH 10/52] Fix references. --- docs/source/data_view.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/data_view.rst b/docs/source/data_view.rst index 0270a88c7..dea2046e0 100644 --- a/docs/source/data_view.rst +++ b/docs/source/data_view.rst @@ -152,6 +152,6 @@ to just indicate that the entire view needs updating:: -.. substitute:: AbstractDataModel :py:class:`~pyface.data_view.abstract_data_model.AbstractDataModel` -.. substitute:: can_set_value :py:class:`~pyface.data_view.abstract_data_model.AbstractDataModel.can_set_value` -.. substitute:: set_value :py:class:`~pyface.data_view.abstract_data_model.AbstractDataModel.set_value` +.. |AbstractDataModel| replace:: :py:class:`~pyface.data_view.abstract_data_model.AbstractDataModel` +.. |can_set_value| replace:: :py:class:`~pyface.data_view.abstract_data_model.AbstractDataModel.can_set_value` +.. |set_value| replace:: :py:class:`~pyface.data_view.abstract_data_model.AbstractDataModel.set_value` From cce14b1612c4d04d107de3ad9bc5ada03f60821f Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Thu, 18 Jun 2020 09:22:49 +0100 Subject: [PATCH 11/52] Move the column data model to the examples, clean up example. --- .../data_view}/column_data_model.py | 0 examples/data_view/column_example.py | 257 ++++-------------- pyface/data_view/data_models/api.py | 11 +- 3 files changed, 69 insertions(+), 199 deletions(-) rename {pyface/data_view/data_models => examples/data_view}/column_data_model.py (100%) diff --git a/pyface/data_view/data_models/column_data_model.py b/examples/data_view/column_data_model.py similarity index 100% rename from pyface/data_view/data_models/column_data_model.py rename to examples/data_view/column_data_model.py diff --git a/examples/data_view/column_example.py b/examples/data_view/column_example.py index a03617a4a..3f066fdac 100644 --- a/examples/data_view/column_example.py +++ b/examples/data_view/column_example.py @@ -8,18 +8,19 @@ # # Thanks for using Enthought open source! +from functools import partial from random import choice, randint from traits.api import HasStrictTraits, Instance, Int, Str, List from pyface.api import ApplicationWindow, GUI -from pyface.data_view.abstract_value_type import AbstractValueType, none_value -from pyface.data_view.data_models.column_data_model import ( - AbstractRowInfo, ColumnDataModel, HasTraitsRowInfo -) from pyface.data_view.i_data_view_widget import IDataViewWidget from pyface.data_view.data_view_widget import DataViewWidget -from pyface.data_view.value_types.api import IntValue, TextValue +from pyface.data_view.value_types.api import IntValue, TextValue, no_value + +from column_data_model import ( + AbstractRowInfo, ColumnDataModel, HasTraitsRowInfo +) class Address(HasStrictTraits): @@ -52,7 +53,7 @@ class Person(HasStrictTraits): ), HasTraitsRowInfo( title="Address", - value_type=none_value, + value_type=no_value, value='address', rows=[ HasTraitsRowInfo( @@ -94,7 +95,6 @@ def _create_contents(self, parent): data=self.data, row_info=self.row_info, ), - #header_visible=False, ) self.data_view._create() return self.data_view.control @@ -103,202 +103,63 @@ def _data_default(self): import numpy return numpy.random.uniform(size=(100000, 10)) + male_names = [ - 'Michael', - 'Edward', - 'Timothy', - 'James', - 'George', - 'Ralph', - 'David', - 'Martin', - 'Bryce', - 'Richard', - 'Eric', - 'Travis', - 'Robert', - 'Bryan', - 'Alan', - 'Harold', - 'John', - 'Stephen', - 'Gael', - 'Frederic', - 'Eli', - 'Scott', - 'Samuel', - 'Alexander', - 'Tobias', - 'Sven', - 'Peter', - 'Albert', - 'Thomas', - 'Horatio', - 'Julius', - 'Henry', - 'Walter', - 'Woodrow', - 'Dylan', - 'Elmer'] + 'Michael', 'Edward', 'Timothy', 'James', 'George', 'Ralph', 'David', + 'Martin', 'Bryce', 'Richard', 'Eric', 'Travis', 'Robert', 'Bryan', + 'Alan', 'Harold', 'John', 'Stephen', 'Gael', 'Frederic', 'Eli', 'Scott', + 'Samuel', 'Alexander', 'Tobias', 'Sven', 'Peter', 'Albert', 'Thomas', + 'Horatio', 'Julius', 'Henry', 'Walter', 'Woodrow', 'Dylan', 'Elmer', +] female_names = [ - 'Leah', - 'Jaya', - 'Katrina', - 'Vibha', - 'Diane', - 'Lisa', - 'Jean', - 'Alice', - 'Rebecca', - 'Delia', - 'Christine', - 'Marie', - 'Dorothy', - 'Ellen', - 'Victoria', - 'Elizabeth', - 'Margaret', - 'Joyce', - 'Sally', - 'Ethel', - 'Esther', - 'Suzanne', - 'Monica', - 'Hortense', - 'Samantha', - 'Tabitha', - 'Judith', - 'Ariel', - 'Helen', - 'Mary', - 'Jane', - 'Janet', - 'Jennifer', - 'Rita', - 'Rena', - 'Rianna'] + 'Leah', 'Jaya', 'Katrina', 'Vibha', 'Diane', 'Lisa', 'Jean', 'Alice', + 'Rebecca', 'Delia', 'Christine', 'Marie', 'Dorothy', 'Ellen', 'Victoria', + 'Elizabeth', 'Margaret', 'Joyce', 'Sally', 'Ethel', 'Esther', 'Suzanne', + 'Monica', 'Hortense', 'Samantha', 'Tabitha', 'Judith', 'Ariel', 'Helen', + 'Mary', 'Jane', 'Janet', 'Jennifer', 'Rita', 'Rena', 'Rianna', +] all_names = male_names + female_names - -male_name = lambda: choice(male_names) -female_name = lambda: choice(female_names) -any_name = lambda: choice(all_names) -age = lambda: randint(15, 72) - -family_name = lambda: choice(['Jones', - 'Smith', - 'Thompson', - 'Hayes', - 'Thomas', - 'Boyle', - "O'Reilly", - 'Lebowski', - 'Lennon', - 'Starr', - 'McCartney', - 'Harrison', - 'Harrelson', - 'Steinbeck', - 'Rand', - 'Hemingway', - 'Zhivago', - 'Clemens', - 'Heinlien', - 'Farmer', - 'Niven', - 'Van Vogt', - 'Sturbridge', - 'Washington', - 'Adams', - 'Bush', - 'Kennedy', - 'Ford', - 'Lincoln', - 'Jackson', - 'Johnson', - 'Eisenhower', - 'Truman', - 'Roosevelt', - 'Wilson', - 'Coolidge', - 'Mack', - 'Moon', - 'Monroe', - 'Springsteen', - 'Rigby', - "O'Neil", - 'Philips', - 'Clinton', - 'Clapton', - 'Santana', - 'Midler', - 'Flack', - 'Conner', - 'Bond', - 'Seinfeld', - 'Costanza', - 'Kramer', - 'Falk', - 'Moore', - 'Cramdon', - 'Baird', - 'Baer', - 'Spears', - 'Simmons', - 'Roberts', - 'Michaels', - 'Stuart', - 'Montague', - 'Miller']) - -street = lambda: '%d %s %s' % (randint(11, - 999), - choice(['Spring', - 'Summer', - 'Moonlight', - 'Winding', - 'Windy', - 'Whispering', - 'Falling', - 'Roaring', - 'Hummingbird', - 'Mockingbird', - 'Bluebird', - 'Robin', - 'Babbling', - 'Cedar', - 'Pine', - 'Ash', - 'Maple', - 'Oak', - 'Birch', - 'Cherry', - 'Blossom', - 'Rosewood', - 'Apple', - 'Peach', - 'Blackberry', - 'Strawberry', - 'Starlight', - 'Wilderness', - 'Dappled', - 'Beaver', - 'Acorn', - 'Pecan', - 'Pheasant', - 'Owl']), - choice(['Way', - 'Lane', - 'Boulevard', - 'Street', - 'Drive', - 'Circle', - 'Avenue', - 'Trail'])) - -city = lambda: choice(['Boston', 'Cambridge', ]) -country = lambda: choice(['USA', 'UK']) +any_name = partial(choice, all_names) +age = partial(randint, 15, 72) + + +def family_name(): + return choice([ + 'Jones', 'Smith', 'Thompson', 'Hayes', 'Thomas', 'Boyle', "O'Reilly", + 'Lebowski', 'Lennon', 'Starr', 'McCartney', 'Harrison', 'Harrelson', + 'Steinbeck', 'Rand', 'Hemingway', 'Zhivago', 'Clemens', 'Heinlien', + 'Farmer', 'Niven', 'Van Vogt', 'Sturbridge', 'Washington', 'Adams', + 'Bush', 'Kennedy', 'Ford', 'Lincoln', 'Jackson', 'Johnson', + 'Eisenhower', 'Truman', 'Roosevelt', 'Wilson', 'Coolidge', 'Mack', + 'Moon', 'Monroe', 'Springsteen', 'Rigby', "O'Neil", 'Philips', + 'Clinton', 'Clapton', 'Santana', 'Midler', 'Flack', 'Conner', 'Bond', + 'Seinfeld', 'Costanza', 'Kramer', 'Falk', 'Moore', 'Cramdon', 'Baird', + 'Baer', 'Spears', 'Simmons', 'Roberts', 'Michaels', 'Stuart', + 'Montague', 'Miller', + ]) + + +def street(): + number = randint(11, 999) + text_1 = choice([ + 'Spring', 'Summer', 'Moonlight', 'Winding', 'Windy', 'Whispering', + 'Falling', 'Roaring', 'Hummingbird', 'Mockingbird', 'Bluebird', + 'Robin', 'Babbling', 'Cedar', 'Pine', 'Ash', 'Maple', 'Oak', 'Birch', + 'Cherry', 'Blossom', 'Rosewood', 'Apple', 'Peach', 'Blackberry', + 'Strawberry', 'Starlight', 'Wilderness', 'Dappled', 'Beaver', 'Acorn', + 'Pecan', 'Pheasant', 'Owl' + ]) + text_2 = choice([ + 'Way', 'Lane', 'Boulevard', 'Street', 'Drive', 'Circle', 'Avenue', + 'Trail', + ]) + return '%d %s %s' % (number, text_1, text_2) + + +city = partial(choice, ['Boston', 'Cambridge', ]) +country = partial(choice, ['USA', 'UK']) people = [ Person( diff --git a/pyface/data_view/data_models/api.py b/pyface/data_view/data_models/api.py index 9104a74f0..99c9c9d73 100644 --- a/pyface/data_view/data_models/api.py +++ b/pyface/data_view/data_models/api.py @@ -1,2 +1,11 @@ +# (C) Copyright 2005-2020 Enthought, Inc., Austin, TX +# All rights reserved. +# +# This software is provided without warranty under the terms of the BSD +# license included in LICENSE.txt and may be redistributed only under +# the conditions described in the aforementioned license. The license +# is also available online at http://www.enthought.com/licenses/BSD.txt +# +# Thanks for using Enthought open source! + from .array_data_model import ArrayDataModel -from .column_data_model import ColumnDataModel \ No newline at end of file From a5616459d16bc00ab1b0e0ce2f23614c21dbf95c Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Thu, 18 Jun 2020 09:23:51 +0100 Subject: [PATCH 12/52] Add copyright headers. --- pyface/data_view/value_types/api.py | 10 +++++++++- .../data_view/value_types/tests/test_numeric_value.py | 10 ++++++++++ pyface/data_view/value_types/tests/test_text_value.py | 10 ++++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/pyface/data_view/value_types/api.py b/pyface/data_view/value_types/api.py index 26221fa97..a8485756b 100644 --- a/pyface/data_view/value_types/api.py +++ b/pyface/data_view/value_types/api.py @@ -1,4 +1,12 @@ - +# (C) Copyright 2005-2020 Enthought, Inc., Austin, TX +# All rights reserved. +# +# This software is provided without warranty under the terms of the BSD +# license included in LICENSE.txt and may be redistributed only under +# the conditions described in the aforementioned license. The license +# is also available online at http://www.enthought.com/licenses/BSD.txt +# +# Thanks for using Enthought open source! from .constant_value import ConstantValue from .editable_value import EditableValue diff --git a/pyface/data_view/value_types/tests/test_numeric_value.py b/pyface/data_view/value_types/tests/test_numeric_value.py index 16c6c7bd3..7611dcd93 100644 --- a/pyface/data_view/value_types/tests/test_numeric_value.py +++ b/pyface/data_view/value_types/tests/test_numeric_value.py @@ -1,3 +1,13 @@ +# (C) Copyright 2005-2020 Enthought, Inc., Austin, TX +# All rights reserved. +# +# This software is provided without warranty under the terms of the BSD +# license included in LICENSE.txt and may be redistributed only under +# the conditions described in the aforementioned license. The license +# is also available online at http://www.enthought.com/licenses/BSD.txt +# +# Thanks for using Enthought open source! + from unittest import TestCase from unittest.mock import Mock diff --git a/pyface/data_view/value_types/tests/test_text_value.py b/pyface/data_view/value_types/tests/test_text_value.py index a875b9559..a0f8a8847 100644 --- a/pyface/data_view/value_types/tests/test_text_value.py +++ b/pyface/data_view/value_types/tests/test_text_value.py @@ -1,3 +1,13 @@ +# (C) Copyright 2005-2020 Enthought, Inc., Austin, TX +# All rights reserved. +# +# This software is provided without warranty under the terms of the BSD +# license included in LICENSE.txt and may be redistributed only under +# the conditions described in the aforementioned license. The license +# is also available online at http://www.enthought.com/licenses/BSD.txt +# +# Thanks for using Enthought open source! + from unittest import TestCase from unittest.mock import Mock From 45fd7ed029ae2d74a32ea9c7f25004e2535b4535 Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Thu, 18 Jun 2020 09:52:09 +0100 Subject: [PATCH 13/52] Add tests for remaining value type classes. --- .../tests/test_abstract_value_type.py | 68 ++++++++++++++++++ .../value_types/tests/test_editable_value.py | 70 +++++++++++++++++++ 2 files changed, 138 insertions(+) create mode 100644 pyface/data_view/tests/test_abstract_value_type.py create mode 100644 pyface/data_view/value_types/tests/test_editable_value.py diff --git a/pyface/data_view/tests/test_abstract_value_type.py b/pyface/data_view/tests/test_abstract_value_type.py new file mode 100644 index 000000000..1b9d08c9b --- /dev/null +++ b/pyface/data_view/tests/test_abstract_value_type.py @@ -0,0 +1,68 @@ +# (C) Copyright 2005-2020 Enthought, Inc., Austin, TX +# All rights reserved. +# +# This software is provided without warranty under the terms of the BSD +# license included in LICENSE.txt and may be redistributed only under +# the conditions described in the aforementioned license. The license +# is also available online at http://www.enthought.com/licenses/BSD.txt +# +# Thanks for using Enthought open source! + +from unittest import TestCase +from unittest.mock import Mock + +from traits.api import Str +from traits.testing.unittest_tools import UnittestTools + +from ..abstract_value_type import AbstractValueType + + +class ValueType(AbstractValueType): + + #: a parameter which should fire the update trait + sample_parameter = Str(update=True) + + +class TestAbstractValueType(UnittestTools, TestCase): + + def setUp(self): + self.model = Mock() + self.model.get_value = Mock(return_value=1.0) + self.model.can_set_value = Mock(return_value=True) + self.model.set_value = Mock(return_value=True) + + def test_can_edit(self): + value_type = ValueType() + result = value_type.can_edit(self.model, [0], [0]) + self.assertTrue(result) + + def test_get_editable(self): + value_type = ValueType() + result = value_type.get_editable(self.model, [0], [0]) + self.assertEqual(result, 1.0) + + def test_set_editable(self): + value_type = ValueType() + result = value_type.set_editable(self.model, [0], [0], 2.0) + self.assertTrue(result) + + def test_set_editable_can_edit_false(self): + self.model.can_set_value = Mock(return_value=False) + value_type = ValueType() + result = value_type.set_editable(self.model, [0], [0], 2.0) + self.assertFalse(result) + + def test_get_text(self): + value_type = ValueType() + result = value_type.get_text(self.model, [0], [0]) + self.assertEqual(result, "1.0") + + def test_set_text(self): + value_type = ValueType() + result = value_type.set_text(self.model, [0], [0], "2.0") + self.assertFalse(result) + + def test_parameter_update(self): + value_type = ValueType() + with self.assertTraitChanges(value_type, 'updated', count=1): + value_type.sample_parameter = "new value" diff --git a/pyface/data_view/value_types/tests/test_editable_value.py b/pyface/data_view/value_types/tests/test_editable_value.py new file mode 100644 index 000000000..dc11432a9 --- /dev/null +++ b/pyface/data_view/value_types/tests/test_editable_value.py @@ -0,0 +1,70 @@ +# (C) Copyright 2005-2020 Enthought, Inc., Austin, TX +# All rights reserved. +# +# This software is provided without warranty under the terms of the BSD +# license included in LICENSE.txt and may be redistributed only under +# the conditions described in the aforementioned license. The license +# is also available online at http://www.enthought.com/licenses/BSD.txt +# +# Thanks for using Enthought open source! + +from unittest import TestCase +from unittest.mock import Mock + +from traits.testing.unittest_tools import UnittestTools + +from ..editable_value import EditableValue + + +class EditableWithValid(EditableValue): + + def is_valid(self, model, row, column, value): + return value >= 0 + + +class TestAbstractValueType(UnittestTools, TestCase): + + def setUp(self): + self.model = Mock() + self.model.get_value = Mock(return_value=1.0) + self.model.can_set_value = Mock(return_value=True) + self.model.set_value = Mock(return_value=True) + + def test_default(self): + value_type = EditableValue() + self.assertTrue(value_type.is_editable) + + def test_is_valid(self): + value_type = EditableValue() + result = value_type.is_valid(self.model, [0], [0], 2.0) + self.assertTrue(result) + + def test_can_edit(self): + value_type = EditableValue() + result = value_type.can_edit(self.model, [0], [0]) + self.assertTrue(result) + + def test_can_edit_not_editable(self): + value_type = EditableValue(is_editable=False) + result = value_type.can_edit(self.model, [0], [0]) + self.assertFalse(result) + + def test_set_editable(self): + value_type = EditableValue() + result = value_type.set_editable(self.model, [0], [0], 2.0) + self.assertTrue(result) + + def test_set_editable_not_editable(self): + value_type = EditableValue(is_editable=False) + result = value_type.set_editable(self.model, [0], [0], 2.0) + self.assertFalse(result) + + def test_set_editable_not_valid(self): + value_type = EditableWithValid() + result = value_type.set_editable(self.model, [0], [0], -1.0) + self.assertFalse(result) + + def test_is_editable_update(self): + value_type = EditableValue() + with self.assertTraitChanges(value_type, 'updated', count=1): + value_type.is_editable = False From 9208713cd31ec8dc8e93ad9c9efea5a022f5be47 Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Mon, 22 Jun 2020 11:14:38 +0100 Subject: [PATCH 14/52] Apply suggestions from code review Co-authored-by: Ieva --- docs/source/data_view.rst | 2 +- pyface/data_view/data_models/array_data_model.py | 4 ++-- pyface/data_view/value_types/numeric_value.py | 2 +- pyface/data_view/value_types/tests/test_editable_value.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/source/data_view.rst b/docs/source/data_view.rst index dea2046e0..5b07a3cf2 100644 --- a/docs/source/data_view.rst +++ b/docs/source/data_view.rst @@ -1,7 +1,7 @@ Pyface DataViews ================= -The Pyface DataView API is allows visualization of heirarchical and +The Pyface DataView API allows visualization of heirarchical and non-heirarchical tabular data. Data Models diff --git a/pyface/data_view/data_models/array_data_model.py b/pyface/data_view/data_models/array_data_model.py index c06f874d2..f5aab78ff 100644 --- a/pyface/data_view/data_models/array_data_model.py +++ b/pyface/data_view/data_models/array_data_model.py @@ -9,7 +9,7 @@ # Thanks for using Enthought open source! """ Provides an N-dimensional array data model implementation. -This module provides an a concrete implementation of a data model for an +This module provides a concrete implementation of a data model for an n-dim numpy array. """ from traits.api import Array, Instance, observe @@ -258,4 +258,4 @@ def _value_type_default(self): elif np.issubdtype(scalar_type, np.character): return TextValue() - return TextValue(is_editable=False) \ No newline at end of file + return TextValue(is_editable=False) diff --git a/pyface/data_view/value_types/numeric_value.py b/pyface/data_view/value_types/numeric_value.py index 1dedf7e1e..fdbf83ba0 100644 --- a/pyface/data_view/value_types/numeric_value.py +++ b/pyface/data_view/value_types/numeric_value.py @@ -30,7 +30,7 @@ class NumericValue(EditableValue): #: The maximum value for the numeric value. maximum = Float(inf) - #: A function that converts to the a numeric type. + #: A function that converts to the numeric type. evaluate = Callable() #: A function that converts the required type to a string for display. diff --git a/pyface/data_view/value_types/tests/test_editable_value.py b/pyface/data_view/value_types/tests/test_editable_value.py index dc11432a9..1acc89974 100644 --- a/pyface/data_view/value_types/tests/test_editable_value.py +++ b/pyface/data_view/value_types/tests/test_editable_value.py @@ -22,7 +22,7 @@ def is_valid(self, model, row, column, value): return value >= 0 -class TestAbstractValueType(UnittestTools, TestCase): +class TestEditableValue(UnittestTools, TestCase): def setUp(self): self.model = Mock() From 597f86949b17fe80dfcef49f2ec236b158ee478f Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Tue, 23 Jun 2020 10:59:37 +0100 Subject: [PATCH 15/52] Updates and fixes from PR review. --- docs/source/data_view.rst | 22 ++- examples/data_view/array_example.py | 1 - examples/data_view/column_data_model.py | 4 +- pyface/data_view/abstract_data_model.py | 5 +- pyface/data_view/abstract_value_type.py | 2 +- pyface/data_view/data_models/api.py | 2 +- .../data_view/data_models/array_data_model.py | 50 +++-- .../tests/test_array_data_model.py | 185 ++++++++++++++++-- pyface/data_view/data_view_widget.py | 2 +- pyface/data_view/i_data_view_widget.py | 4 - pyface/data_view/index_manager.py | 6 +- .../tests/test_abstract_value_type.py | 7 +- pyface/data_view/tests/test_index_manager.py | 16 +- pyface/data_view/value_types/api.py | 10 +- .../data_view/value_types/constant_value.py | 1 - pyface/data_view/value_types/numeric_value.py | 9 - .../value_types/tests/test_constant_value.py | 7 +- .../value_types/tests/test_editable_value.py | 2 +- .../value_types/tests/test_no_value.py | 2 +- .../value_types/tests/test_numeric_value.py | 4 +- .../value_types/tests/test_text_value.py | 3 +- .../ui/qt4/data_view/data_view_item_model.py | 3 +- pyface/ui/qt4/data_view/data_view_widget.py | 3 - pyface/ui/wx/data_view/data_view_widget.py | 8 +- 24 files changed, 258 insertions(+), 100 deletions(-) diff --git a/docs/source/data_view.rst b/docs/source/data_view.rst index 5b07a3cf2..f9771a9df 100644 --- a/docs/source/data_view.rst +++ b/docs/source/data_view.rst @@ -32,18 +32,20 @@ displaying the value:: def get_column_count(self, row): return 1 -The data is non-heirarchical, so on the root has children and the number -of child rows of the root is the length of the dictionary:: +The data is non-heirarchical, so the root has children and no other +rows have children:: def can_have_children(self, row): if len(row) == 0: return True return False +The number of child rows of the root is the length of the dictionary:: + def get_row_count(self, row): if len(row) == 0: return len(self.data) - return False + return 0 Data Values ~~~~~~~~~~~ @@ -57,7 +59,7 @@ from the dictionary:: def get_value(self, row, column): if len(row) == 0: # this is a column header - if len(row) == 0: + if len(column) == 0: # title of the row headers return self.keys_header else: @@ -110,12 +112,12 @@ of |set_value| that updates the data in-place. This would look something like:: return len(row) != 0 and len(column) != 0: def set_value(self, row, column, value): - if len(row) == 0 or len(column) == 0: - return False - row_index = row[0] - key = list(self.data)[row_index] - self.data[key] = value - return True + if self.can_set_value(row, column): + row_index = row[0] + key = list(self.data)[row_index] + self.data[key] = value + return True + return False Update Events ------------- diff --git a/examples/data_view/array_example.py b/examples/data_view/array_example.py index a03cafa66..84f07659e 100644 --- a/examples/data_view/array_example.py +++ b/examples/data_view/array_example.py @@ -29,7 +29,6 @@ def _create_contents(self, parent): self.data_view = DataViewWidget( parent=parent, data_model=ArrayDataModel(data=self.data), - #header_visible=False, ) self.data_view._create() return self.data_view.control diff --git a/examples/data_view/column_data_model.py b/examples/data_view/column_data_model.py index fc3df8a51..72ac064bf 100644 --- a/examples/data_view/column_data_model.py +++ b/examples/data_view/column_data_model.py @@ -11,8 +11,8 @@ from abc import abstractmethod from traits.api import ( - ABCHasStrictTraits, Callable, ComparisonMode, Event, HasTraits, Instance, - List, Str, Tuple, observe + ABCHasStrictTraits, ComparisonMode, Event, HasTraits, Instance, + List, Str, observe ) from traits.trait_base import xgetattr, xsetattr diff --git a/pyface/data_view/abstract_data_model.py b/pyface/data_view/abstract_data_model.py index 4d566b2e8..8540c1b2b 100644 --- a/pyface/data_view/abstract_data_model.py +++ b/pyface/data_view/abstract_data_model.py @@ -265,8 +265,7 @@ def iter_items(self, start_row=[]): row_index, column_index The current row and column indices. """ - for row in self.iter_rows(): - if row != []: - yield row, [] + for row in self.iter_rows(start_row): + yield row, [] for column in range(self.get_column_count(row)): yield row, [column] diff --git a/pyface/data_view/abstract_value_type.py b/pyface/data_view/abstract_value_type.py index 9f19a7220..fca436616 100644 --- a/pyface/data_view/abstract_value_type.py +++ b/pyface/data_view/abstract_value_type.py @@ -19,7 +19,7 @@ and how to actually display it. """ -from traits.api import ABCHasStrictTraits, Event, Str, observe +from traits.api import ABCHasStrictTraits, Event, observe class AbstractValueType(ABCHasStrictTraits): diff --git a/pyface/data_view/data_models/api.py b/pyface/data_view/data_models/api.py index 99c9c9d73..4e6ad7712 100644 --- a/pyface/data_view/data_models/api.py +++ b/pyface/data_view/data_models/api.py @@ -8,4 +8,4 @@ # # Thanks for using Enthought open source! -from .array_data_model import ArrayDataModel +from .array_data_model import ArrayDataModel # noqa: F401 diff --git a/pyface/data_view/data_models/array_data_model.py b/pyface/data_view/data_models/array_data_model.py index f5aab78ff..1edfe7ef8 100644 --- a/pyface/data_view/data_models/array_data_model.py +++ b/pyface/data_view/data_models/array_data_model.py @@ -24,15 +24,15 @@ class ArrayDataModel(AbstractDataModel): - #: The array being displayed. + #: The array being displayed. This must have dimension at least 2. data = Array() #: The index manager that helps convert toolkit indices to data view #: indices. index_manager = Instance(TupleIndexManager, args=()) - #: The value type of the column titles. - header_label_type = Instance( + #: The value type of the label headers. + label_header_type = Instance( AbstractValueType, factory=ConstantValue, kw={'text': "Index"}, @@ -45,7 +45,7 @@ class ArrayDataModel(AbstractDataModel): kw={'is_editable': False}, ) - #: The value type of the column titles. + #: The value type of the row titles. row_header_type = Instance( AbstractValueType, factory=IntValue, @@ -58,7 +58,7 @@ class ArrayDataModel(AbstractDataModel): # Data structure methods def get_column_count(self, row): - """ How many columns in the row of the data view model. + """ How many columns in a row of the data view model. The total number of columns in the table is given by the column count of the Root row. @@ -88,7 +88,7 @@ def can_have_children(self, row): can_have_children : bool Whether or not the row can ever have child rows. """ - if len(row) < len(self.data.shape) - 1: + if len(row) < self.data.ndim - 1: return True return False @@ -107,7 +107,7 @@ def get_row_count(self, row): has_children : bool Whether or not the row currently has child rows. """ - if len(row) < len(self.data.shape) - 1: + if len(row) < self.data.ndim - 1: return self.data.shape[len(row)] return 0 @@ -127,12 +127,14 @@ def get_value(self, row, column): The number of child rows that the row has. """ if row == []: + if column == []: + return None return column[0] elif column == []: return row[-1] else: index = tuple(row + column) - if len(index) != len(self.data.shape): + if len(index) != self.data.ndim: return None return self.data[index] @@ -176,13 +178,13 @@ def set_value(self, row, column, value): value : any The value represented by the given row and column. """ - index = tuple(row + column) - if len(index) < self.data.ndim: - return False + if self.can_set_value(row, column): + index = tuple(row + column) + self.data[index] = value + self.values_changed = (row, column, row, column) + return True - self.data[index] = value - self.values_changed = (row, column, row, column) - return True + return False def get_value_type(self, row, column): """ Return the text value for the row and column. @@ -205,12 +207,11 @@ def get_value_type(self, row, column): """ if row == []: if column == []: - return self.header_label_type + return self.label_header_type return self.column_header_type elif column == []: - # XXX not currently used return self.row_header_type - elif len(row) < len(self.data.shape) - 1: + elif len(row) < self.data.ndim - 1: return no_value else: return self.value_type @@ -222,7 +223,7 @@ def data_updated(self, event): """ Handle the array being replaced with a new array. """ if event.new.shape == event.old.shape: self.values_changed = ( - ([0], [0], [event.old.shape[0]], [event.old.shape[1]]) + ([0], [0], [event.old.shape[0] - 1], [event.old.shape[-1] - 1]) ) else: self.structure_changed = True @@ -231,21 +232,28 @@ def data_updated(self, event): def value_type_updated(self, event): """ Handle the value type being updated. """ self.values_changed = ( - ([0], [0], [self.data.shape[0]], [self.data.shape[1]]) + ([0], [0], [self.data.shape[0] - 1], [self.data.shape[-1] - 1]) ) @observe('column_header_type.updated', dispatch='ui') def column_header_type_updated(self, event): """ Handle the header type being updated. """ self.values_changed = ( - ([], [0], [], [self.data.shape[1]]) + ([], [0], [], [self.data.shape[-1] - 1]) ) @observe('row_header_type.updated', dispatch='ui') def value_header_type_updated(self, event): """ Handle the header type being updated. """ self.values_changed = ( - ([0], [], [self.data.shape[0]], []) + ([0], [], [self.data.shape[0] - 1], []) + ) + + @observe('label_header_type.updated', dispatch='ui') + def label_header_type_updated(self, event): + """ Handle the header type being updated. """ + self.values_changed = ( + ([], [], [], []) ) def _value_type_default(self): diff --git a/pyface/data_view/data_models/tests/test_array_data_model.py b/pyface/data_view/data_models/tests/test_array_data_model.py index ee9c48ad1..db90bbda1 100644 --- a/pyface/data_view/data_models/tests/test_array_data_model.py +++ b/pyface/data_view/data_models/tests/test_array_data_model.py @@ -8,14 +8,16 @@ # # Thanks for using Enthought open source! -from unittest import TestCase, expectedFailure +from unittest import TestCase from traits.testing.unittest_tools import UnittestTools from traits.testing.optional_dependencies import numpy as np, requires_numpy from pyface.data_view.abstract_value_type import AbstractValueType -from pyface.data_view.value_types.api import FloatValue, IntValue, TextValue -from ..array_data_model import ArrayDataModel +from pyface.data_view.value_types.api import ( + FloatValue, IntValue, TextValue, no_value +) +from pyface.data_view.data_models.array_data_model import ArrayDataModel @requires_numpy @@ -23,7 +25,7 @@ class TestArrayDataModel(UnittestTools, TestCase): def setUp(self): super().setUp() - self.array = np.arange(15.0).reshape(5, 3) + self.array = np.arange(30.0).reshape(5, 2, 3) self.model = ArrayDataModel(data=self.array) self.values_changed_event = None self.structure_changed_event = None @@ -55,7 +57,7 @@ def test_can_have_children(self): for row in self.model.iter_rows(): with self.subTest(row=row): result = self.model.can_have_children(row) - if len(row) == 0: + if len(row) <= 1: self.assertEqual(result, True) else: self.assertEqual(result, False) @@ -66,6 +68,8 @@ def test_get_row_count(self): result = self.model.get_row_count(row) if len(row) == 0: self.assertEqual(result, 5) + elif len(row) == 1: + self.assertEqual(result, 2) else: self.assertEqual(result, 0) @@ -73,28 +77,46 @@ def test_get_value(self): for row, column in self.model.iter_items(): with self.subTest(row=row, column=column): result = self.model.get_value(row, column) - if row == []: + if row == [] and column == []: + self.assertIsNone(result) + elif row == []: self.assertEqual(result, column[0]) elif column == []: - self.assertEqual(result, row[0]) + self.assertEqual(result, row[-1]) + elif len(row) == 1: + self.assertIsNone(result) else: - self.assertEqual(result, self.array[row[0], column[0]]) + self.assertEqual( + result, + self.array[row[0], row[1], column[0]] + ) def test_set_value(self): for row, column in self.model.iter_items(): with self.subTest(row=row, column=column): - if row == []: + if row == [] and column == []: + result = self.model.set_value(row, column, 0) + self.assertFalse(result) + elif row == []: result = self.model.set_value(row, column, column[0] + 1) self.assertFalse(result) elif column == []: - result = self.model.set_value(row, column, row[0] + 1) + result = self.model.set_value(row, column, row[-1] + 1) self.assertFalse(result) + elif len(row) == 1: + value = 6.0 * row[-1] + 2 * column[0] + with self.assertTraitDoesNotChange( + self.model, "values_changed"): + result = self.model.set_value(row, column, value) else: - value = 6.0 * row[0] + 2 * column[0] + value = 6.0 * row[-1] + 2 * column[0] with self.assertTraitChanges(self.model, "values_changed"): result = self.model.set_value(row, column, value) self.assertTrue(result) - self.assertEqual(self.array[row[0], column[0]], value) + self.assertEqual( + self.array[row[0], row[1], column[0]], + value, + ) self.assertEqual( self.values_changed_event.new, (row, column, row, column) @@ -104,12 +126,17 @@ def test_get_value_type(self): for row, column in self.model.iter_items(): with self.subTest(row=row, column=column): result = self.model.get_value_type(row, column) - if row == []: + if row == [] and column == []: + self.assertIsInstance(result, AbstractValueType) + self.assertIs(result, self.model.label_header_type) + elif row == []: self.assertIsInstance(result, AbstractValueType) self.assertIs(result, self.model.column_header_type) elif column == []: self.assertIsInstance(result, AbstractValueType) self.assertIs(result, self.model.row_header_type) + elif len(row) == 1: + self.assertIs(result, no_value) else: self.assertIsInstance(result, AbstractValueType) self.assertIs(result, self.model.value_type) @@ -119,7 +146,7 @@ def test_data_updated(self): self.model.data = 2 * self.array self.assertEqual( self.values_changed_event.new, - ([0], [0], [5], [3]) + ([0], [0], [4], [2]) ) def test_data_updated_new_shape(self): @@ -132,7 +159,7 @@ def test_type_updated(self): self.model.value_type = IntValue() self.assertEqual( self.values_changed_event.new, - ([0], [0], [5], [3]) + ([0], [0], [4], [2]) ) def test_type_attribute_updated(self): @@ -140,7 +167,129 @@ def test_type_attribute_updated(self): self.model.value_type.is_editable = False self.assertEqual( self.values_changed_event.new, - ([0], [0], [5], [3]) + ([0], [0], [4], [2]) + ) + + def test_row_header_type_updated(self): + with self.assertTraitChanges(self.model, "values_changed"): + self.model.row_header_type = no_value + self.assertEqual( + self.values_changed_event.new, + ([0], [], [4], []) + ) + + def test_row_header_attribute_updated(self): + with self.assertTraitChanges(self.model, "values_changed"): + self.model.row_header_type.format = str + self.assertEqual( + self.values_changed_event.new, + ([0], [], [4], []) + ) + + def test_column_header_type_updated(self): + with self.assertTraitChanges(self.model, "values_changed"): + self.model.column_header_type = no_value + self.assertEqual( + self.values_changed_event.new, + ([], [0], [], [2]) + ) + + def test_column_header_type_attribute_updated(self): + with self.assertTraitChanges(self.model, "values_changed"): + self.model.column_header_type.format = str + self.assertEqual( + self.values_changed_event.new, + ([], [0], [], [2]) + ) + + def test_label_header_type_updated(self): + with self.assertTraitChanges(self.model, "values_changed"): + self.model.label_header_type = no_value + self.assertEqual( + self.values_changed_event.new, + ([], [], [], []) + ) + + def test_label_header_type_attribute_updated(self): + with self.assertTraitChanges(self.model, "values_changed"): + self.model.label_header_type.text = "My Table" + self.assertEqual( + self.values_changed_event.new, + ([], [], [], []) + ) + def test_iter_rows(self): + result = list(self.model.iter_rows()) + self.assertEqual( + result, + [ + [], + [0], + [0, 0], + [0, 1], + [1], + [1, 0], + [1, 1], + [2], + [2, 0], + [2, 1], + [3], + [3, 0], + [3, 1], + [4], + [4, 0], + [4, 1], + ] + ) + + def test_iter_rows_start(self): + result = list(self.model.iter_rows([2])) + self.assertEqual( + result, + [[2], [2, 0], [2, 1]] + ) + + def test_iter_rows_leaf(self): + result = list(self.model.iter_rows([2, 0])) + self.assertEqual(result, [[2, 0]]) + + def test_iter_items(self): + result = list(self.model.iter_items()) + self.assertEqual( + result, + [ + ([], []), + ([], [0]), ([], [1]), ([], [2]), + ([0], []), + ([0], [0]), ([0], [1]), ([0], [2]), + ([0, 0], []), + ([0, 0], [0]), ([0, 0], [1]), ([0, 0], [2]), + ([0, 1], []), + ([0, 1], [0]), ([0, 1], [1]), ([0, 1], [2]), + ([1], []), + ([1], [0]), ([1], [1]), ([1], [2]), + ([1, 0], []), + ([1, 0], [0]), ([1, 0], [1]), ([1, 0], [2]), + ([1, 1], []), + ([1, 1], [0]), ([1, 1], [1]), ([1, 1], [2]), + ([2], []), + ([2], [0]), ([2], [1]), ([2], [2]), + ([2, 0], []), + ([2, 0], [0]), ([2, 0], [1]), ([2, 0], [2]), + ([2, 1], []), + ([2, 1], [0]), ([2, 1], [1]), ([2, 1], [2]), + ([3], []), + ([3], [0]), ([3], [1]), ([3], [2]), + ([3, 0], []), + ([3, 0], [0]), ([3, 0], [1]), ([3, 0], [2]), + ([3, 1], []), + ([3, 1], [0]), ([3, 1], [1]), ([3, 1], [2]), + ([4], []), + ([4], [0]), ([4], [1]), ([4], [2]), + ([4, 0], []), + ([4, 0], [0]), ([4, 0], [1]), ([4, 0], [2]), + ([4, 1], []), + ([4, 1], [0]), ([4, 1], [1]), ([4, 1], [2]), + ] ) def test_default_value_type(self): @@ -155,3 +304,7 @@ def test_default_value_type(self): data = np.array([['a', 'b', 'c'], ['e', 'f', 'g']]) model = ArrayDataModel(data=data) self.assertIsInstance(model.value_type, TextValue) + + data = np.array([['a', 'b', 'c'], ['e', 'f', 'g']], dtype=object) + model = ArrayDataModel(data=data) + self.assertIsInstance(model.value_type, TextValue) diff --git a/pyface/data_view/data_view_widget.py b/pyface/data_view/data_view_widget.py index 434ca7827..9d62fcf15 100644 --- a/pyface/data_view/data_view_widget.py +++ b/pyface/data_view/data_view_widget.py @@ -10,4 +10,4 @@ from pyface.toolkit import toolkit_object -DataViewWidget = toolkit_object('data_view.data_view_widget:DataViewWidget') \ No newline at end of file +DataViewWidget = toolkit_object('data_view.data_view_widget:DataViewWidget') diff --git a/pyface/data_view/i_data_view_widget.py b/pyface/data_view/i_data_view_widget.py index 9849afede..547218d37 100644 --- a/pyface/data_view/i_data_view_widget.py +++ b/pyface/data_view/i_data_view_widget.py @@ -12,7 +12,6 @@ from pyface.data_view.abstract_data_model import AbstractDataModel from pyface.i_widget import IWidget -from .data_view_item_model import DataViewItemModel class IDataViewWidget(IWidget): @@ -58,6 +57,3 @@ def _get_control_header_visible(self): def _set_control_header_visible(self, tooltip): """ Toolkit specific method to set the visibility of the header. """ raise NotImplementedError() - - - diff --git a/pyface/data_view/index_manager.py b/pyface/data_view/index_manager.py index e8fcdd9a8..b5a29e65c 100644 --- a/pyface/data_view/index_manager.py +++ b/pyface/data_view/index_manager.py @@ -238,8 +238,7 @@ class IntIndexManager(AbstractIndexManager): def create_index(self, parent: t.Any, row: int) -> t.Any: """ Given a parent index and a row number, create an index. - This method always raises RuntimeError, as the only persistent - index for an IntIndexManager is the Root, which has no parent. + This should only ever be called with Root as the parent. Parameters ---------- @@ -343,9 +342,6 @@ class TupleIndexManager(AbstractIndexManager): def create_index(self, parent: t.Any, row: int) -> t.Any: """ Given a parent index and a row number, create an index. - This method always raises RuntimeError, as the only persistent - index for an IntIndexManager is the Root, which has no parent. - Parameters ---------- parent : index object diff --git a/pyface/data_view/tests/test_abstract_value_type.py b/pyface/data_view/tests/test_abstract_value_type.py index 1b9d08c9b..493fbdce4 100644 --- a/pyface/data_view/tests/test_abstract_value_type.py +++ b/pyface/data_view/tests/test_abstract_value_type.py @@ -14,7 +14,7 @@ from traits.api import Str from traits.testing.unittest_tools import UnittestTools -from ..abstract_value_type import AbstractValueType +from pyface.data_view.abstract_value_type import AbstractValueType class ValueType(AbstractValueType): @@ -52,6 +52,11 @@ def test_set_editable_can_edit_false(self): result = value_type.set_editable(self.model, [0], [0], 2.0) self.assertFalse(result) + def test_has_text(self): + value_type = ValueType() + result = value_type.has_text(self.model, [0], [0]) + self.assertTrue(result) + def test_get_text(self): value_type = ValueType() result = value_type.get_text(self.model, [0], [0]) diff --git a/pyface/data_view/tests/test_index_manager.py b/pyface/data_view/tests/test_index_manager.py index bf82cd091..1dcd82615 100644 --- a/pyface/data_view/tests/test_index_manager.py +++ b/pyface/data_view/tests/test_index_manager.py @@ -10,8 +10,8 @@ from unittest import TestCase -from ..index_manager import ( - AbstractIndexManager, IntIndexManager, Root, TupleIndexManager, +from pyface.data_view.index_manager import ( + IntIndexManager, Root, TupleIndexManager, ) @@ -89,6 +89,18 @@ def setUp(self): def tearDown(self): self.index_manager.reset() + def test_create_index_root(self): + result = self.index_manager.create_index(Root, 5) + self.assertEqual(result, 5) + + def test_create_index_leaf(self): + with self.assertRaises(RuntimeError): + self.index_manager.create_index(5, 1) + + def test_create_index_negative(self): + with self.assertRaises(IndexError): + self.index_manager.create_index(Root, -5) + class TestTupleIndexManager(IndexManagerMixin, TestCase): diff --git a/pyface/data_view/value_types/api.py b/pyface/data_view/value_types/api.py index a8485756b..1e42d4e49 100644 --- a/pyface/data_view/value_types/api.py +++ b/pyface/data_view/value_types/api.py @@ -8,8 +8,8 @@ # # Thanks for using Enthought open source! -from .constant_value import ConstantValue -from .editable_value import EditableValue -from .no_value import NoValue, no_value -from .numeric_value import FloatValue, IntValue, NumericValue -from .text_value import TextValue +from .constant_value import ConstantValue # noqa: F401 +from .editable_value import EditableValue # noqa: F401 +from .no_value import NoValue, no_value # noqa: F401 +from .numeric_value import FloatValue, IntValue, NumericValue # noqa: F401 +from .text_value import TextValue # noqa: F401 diff --git a/pyface/data_view/value_types/constant_value.py b/pyface/data_view/value_types/constant_value.py index 5b62d5406..52925beee 100644 --- a/pyface/data_view/value_types/constant_value.py +++ b/pyface/data_view/value_types/constant_value.py @@ -29,4 +29,3 @@ def can_edit(self, model, row, column): def get_text(self, model, row, column): return self.text - diff --git a/pyface/data_view/value_types/numeric_value.py b/pyface/data_view/value_types/numeric_value.py index fdbf83ba0..86dca8ecb 100644 --- a/pyface/data_view/value_types/numeric_value.py +++ b/pyface/data_view/value_types/numeric_value.py @@ -69,12 +69,3 @@ class IntValue(NumericValue): class FloatValue(NumericValue): evaluate = Callable(float) - - -class ProportionValue(NumericValue): - - minimum = 0.0 - - maximum = 1.0 - - evaluate = Callable(float) diff --git a/pyface/data_view/value_types/tests/test_constant_value.py b/pyface/data_view/value_types/tests/test_constant_value.py index 3e5562884..eeca97939 100644 --- a/pyface/data_view/value_types/tests/test_constant_value.py +++ b/pyface/data_view/value_types/tests/test_constant_value.py @@ -13,7 +13,7 @@ from traits.testing.unittest_tools import UnittestTools -from ..constant_value import ConstantValue +from pyface.data_view.value_types.constant_value import ConstantValue class TestConstantValue(UnittestTools, TestCase): @@ -39,7 +39,10 @@ def test_has_text_true(self): def test_get_text(self): value_type = ConstantValue(text="something") - self.assertEqual(value_type.get_text(self.model, [0], [0]), "something") + self.assertEqual( + value_type.get_text(self.model, [0], [0]), + "something" + ) def test_text_changed(self): value_type = ConstantValue() diff --git a/pyface/data_view/value_types/tests/test_editable_value.py b/pyface/data_view/value_types/tests/test_editable_value.py index 1acc89974..0b433e7b2 100644 --- a/pyface/data_view/value_types/tests/test_editable_value.py +++ b/pyface/data_view/value_types/tests/test_editable_value.py @@ -13,7 +13,7 @@ from traits.testing.unittest_tools import UnittestTools -from ..editable_value import EditableValue +from pyface.data_view.value_types.editable_value import EditableValue class EditableWithValid(EditableValue): diff --git a/pyface/data_view/value_types/tests/test_no_value.py b/pyface/data_view/value_types/tests/test_no_value.py index 1a2db8a64..8e49c821d 100644 --- a/pyface/data_view/value_types/tests/test_no_value.py +++ b/pyface/data_view/value_types/tests/test_no_value.py @@ -11,7 +11,7 @@ from unittest import TestCase from unittest.mock import Mock -from ..no_value import NoValue +from pyface.data_view.value_types.no_value import NoValue class TestNoValue(TestCase): diff --git a/pyface/data_view/value_types/tests/test_numeric_value.py b/pyface/data_view/value_types/tests/test_numeric_value.py index 7611dcd93..301e87dce 100644 --- a/pyface/data_view/value_types/tests/test_numeric_value.py +++ b/pyface/data_view/value_types/tests/test_numeric_value.py @@ -11,7 +11,9 @@ from unittest import TestCase from unittest.mock import Mock -from ..numeric_value import FloatValue, IntValue, NumericValue, format_locale +from pyface.data_view.value_types.numeric_value import ( + FloatValue, IntValue, NumericValue, format_locale +) class TestNumericValue(TestCase): diff --git a/pyface/data_view/value_types/tests/test_text_value.py b/pyface/data_view/value_types/tests/test_text_value.py index a0f8a8847..4e20c1e14 100644 --- a/pyface/data_view/value_types/tests/test_text_value.py +++ b/pyface/data_view/value_types/tests/test_text_value.py @@ -11,7 +11,7 @@ from unittest import TestCase from unittest.mock import Mock -from ..text_value import TextValue +from pyface.data_view.value_types.text_value import TextValue class TestTextValue(TestCase): @@ -55,4 +55,3 @@ def test_set_text(self): self.assertTrue(success) self.model.set_value.assert_called_once_with([0], [0], "test") - diff --git a/pyface/ui/qt4/data_view/data_view_item_model.py b/pyface/ui/qt4/data_view/data_view_item_model.py index c1a27e947..7864ed862 100644 --- a/pyface/ui/qt4/data_view/data_view_item_model.py +++ b/pyface/ui/qt4/data_view/data_view_item_model.py @@ -10,6 +10,7 @@ import logging +from pyface.qt import is_qt5 from pyface.qt.QtCore import QAbstractItemModel, QModelIndex, Qt from pyface.data_view.index_manager import Root from pyface.data_view.abstract_data_model import AbstractDataModel @@ -137,7 +138,7 @@ def flags(self, index): value_type = self.model.get_value_type(row, column) flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable - if not self.model.can_have_children(row): + if is_qt5 and not self.model.can_have_children(row): flags |= Qt.ItemNeverHasChildren if value_type and value_type.can_edit(self.model, row, column): diff --git a/pyface/ui/qt4/data_view/data_view_widget.py b/pyface/ui/qt4/data_view/data_view_widget.py index 8f75443df..2b67bc05d 100644 --- a/pyface/ui/qt4/data_view/data_view_widget.py +++ b/pyface/ui/qt4/data_view/data_view_widget.py @@ -51,6 +51,3 @@ def _set_control_header_visible(self, tooltip): def update_item_model(self, event): if self._item_model is not None: self._item_model.model = event.new - - - diff --git a/pyface/ui/wx/data_view/data_view_widget.py b/pyface/ui/wx/data_view/data_view_widget.py index dad413f78..6849ea068 100644 --- a/pyface/ui/wx/data_view/data_view_widget.py +++ b/pyface/ui/wx/data_view/data_view_widget.py @@ -8,13 +8,12 @@ # # Thanks for using Enthought open source! -from traits.api import Bool, Instance, observe, provides +from traits.api import Instance, observe, provides from wx.dataview import ( DataViewCtrl, DataViewModel as wxDataViewModel, DATAVIEW_CELL_EDITABLE, - DATAVIEW_CELL_ACTIVATABLE, EVT_DATAVIEW_ITEM_ACTIVATED + DATAVIEW_CELL_ACTIVATABLE ) -from pyface.data_view.abstract_data_model import AbstractDataModel from pyface.data_view.i_data_view_widget import ( IDataViewWidget, MDataViewWidget ) @@ -76,6 +75,3 @@ def _set_control_header_visible(self, tooltip): def update_item_model(self, event): if self._item_model is not None: self._item_model.model = event.new - - - From 23b852d684db319f65632631198741cb742f3408 Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Tue, 23 Jun 2020 11:04:12 +0100 Subject: [PATCH 16/52] Add some more tests, improve a comment. --- pyface/data_view/abstract_data_model.py | 3 ++- .../tests/test_array_data_model.py | 24 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/pyface/data_view/abstract_data_model.py b/pyface/data_view/abstract_data_model.py index 8540c1b2b..e2d1a2c12 100644 --- a/pyface/data_view/abstract_data_model.py +++ b/pyface/data_view/abstract_data_model.py @@ -73,7 +73,8 @@ class AbstractDataModel(ABCHasStrictTraits): #: Event fired when value changes without changes to structure. This #: should be set to a 4-tuple of (start_row_index, start_column_index, #: end_row_index, end_column_index) indicated the subset of data which - #: changed. + #: changed. These end values are inclusive, unlike standard Python + #: slicing notation. values_changed = Event() # Data structure methods diff --git a/pyface/data_view/data_models/tests/test_array_data_model.py b/pyface/data_view/data_models/tests/test_array_data_model.py index db90bbda1..e27466c92 100644 --- a/pyface/data_view/data_models/tests/test_array_data_model.py +++ b/pyface/data_view/data_models/tests/test_array_data_model.py @@ -292,6 +292,30 @@ def test_iter_items(self): ] ) + def test_iter_items_start(self): + result = list(self.model.iter_items([2])) + self.assertEqual( + result, + [ + ([2], []), + ([2], [0]), ([2], [1]), ([2], [2]), + ([2, 0], []), + ([2, 0], [0]), ([2, 0], [1]), ([2, 0], [2]), + ([2, 1], []), + ([2, 1], [0]), ([2, 1], [1]), ([2, 1], [2]), + ] + ) + + def test_iter_items_leaf(self): + result = list(self.model.iter_items([2, 0])) + self.assertEqual( + result, + [ + ([2, 0], []), + ([2, 0], [0]), ([2, 0], [1]), ([2, 0], [2]), + ] + ) + def test_default_value_type(self): data = np.arange(15).reshape(5, 3) model = ArrayDataModel(data=data) From 8a9f49e26d20322f5881adb3ff922bc36dff301b Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Tue, 23 Jun 2020 12:37:30 +0100 Subject: [PATCH 17/52] Get a smoke test working for the DataViewWidget class. --- pyface/data_view/abstract_data_model.py | 4 +- pyface/data_view/abstract_value_type.py | 6 +- .../data_view/tests/test_data_view_widget.py | 82 +++++++++++++++++++ .../value_types/tests/test_text_value.py | 9 ++ pyface/data_view/value_types/text_value.py | 7 +- 5 files changed, 101 insertions(+), 7 deletions(-) create mode 100644 pyface/data_view/tests/test_data_view_widget.py diff --git a/pyface/data_view/abstract_data_model.py b/pyface/data_view/abstract_data_model.py index e2d1a2c12..f13e0acf6 100644 --- a/pyface/data_view/abstract_data_model.py +++ b/pyface/data_view/abstract_data_model.py @@ -238,7 +238,7 @@ def iter_rows(self, start=[]): ---------- start : row index The row to start at. The iterator will yeild the row and all - child rows. + descendant rows. Yields ------ @@ -259,7 +259,7 @@ def iter_items(self, start_row=[]): ---------- start : row index The row to start at. The iterator will yeild the row and all - child rows. + descendant rows and all column indices for those rows. Yields ------ diff --git a/pyface/data_view/abstract_value_type.py b/pyface/data_view/abstract_value_type.py index fca436616..f523213d4 100644 --- a/pyface/data_view/abstract_value_type.py +++ b/pyface/data_view/abstract_value_type.py @@ -82,10 +82,10 @@ def get_editable(self, model, row, column): return model.get_value(row, column) def set_editable(self, model, row, column, value): - """ Return a value suitable for editing. + """ Set a value that is returned from editing. - The default implementation is to return the underlying data value - directly from the data model. + The default implementation is to set the value directly from the + data model. Returns True if successful, False if it fails. Parameters ---------- diff --git a/pyface/data_view/tests/test_data_view_widget.py b/pyface/data_view/tests/test_data_view_widget.py new file mode 100644 index 000000000..d8c4fce9c --- /dev/null +++ b/pyface/data_view/tests/test_data_view_widget.py @@ -0,0 +1,82 @@ +# (C) Copyright 2005-2020 Enthought, Inc., Austin, TX +# All rights reserved. +# +# This software is provided without warranty under the terms of the BSD +# license included in LICENSE.txt and may be redistributed only under +# the conditions described in the aforementioned license. The license +# is also available online at http://www.enthought.com/licenses/BSD.txt +# +# Thanks for using Enthought open source! + + +import unittest + +from traits.testing.optional_dependencies import numpy as np, requires_numpy +from traits.testing.unittest_tools import UnittestTools + +from pyface.gui import GUI +from pyface.window import Window +from pyface.toolkit import toolkit_object + +from pyface.data_view.data_models.api import ArrayDataModel +from pyface.data_view.data_view_widget import DataViewWidget + + +@requires_numpy +class TestWidget(unittest.TestCase, UnittestTools): + def setUp(self): + self.gui = GUI() + + self.parent = Window() + self.parent._create() + self.addCleanup(self._destroy_parent) + self.gui.process_events() + + self.widget = self._create_widget() + + self.parent.open() + self.gui.process_events() + + def _create_widget(self): + self.data = np.arange(30.0).reshape(5, 6) + self.model = ArrayDataModel(data=self.data) + return DataViewWidget( + parent=self.parent.control, + data_model=self.model + ) + + def _create_widget_control(self): + self.widget._create() + self.addCleanup(self._destroy_widget) + self.widget.show(True) + self.gui.process_events() + + def _destroy_parent(self): + self.parent.destroy() + self.gui.process_events() + self.parent = None + + def _destroy_widget(self): + self.widget.destroy() + self.gui.process_events() + self.widget = None + + def test_defaults(self): + self.assertTrue(self.widget.header_visible) + + def test_lifecycle(self): + self._create_widget_control() + + def test_header_visible(self): + self._create_widget_control() + + self.widget.header_visible = False + self.gui.process_events() + + self.assertFalse(self.widget._get_control_header_visible()) + + def test_header_visible_before_control(self): + self.widget.header_visible = False + + self._create_widget_control() + self.assertFalse(self.widget._get_control_header_visible()) diff --git a/pyface/data_view/value_types/tests/test_text_value.py b/pyface/data_view/value_types/tests/test_text_value.py index 4e20c1e14..a885b98e1 100644 --- a/pyface/data_view/value_types/tests/test_text_value.py +++ b/pyface/data_view/value_types/tests/test_text_value.py @@ -55,3 +55,12 @@ def test_set_text(self): self.assertTrue(success) self.model.set_value.assert_called_once_with([0], [0], "test") + + def test_set_text_no_set_value(self): + self.model.can_set_value = Mock(return_value=False) + + value = TextValue() + success = value.set_text(self.model, [0], [0], "test") + + self.assertFalse(success) + self.model.set_value.assert_not_called() diff --git a/pyface/data_view/value_types/text_value.py b/pyface/data_view/value_types/text_value.py index aeee39425..33e739eb6 100644 --- a/pyface/data_view/value_types/text_value.py +++ b/pyface/data_view/value_types/text_value.py @@ -16,7 +16,7 @@ class TextValue(EditableValue): """ def set_text(self, model, row, column, text): - """ Set the textual representation of the underlying value. + """ Set the text of the underlying value. Parameters ---------- @@ -34,4 +34,7 @@ def set_text(self, model, row, column, text): success : bool Whether or not the value was successfully set. """ - return model.set_value(row, column, text) + if model.can_set_value(row, column): + return model.set_value(row, column, text) + + return False From 8e700929fe6a604cc7309d850a754a450be410b5 Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Tue, 23 Jun 2020 12:54:59 +0100 Subject: [PATCH 18/52] More clean-up of the toolkit implementations. --- .../ui/qt4/data_view/data_view_item_model.py | 1 + pyface/ui/qt4/data_view/data_view_widget.py | 16 ++++++++++++++-- pyface/ui/wx/data_view/data_view_model.py | 6 +++++- pyface/ui/wx/data_view/data_view_widget.py | 19 ++++++++++++------- 4 files changed, 32 insertions(+), 10 deletions(-) diff --git a/pyface/ui/qt4/data_view/data_view_item_model.py b/pyface/ui/qt4/data_view/data_view_item_model.py index 7864ed862..ccadf8cc9 100644 --- a/pyface/ui/qt4/data_view/data_view_item_model.py +++ b/pyface/ui/qt4/data_view/data_view_item_model.py @@ -22,6 +22,7 @@ class DataViewItemModel(QAbstractItemModel): + """ A QAbstractItemModel that understands AbstractDataModels. """ def __init__(self, model, parent=None): super().__init__(parent) diff --git a/pyface/ui/qt4/data_view/data_view_widget.py b/pyface/ui/qt4/data_view/data_view_widget.py index 2b67bc05d..28ba09823 100644 --- a/pyface/ui/qt4/data_view/data_view_widget.py +++ b/pyface/ui/qt4/data_view/data_view_widget.py @@ -24,10 +24,14 @@ @provides(IDataViewWidget) class DataViewWidget(MDataViewWidget, Widget): + """ The Qt implementation of the DataViewWidget. """ + #: The QAbstractItemModel instance used by the view. This will + #: usually be a DataViewItemModel subclass. _item_model = Instance(QAbstractItemModel) def _create_control(self, parent): + """ Create the DataViewWidget's toolkit control. """ self._create_item_model() control = QTreeView(parent) @@ -37,14 +41,22 @@ def _create_control(self, parent): return control def _create_item_model(self): + """ Create the DataViewItemModel which wraps the data model. """ self._item_model = DataViewItemModel(self.data_model) + def destroy(self): + """ Perform any actions required to destroy the control. + """ + super().destroy() + # ensure that we release the reference to the item model + self._item_model = None + def _get_control_header_visible(self): - """ Toolkit specific method to get the control's tooltip. """ + """ Method to get the control's header visibility. """ return not self.control.isHeaderHidden() def _set_control_header_visible(self, tooltip): - """ Toolkit specific method to set the control's tooltip. """ + """ Method to set the control's header visibility. """ self.control.setHeaderHidden(not tooltip) @observe('data_model', dispatch='ui') diff --git a/pyface/ui/wx/data_view/data_view_model.py b/pyface/ui/wx/data_view/data_view_model.py index e563bc136..22a0e843c 100644 --- a/pyface/ui/wx/data_view/data_view_model.py +++ b/pyface/ui/wx/data_view/data_view_model.py @@ -18,6 +18,7 @@ # XXX This file is scaffolding and may need to be rewritten or expanded class DataViewModel(wxDataViewModel): + """ A wxDataViewModel that understands AbstractDataModels. """ def __init__(self, model): super().__init__() @@ -83,7 +84,10 @@ def on_values_changed(self, event): self.ItemChanged(self._to_item(top)) else: # multiple item change - items = [self._to_item(top[:i] + [row]) for row in range(top[i], bottom[i]+1)] + items = [ + self._to_item(top[:i] + [row]) + for row in range(top[i], bottom[i]+1) + ] self.ItemsChanged(items) def GetParent(self, item): diff --git a/pyface/ui/wx/data_view/data_view_widget.py b/pyface/ui/wx/data_view/data_view_widget.py index 6849ea068..1e6ae5c90 100644 --- a/pyface/ui/wx/data_view/data_view_widget.py +++ b/pyface/ui/wx/data_view/data_view_widget.py @@ -25,10 +25,14 @@ @provides(IDataViewWidget) class DataViewWidget(MDataViewWidget, Widget): + """ The Wx implementation of the DataViewWidget. """ + #: The QAbstractItemModel instance used by the view. This will + #: usually be a DataViewModel subclass. _item_model = Instance(wxDataViewModel) def _create_control(self, parent): + """ Create the DataViewWidget's toolkit control. """ self._create_item_model() control = DataViewCtrl(parent) @@ -53,22 +57,23 @@ def _create_control(self, parent): return control def _create_item_model(self): + """ Create the DataViewItemModel which wraps the data model. """ self._item_model = DataViewModel(self.data_model) def destroy(self): - if self.control is not None: - # unhook things here - self._item_model = None + """ Perform any actions required to destroy the control. """ super().destroy() + # ensure that we release the reference to the item model + self._item_model = None def _get_control_header_visible(self): - """ Toolkit specific method to get the control's tooltip. """ - # need to read DV_NO_HEADER + """ Method to get the control's header visibility. """ + # TODO: need to read DV_NO_HEADER pass def _set_control_header_visible(self, tooltip): - """ Toolkit specific method to set the control's tooltip. """ - # need to toggle DV_NO_HEADER + """ Method to set the control's header visibility. """ + # TODO: need to toggle DV_NO_HEADER pass @observe('data_model', dispatch='ui') From d50d7ef2c6404bca461c3ed4b8bdb19cbef9d5ec Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Sun, 28 Jun 2020 10:51:00 +0100 Subject: [PATCH 19/52] Apply suggestions from code review Co-authored-by: Ieva --- pyface/data_view/abstract_data_model.py | 6 +++--- pyface/data_view/abstract_value_type.py | 14 ++++++++------ pyface/data_view/index_manager.py | 6 +----- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/pyface/data_view/abstract_data_model.py b/pyface/data_view/abstract_data_model.py index f13e0acf6..584f791ed 100644 --- a/pyface/data_view/abstract_data_model.py +++ b/pyface/data_view/abstract_data_model.py @@ -55,7 +55,7 @@ class AbstractDataModel(ABCHasStrictTraits): If the data is to be editable then the subclass should override the - ``set_data`` method. It should attempt to change the underlying data as a + ``set_value`` method. It should attempt to change the underlying data as a side-effect, and return True on success and False on failure (for example, setting an invalid value). If the underlying data structure cannot be listened to internally (such as a numpy array or Pandas data frame), this @@ -208,10 +208,10 @@ def set_value(self, row, column, value): @abstractmethod def get_value_type(self, row, column): - """ Return the text value for the row and column. + """ Return the value type of the given row and column. The value type for column headers are returned by calling this method - with row equal to []. The value typess for row headers are returned + with row equal to []. The value types for row headers are returned by calling this method with column equal to []. Parameters diff --git a/pyface/data_view/abstract_value_type.py b/pyface/data_view/abstract_value_type.py index f523213d4..357cc662a 100644 --- a/pyface/data_view/abstract_value_type.py +++ b/pyface/data_view/abstract_value_type.py @@ -30,7 +30,7 @@ class AbstractValueType(ABCHasStrictTraits): display. Subclasses should mark traits that potentially affect the display of values - with ``update=True`` metdadata, or alternativelym fire the ``updated`` + with ``update=True`` metdadata, or alternatively fire the ``updated`` event when the state of the value type changes. """ @@ -95,11 +95,13 @@ def set_editable(self, model, row, column, value): The row in the data model being queried. column : sequence of int The column in the data model being queried. + value : any + The value to set. Returns ------- - value : any - The value to edit. + success : bool + Whether or not the value was successfully set. """ if not self.can_edit(model, row, column): return False @@ -122,8 +124,8 @@ def has_text(self, model, row, column): Returns ------- - value : any - The value to edit. + has_text : bool + Whether or not the value has a textual representation. """ return self.get_text(model, row, column) != "" @@ -149,7 +151,7 @@ def get_text(self, model, row, column): return str(model.get_value(row, column)) def set_text(self, model, row, column, text): - """ Set the textual representation of the underlying value. + """ Set the text of the underlying value. This is provided primarily for backends which may not permit non-text editing of values, in which case this provides an diff --git a/pyface/data_view/index_manager.py b/pyface/data_view/index_manager.py index b5a29e65c..d9de16da0 100644 --- a/pyface/data_view/index_manager.py +++ b/pyface/data_view/index_manager.py @@ -257,8 +257,7 @@ def create_index(self, parent: t.Any, row: int) -> t.Any: IndexError Negative row values raise an IndexError exception. RuntimeError - The only persistent index for an IntIndexManager is the - root, so this method always raises. + If the parent is not the Root, a RuntimeError will be raised """ if row < 0: raise IndexError("Row must be non-negative. Got {}".format(row)) @@ -358,9 +357,6 @@ def create_index(self, parent: t.Any, row: int) -> t.Any: ------ IndexError Negative row values raise an IndexError exception. - RuntimeError - The only persistent index for an IntIndexManager is the - root, so this method always raises. """ if row < 0: raise IndexError("Row must be non-negative. Got {}".format(row)) From 4792f66e5a1551d5f707c6c1fb0f969602522de3 Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Tue, 30 Jun 2020 12:56:24 +0100 Subject: [PATCH 20/52] Fixes based in PR suggestions. --- pyface/data_view/abstract_data_model.py | 17 ++++---- pyface/data_view/abstract_value_type.py | 8 +++- .../data_view/data_models/array_data_model.py | 42 ++++++++++++------- .../data_view/tests/test_data_view_widget.py | 1 - .../data_view/value_types/editable_value.py | 15 ++++--- 5 files changed, 51 insertions(+), 32 deletions(-) diff --git a/pyface/data_view/abstract_data_model.py b/pyface/data_view/abstract_data_model.py index 584f791ed..c6daf5099 100644 --- a/pyface/data_view/abstract_data_model.py +++ b/pyface/data_view/abstract_data_model.py @@ -231,12 +231,12 @@ def get_value_type(self, row, column): # Convenience iterator methods - def iter_rows(self, start=[]): + def iter_rows(self, start_row=[]): """ Iterator that yields rows in preorder. Parameters ---------- - start : row index + start_row : row index The row to start at. The iterator will yeild the row and all descendant rows. @@ -245,21 +245,22 @@ def iter_rows(self, start=[]): row_index The current row index. """ - yield start - if self.can_have_children(start): - for row in range(self.get_row_count(start)): - yield from self.iter_rows(start + [row]) + yield start_row + if self.can_have_children(start_row): + for row in range(self.get_row_count(start_row)): + yield from self.iter_rows(start_row + [row]) def iter_items(self, start_row=[]): """ Iterator that yields rows and columns in preorder. + This yields pairs of row, column for all rows in preorder + and and all column indices for all rows, including []. Columns are iterated in order. Parameters ---------- start : row index - The row to start at. The iterator will yeild the row and all - descendant rows and all column indices for those rows. + The row to start iteration from. Yields ------ diff --git a/pyface/data_view/abstract_value_type.py b/pyface/data_view/abstract_value_type.py index 357cc662a..86ed58eb0 100644 --- a/pyface/data_view/abstract_value_type.py +++ b/pyface/data_view/abstract_value_type.py @@ -32,6 +32,12 @@ class AbstractValueType(ABCHasStrictTraits): Subclasses should mark traits that potentially affect the display of values with ``update=True`` metdadata, or alternatively fire the ``updated`` event when the state of the value type changes. + + Each data channel is set up to have a method which returns whether there + is a value for the channel, a second method which returns the value, + and an optional third method which sets the channel value. These methods + should not raise an Exception, eveen when called inappropriately (eg. + calling a "get" method after a "has" method has returned False). """ #: Fired when a change occurs that requires updating values. @@ -146,7 +152,7 @@ def get_text(self, model, row, column): Returns ------- text : str - The value to edit. + The textual representation of the underlying value. """ return str(model.get_value(row, column)) diff --git a/pyface/data_view/data_models/array_data_model.py b/pyface/data_view/data_models/array_data_model.py index 1edfe7ef8..dba31c7d7 100644 --- a/pyface/data_view/data_models/array_data_model.py +++ b/pyface/data_view/data_models/array_data_model.py @@ -23,6 +23,22 @@ class ArrayDataModel(AbstractDataModel): + """ A data model for an n-dim array. + + This data model presents the data from a multidimensional array + heirarchically by dimension. The underlying array must be at least 2 + dimensional. + + Values are adapted by the ``value_type`` trait. This provides sensible + default values for integer, float and text dtypes, but other dtypes may + need the user of the class to supply an appropriate value type class to + adapt values. + + There are additional value types which provide data sources for row + headers, column headers, and the label of the row header column. The + defaults are likely suitable for most cases, but can be overriden if + required. + """ #: The array being displayed. This must have dimension at least 2. data = Array() @@ -31,7 +47,7 @@ class ArrayDataModel(AbstractDataModel): #: indices. index_manager = Instance(TupleIndexManager, args=()) - #: The value type of the label headers. + #: The value type of the row index column header. label_header_type = Instance( AbstractValueType, factory=ConstantValue, @@ -60,8 +76,8 @@ class ArrayDataModel(AbstractDataModel): def get_column_count(self, row): """ How many columns in a row of the data view model. - The total number of columns in the table is given by the column - count of the Root row. + The number of columns is always the size of the last dimension of the + array. Parameters ---------- @@ -78,6 +94,10 @@ def get_column_count(self, row): def can_have_children(self, row): """ Whether or not a row can have child rows. + A row is a leaf row if the length of the index is one less than + the dimension of the array: the final coordinate for the value will + be supplied by the column index. + Parameters ---------- row : sequence of int @@ -95,7 +115,8 @@ def can_have_children(self, row): def get_row_count(self, row): """ Whether or not the row currently has any child rows. - Subclasses may override this to provide a more direct implementation. + The number of rows in a non-leaf row is equal to the size of the + next dimension. Parameters ---------- @@ -163,9 +184,6 @@ def can_set_value(self, row, column): def set_value(self, row, column, value): """ Return the Python value for the row and column. - The values for column headers are returned by calling this method - with row as Root. - Parameters ---------- row : sequence of int @@ -189,10 +207,6 @@ def set_value(self, row, column, value): def get_value_type(self, row, column): """ Return the text value for the row and column. - The value type for column headers are returned by calling this method - with row equal to []. The value typess for row headers are returned - by calling this method with column equal to []. - Parameters ---------- row : sequence of int @@ -237,21 +251,21 @@ def value_type_updated(self, event): @observe('column_header_type.updated', dispatch='ui') def column_header_type_updated(self, event): - """ Handle the header type being updated. """ + """ Handle the column header type being updated. """ self.values_changed = ( ([], [0], [], [self.data.shape[-1] - 1]) ) @observe('row_header_type.updated', dispatch='ui') def value_header_type_updated(self, event): - """ Handle the header type being updated. """ + """ Handle the value header type being updated. """ self.values_changed = ( ([0], [], [self.data.shape[0] - 1], []) ) @observe('label_header_type.updated', dispatch='ui') def label_header_type_updated(self, event): - """ Handle the header type being updated. """ + """ Handle the label header type being updated. """ self.values_changed = ( ([], [], [], []) ) diff --git a/pyface/data_view/tests/test_data_view_widget.py b/pyface/data_view/tests/test_data_view_widget.py index d8c4fce9c..0c968f1b0 100644 --- a/pyface/data_view/tests/test_data_view_widget.py +++ b/pyface/data_view/tests/test_data_view_widget.py @@ -16,7 +16,6 @@ from pyface.gui import GUI from pyface.window import Window -from pyface.toolkit import toolkit_object from pyface.data_view.data_models.api import ArrayDataModel from pyface.data_view.data_view_widget import DataViewWidget diff --git a/pyface/data_view/value_types/editable_value.py b/pyface/data_view/value_types/editable_value.py index 1fad240b5..627488171 100644 --- a/pyface/data_view/value_types/editable_value.py +++ b/pyface/data_view/value_types/editable_value.py @@ -74,24 +74,23 @@ def can_edit(self, model, row, column): return model.can_set_value(row, column) and self.is_editable def set_editable(self, model, row, column, value): - """ Return whether or not the value can be edited. - - A cell is editable if the underlying data can be set, and the - ``is_editable`` flag is set to True + """ Set the edited value. Parameters ---------- model : AbstractDataModel The data model holding the data. row : sequence of int - The row in the data model being queried. + The row in the data model being set. column : sequence of int - The column in the data model being queried. + The column in the data model being set. + value : any + The value being set. Returns ------- - can_edit : bool - Whether or not the value is editable. + success : bool + Whether or not the value was set successfully. """ if not (self.can_edit(model, row, column) and self.is_valid(model, row, column, value)): From 6f2d37f8c8671d677132e036802a2eafbb8ef5e7 Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Tue, 30 Jun 2020 13:18:52 +0100 Subject: [PATCH 21/52] Fix flake8 issue. --- pyface/data_view/data_models/tests/test_array_data_model.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyface/data_view/data_models/tests/test_array_data_model.py b/pyface/data_view/data_models/tests/test_array_data_model.py index e27466c92..e38905e3f 100644 --- a/pyface/data_view/data_models/tests/test_array_data_model.py +++ b/pyface/data_view/data_models/tests/test_array_data_model.py @@ -217,6 +217,7 @@ def test_label_header_type_attribute_updated(self): self.values_changed_event.new, ([], [], [], []) ) + def test_iter_rows(self): result = list(self.model.iter_rows()) self.assertEqual( From eabf7f5a818f472a5fb1cf8dfb57bb88dea66f75 Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Wed, 1 Jul 2020 18:20:31 +0100 Subject: [PATCH 22/52] Convert from lists for indices to tuples. --- examples/data_view/column_data_model.py | 6 +- pyface/data_view/abstract_data_model.py | 11 +- .../data_view/data_models/array_data_model.py | 24 +-- .../tests/test_array_data_model.py | 158 +++++++++--------- pyface/data_view/index_manager.py | 33 ++-- pyface/data_view/tests/test_index_manager.py | 26 +-- .../ui/qt4/data_view/data_view_item_model.py | 26 +-- pyface/ui/wx/data_view/data_view_model.py | 16 +- 8 files changed, 149 insertions(+), 151 deletions(-) diff --git a/examples/data_view/column_data_model.py b/examples/data_view/column_data_model.py index 72ac064bf..4e6edd8c5 100644 --- a/examples/data_view/column_data_model.py +++ b/examples/data_view/column_data_model.py @@ -22,10 +22,6 @@ from pyface.data_view.value_types.api import TextValue -def id(obj): - return obj - - class AbstractRowInfo(ABCHasStrictTraits): """ Configuration for a data row in a ColumnDataModel. """ @@ -210,7 +206,7 @@ def can_set_value(self, row, column): can_set_value : bool Whether or not the value can be set. """ - if column == []: + if len(column) == 0: return False else: return True diff --git a/pyface/data_view/abstract_data_model.py b/pyface/data_view/abstract_data_model.py index c6daf5099..65c62f1a4 100644 --- a/pyface/data_view/abstract_data_model.py +++ b/pyface/data_view/abstract_data_model.py @@ -231,7 +231,7 @@ def get_value_type(self, row, column): # Convenience iterator methods - def iter_rows(self, start_row=[]): + def iter_rows(self, start_row=()): """ Iterator that yields rows in preorder. Parameters @@ -245,12 +245,13 @@ def iter_rows(self, start_row=[]): row_index The current row index. """ + start_row = tuple(start_row) yield start_row if self.can_have_children(start_row): for row in range(self.get_row_count(start_row)): - yield from self.iter_rows(start_row + [row]) + yield from self.iter_rows(start_row + (row,)) - def iter_items(self, start_row=[]): + def iter_items(self, start_row=()): """ Iterator that yields rows and columns in preorder. This yields pairs of row, column for all rows in preorder @@ -268,6 +269,6 @@ def iter_items(self, start_row=[]): The current row and column indices. """ for row in self.iter_rows(start_row): - yield row, [] + yield row, () for column in range(self.get_column_count(row)): - yield row, [column] + yield row, (column,) diff --git a/pyface/data_view/data_models/array_data_model.py b/pyface/data_view/data_models/array_data_model.py index dba31c7d7..e2f2dd20b 100644 --- a/pyface/data_view/data_models/array_data_model.py +++ b/pyface/data_view/data_models/array_data_model.py @@ -147,11 +147,11 @@ def get_value(self, row, column): row_count : non-negative int The number of child rows that the row has. """ - if row == []: - if column == []: + if len(row) == 0: + if len(column) == 0: return None return column[0] - elif column == []: + elif len(column) == 0: return row[-1] else: index = tuple(row + column) @@ -219,11 +219,11 @@ def get_value_type(self, row, column): text : str The text to display in the given row and column. """ - if row == []: - if column == []: + if len(row) == 0: + if len(column) == 0: return self.label_header_type return self.column_header_type - elif column == []: + elif len(column) == 0: return self.row_header_type elif len(row) < self.data.ndim - 1: return no_value @@ -237,7 +237,9 @@ def data_updated(self, event): """ Handle the array being replaced with a new array. """ if event.new.shape == event.old.shape: self.values_changed = ( - ([0], [0], [event.old.shape[0] - 1], [event.old.shape[-1] - 1]) + ( + (0,), (0,), + (event.old.shape[0] - 1,), (event.old.shape[-1] - 1,)) ) else: self.structure_changed = True @@ -246,28 +248,28 @@ def data_updated(self, event): def value_type_updated(self, event): """ Handle the value type being updated. """ self.values_changed = ( - ([0], [0], [self.data.shape[0] - 1], [self.data.shape[-1] - 1]) + ((0,), (0,), (self.data.shape[0] - 1,), (self.data.shape[-1] - 1,)) ) @observe('column_header_type.updated', dispatch='ui') def column_header_type_updated(self, event): """ Handle the column header type being updated. """ self.values_changed = ( - ([], [0], [], [self.data.shape[-1] - 1]) + ((), (0,), (), (self.data.shape[-1] - 1,)) ) @observe('row_header_type.updated', dispatch='ui') def value_header_type_updated(self, event): """ Handle the value header type being updated. """ self.values_changed = ( - ([0], [], [self.data.shape[0] - 1], []) + ((0,), (), (self.data.shape[0] - 1,), ()) ) @observe('label_header_type.updated', dispatch='ui') def label_header_type_updated(self, event): """ Handle the label header type being updated. """ self.values_changed = ( - ([], [], [], []) + ((), (), (), ()) ) def _value_type_default(self): diff --git a/pyface/data_view/data_models/tests/test_array_data_model.py b/pyface/data_view/data_models/tests/test_array_data_model.py index e38905e3f..1524ddba3 100644 --- a/pyface/data_view/data_models/tests/test_array_data_model.py +++ b/pyface/data_view/data_models/tests/test_array_data_model.py @@ -77,11 +77,11 @@ def test_get_value(self): for row, column in self.model.iter_items(): with self.subTest(row=row, column=column): result = self.model.get_value(row, column) - if row == [] and column == []: + if len(row) == 0 and len(column) == 0: self.assertIsNone(result) - elif row == []: + elif len(row) == 0: self.assertEqual(result, column[0]) - elif column == []: + elif len(column) == 0: self.assertEqual(result, row[-1]) elif len(row) == 1: self.assertIsNone(result) @@ -94,13 +94,13 @@ def test_get_value(self): def test_set_value(self): for row, column in self.model.iter_items(): with self.subTest(row=row, column=column): - if row == [] and column == []: + if len(row) == 0 and len(column) == 0: result = self.model.set_value(row, column, 0) self.assertFalse(result) - elif row == []: + elif len(row) == 0: result = self.model.set_value(row, column, column[0] + 1) self.assertFalse(result) - elif column == []: + elif len(column) == 0: result = self.model.set_value(row, column, row[-1] + 1) self.assertFalse(result) elif len(row) == 1: @@ -126,13 +126,13 @@ def test_get_value_type(self): for row, column in self.model.iter_items(): with self.subTest(row=row, column=column): result = self.model.get_value_type(row, column) - if row == [] and column == []: + if len(row) == 0 and len(column) == 0: self.assertIsInstance(result, AbstractValueType) self.assertIs(result, self.model.label_header_type) - elif row == []: + elif len(row) == 0: self.assertIsInstance(result, AbstractValueType) self.assertIs(result, self.model.column_header_type) - elif column == []: + elif len(column) == 0: self.assertIsInstance(result, AbstractValueType) self.assertIs(result, self.model.row_header_type) elif len(row) == 1: @@ -146,7 +146,7 @@ def test_data_updated(self): self.model.data = 2 * self.array self.assertEqual( self.values_changed_event.new, - ([0], [0], [4], [2]) + ((0,), (0,), (4,), (2,)) ) def test_data_updated_new_shape(self): @@ -159,7 +159,7 @@ def test_type_updated(self): self.model.value_type = IntValue() self.assertEqual( self.values_changed_event.new, - ([0], [0], [4], [2]) + ((0,), (0,), (4,), (2,)) ) def test_type_attribute_updated(self): @@ -167,7 +167,7 @@ def test_type_attribute_updated(self): self.model.value_type.is_editable = False self.assertEqual( self.values_changed_event.new, - ([0], [0], [4], [2]) + ((0,), (0,), (4,), (2,)) ) def test_row_header_type_updated(self): @@ -175,7 +175,7 @@ def test_row_header_type_updated(self): self.model.row_header_type = no_value self.assertEqual( self.values_changed_event.new, - ([0], [], [4], []) + ((0,), (), (4,), ()) ) def test_row_header_attribute_updated(self): @@ -183,7 +183,7 @@ def test_row_header_attribute_updated(self): self.model.row_header_type.format = str self.assertEqual( self.values_changed_event.new, - ([0], [], [4], []) + ((0,), (), (4,), ()) ) def test_column_header_type_updated(self): @@ -191,7 +191,7 @@ def test_column_header_type_updated(self): self.model.column_header_type = no_value self.assertEqual( self.values_changed_event.new, - ([], [0], [], [2]) + ((), (0,), (), (2,)) ) def test_column_header_type_attribute_updated(self): @@ -199,7 +199,7 @@ def test_column_header_type_attribute_updated(self): self.model.column_header_type.format = str self.assertEqual( self.values_changed_event.new, - ([], [0], [], [2]) + ((), (0,), (), (2,)) ) def test_label_header_type_updated(self): @@ -207,7 +207,7 @@ def test_label_header_type_updated(self): self.model.label_header_type = no_value self.assertEqual( self.values_changed_event.new, - ([], [], [], []) + ((), (), (), ()) ) def test_label_header_type_attribute_updated(self): @@ -215,7 +215,7 @@ def test_label_header_type_attribute_updated(self): self.model.label_header_type.text = "My Table" self.assertEqual( self.values_changed_event.new, - ([], [], [], []) + ((), (), (), ()) ) def test_iter_rows(self): @@ -223,97 +223,97 @@ def test_iter_rows(self): self.assertEqual( result, [ - [], - [0], - [0, 0], - [0, 1], - [1], - [1, 0], - [1, 1], - [2], - [2, 0], - [2, 1], - [3], - [3, 0], - [3, 1], - [4], - [4, 0], - [4, 1], + (), + (0,), + (0, 0), + (0, 1), + (1,), + (1, 0), + (1, 1), + (2,), + (2, 0), + (2, 1), + (3,), + (3, 0), + (3, 1), + (4,), + (4, 0), + (4, 1), ] ) def test_iter_rows_start(self): - result = list(self.model.iter_rows([2])) + result = list(self.model.iter_rows((2,))) self.assertEqual( result, - [[2], [2, 0], [2, 1]] + [(2,), (2, 0), (2, 1)] ) def test_iter_rows_leaf(self): result = list(self.model.iter_rows([2, 0])) - self.assertEqual(result, [[2, 0]]) + self.assertEqual(result, [(2, 0)]) def test_iter_items(self): result = list(self.model.iter_items()) self.assertEqual( result, [ - ([], []), - ([], [0]), ([], [1]), ([], [2]), - ([0], []), - ([0], [0]), ([0], [1]), ([0], [2]), - ([0, 0], []), - ([0, 0], [0]), ([0, 0], [1]), ([0, 0], [2]), - ([0, 1], []), - ([0, 1], [0]), ([0, 1], [1]), ([0, 1], [2]), - ([1], []), - ([1], [0]), ([1], [1]), ([1], [2]), - ([1, 0], []), - ([1, 0], [0]), ([1, 0], [1]), ([1, 0], [2]), - ([1, 1], []), - ([1, 1], [0]), ([1, 1], [1]), ([1, 1], [2]), - ([2], []), - ([2], [0]), ([2], [1]), ([2], [2]), - ([2, 0], []), - ([2, 0], [0]), ([2, 0], [1]), ([2, 0], [2]), - ([2, 1], []), - ([2, 1], [0]), ([2, 1], [1]), ([2, 1], [2]), - ([3], []), - ([3], [0]), ([3], [1]), ([3], [2]), - ([3, 0], []), - ([3, 0], [0]), ([3, 0], [1]), ([3, 0], [2]), - ([3, 1], []), - ([3, 1], [0]), ([3, 1], [1]), ([3, 1], [2]), - ([4], []), - ([4], [0]), ([4], [1]), ([4], [2]), - ([4, 0], []), - ([4, 0], [0]), ([4, 0], [1]), ([4, 0], [2]), - ([4, 1], []), - ([4, 1], [0]), ([4, 1], [1]), ([4, 1], [2]), + ((), ()), + ((), (0,)), ((), (1,)), ((), (2,)), + ((0,), ()), + ((0,), (0,)), ((0,), (1,)), ((0,), (2,)), + ((0, 0), ()), + ((0, 0), (0,)), ((0, 0), (1,)), ((0, 0), (2,)), + ((0, 1), ()), + ((0, 1), (0,)), ((0, 1), (1,)), ((0, 1), (2,)), + ((1,), ()), + ((1,), (0,)), ((1,), (1,)), ((1,), (2,)), + ((1, 0), ()), + ((1, 0), (0,)), ((1, 0), (1,)), ((1, 0), (2,)), + ((1, 1), ()), + ((1, 1), (0,)), ((1, 1), (1,)), ((1, 1), (2,)), + ((2,), ()), + ((2,), (0,)), ((2,), (1,)), ((2,), (2,)), + ((2, 0), ()), + ((2, 0), (0,)), ((2, 0), (1,)), ((2, 0), (2,)), + ((2, 1), ()), + ((2, 1), (0,)), ((2, 1), (1,)), ((2, 1), (2,)), + ((3,), ()), + ((3,), (0,)), ((3,), (1,)), ((3,), (2,)), + ((3, 0), ()), + ((3, 0), (0,)), ((3, 0), (1,)), ((3, 0), (2,)), + ((3, 1), ()), + ((3, 1), (0,)), ((3, 1), (1,)), ((3, 1), (2,)), + ((4,), ()), + ((4,), (0,)), ((4,), (1,)), ((4,), (2,)), + ((4, 0), ()), + ((4, 0), (0,)), ((4, 0), (1,)), ((4, 0), (2,)), + ((4, 1), ()), + ((4, 1), (0,)), ((4, 1), (1,)), ((4, 1), (2,)), ] ) def test_iter_items_start(self): - result = list(self.model.iter_items([2])) + result = list(self.model.iter_items((2,))) self.assertEqual( result, [ - ([2], []), - ([2], [0]), ([2], [1]), ([2], [2]), - ([2, 0], []), - ([2, 0], [0]), ([2, 0], [1]), ([2, 0], [2]), - ([2, 1], []), - ([2, 1], [0]), ([2, 1], [1]), ([2, 1], [2]), + ((2,), ()), + ((2,), (0,)), ((2,), (1,)), ((2,), (2,)), + ((2, 0), ()), + ((2, 0), (0,)), ((2, 0), (1,)), ((2, 0), (2,)), + ((2, 1), ()), + ((2, 1), (0,)), ((2, 1), (1,)), ((2, 1), (2,)), ] ) def test_iter_items_leaf(self): - result = list(self.model.iter_items([2, 0])) + result = list(self.model.iter_items((2, 0))) self.assertEqual( result, [ - ([2, 0], []), - ([2, 0], [0]), ([2, 0], [1]), ([2, 0], [2]), + ((2, 0), ()), + ((2, 0), (0,)), ((2, 0), (1,)), ((2, 0), (2,)), ] ) diff --git a/pyface/data_view/index_manager.py b/pyface/data_view/index_manager.py index d9de16da0..64783cc5d 100644 --- a/pyface/data_view/index_manager.py +++ b/pyface/data_view/index_manager.py @@ -54,7 +54,6 @@ """ from abc import abstractmethod -import typing as t from traits.api import ABCHasStrictTraits, Dict, Int, Tuple @@ -68,7 +67,7 @@ class AbstractIndexManager(ABCHasStrictTraits): """ @abstractmethod - def create_index(self, parent: t.Any, row: int) -> t.Any: + def create_index(self, parent, row): """ Given a parent index and a row number, create an index. The internal structure of the index should not matter to @@ -98,7 +97,7 @@ def create_index(self, parent: t.Any, row: int) -> t.Any: raise NotImplementedError @abstractmethod - def get_parent_and_row(self, index: t.Any) -> t.Tuple[t.Any, int]: + def get_parent_and_row(self, index): """ Given an index object, return the parent index and row. Parameters @@ -121,7 +120,7 @@ def get_parent_and_row(self, index: t.Any) -> t.Tuple[t.Any, int]: """ raise NotImplementedError - def from_sequence(self, indices: t.Sequence[int]) -> t.Any: + def from_sequence(self, indices): """ Given a sequence of indices, return the index object. The default implementation starts at the root and repeatedly calls @@ -149,7 +148,7 @@ def from_sequence(self, indices: t.Sequence[int]) -> t.Any: index = self.create_index(index, row) return index - def to_sequence(self, index: t.Any) -> t.List[int]: + def to_sequence(self, index): """ Given an index, return the corresponding sequence of row values. The default implementation repeatedly calls get_parent_and_row() @@ -166,14 +165,14 @@ def to_sequence(self, index: t.Any) -> t.List[int]: sequence : list of int The row location at each level of the heirarchy. """ - result: t.List[int] = [] + result = () while index != Root: index, row = self.get_parent_and_row(index) - result.insert(0, row) + result = (row,) + result return result @abstractmethod - def from_id(self, id: int) -> t.Any: + def from_id(self, id): """ Given an integer id, return the corresponding index. Parameters @@ -189,7 +188,7 @@ def from_id(self, id: int) -> t.Any: raise NotImplementedError @abstractmethod - def id(self, index: t.Any) -> int: + def id(self, index): """ Given an index, return the corresponding id. Parameters @@ -235,7 +234,7 @@ class IntIndexManager(AbstractIndexManager): efficient. """ - def create_index(self, parent: t.Any, row: int) -> t.Any: + def create_index(self, parent, row): """ Given a parent index and a row number, create an index. This should only ever be called with Root as the parent. @@ -270,7 +269,7 @@ def create_index(self, parent: t.Any, row: int) -> t.Any: ) return row - def get_parent_and_row(self, index: t.Any) -> t.Tuple[t.Any, int]: + def get_parent_and_row(self, index): """ Given an index object, return the parent index and row. Parameters @@ -295,7 +294,7 @@ def get_parent_and_row(self, index: t.Any) -> t.Tuple[t.Any, int]: raise IndexError("Root index has no parent.") return Root, int(index) - def from_id(self, id: int) -> t.Any: + def from_id(self, id): """ Given an integer id, return the corresponding index. Parameters @@ -312,7 +311,7 @@ def from_id(self, id: int) -> t.Any: return Root return id - 1 - def id(self, index: t.Any) -> int: + def id(self, index): """ Given an index, return the corresponding id. Parameters @@ -338,7 +337,7 @@ class TupleIndexManager(AbstractIndexManager): #: A dictionary that maps ids to the canonical version of the tuple. _id_cache = Dict(Int, Tuple, {0: Root}, can_reset=True) - def create_index(self, parent: t.Any, row: int) -> t.Any: + def create_index(self, parent, row): """ Given a parent index and a row number, create an index. Parameters @@ -366,7 +365,7 @@ def create_index(self, parent: t.Any, row: int) -> t.Any: self._id_cache[self.id(canonical_index)] = canonical_index return canonical_index - def get_parent_and_row(self, index: t.Any) -> t.Tuple[t.Any, int]: + def get_parent_and_row(self, index): """ Given an index object, return the parent index and row. Parameters @@ -391,7 +390,7 @@ def get_parent_and_row(self, index: t.Any) -> t.Tuple[t.Any, int]: raise IndexError("Root index has no parent.") return index - def from_id(self, id: int) -> t.Any: + def from_id(self, id): """ Given an integer id, return the corresponding index. Parameters @@ -406,7 +405,7 @@ def from_id(self, id: int) -> t.Any: """ return self._id_cache[id] - def id(self, index: t.Any) -> int: + def id(self, index): """ Given an index, return the corresponding id. Parameters diff --git a/pyface/data_view/tests/test_index_manager.py b/pyface/data_view/tests/test_index_manager.py index 1dcd82615..95ae31000 100644 --- a/pyface/data_view/tests/test_index_manager.py +++ b/pyface/data_view/tests/test_index_manager.py @@ -24,7 +24,7 @@ def test_root_has_no_parent(self): def test_root_to_sequence(self): result = self.index_manager.to_sequence(Root) - self.assertEqual(result, []) + self.assertEqual(result, ()) def test_root_from_sequence(self): result = self.index_manager.from_sequence([]) @@ -38,19 +38,19 @@ def test_root_id_round_trip(self): self.assertIs(result, Root) def test_simple_sequence_round_trip(self): - sequence = [5] + sequence = (5,) index = self.index_manager.from_sequence(sequence) result = self.index_manager.to_sequence(index) self.assertEqual(result, sequence) def test_simple_sequence_invalid(self): - sequence = [-5] + sequence = (-5,) with self.assertRaises(IndexError): self.index_manager.from_sequence(sequence) def test_simple_sequence_to_parent_row(self): - sequence = [5] + sequence = (5,) index = self.index_manager.from_sequence(sequence) result = self.index_manager.get_parent_and_row(index) @@ -70,7 +70,7 @@ def test_simple_row_to_sequence(self): index = self.index_manager.create_index(Root, 5) result = self.index_manager.to_sequence(index) - self.assertEqual(result, [5]) + self.assertEqual(result, (5,)) def test_simple_id_round_trip(self): index = self.index_manager.create_index(Root, 5) @@ -112,21 +112,21 @@ def tearDown(self): self.index_manager.reset() def test_complex_sequence_round_trip(self): - sequence = [5, 6, 7, 8, 9, 10] + sequence = (5, 6, 7, 8, 9, 10) index = self.index_manager.from_sequence(sequence) result = self.index_manager.to_sequence(index) self.assertEqual(result, sequence) def test_complex_sequence_identical_index(self): - sequence = [5, 6, 7, 8, 9, 10] + sequence = (5, 6, 7, 8, 9, 10) index_1 = self.index_manager.from_sequence(sequence[:]) index_2 = self.index_manager.from_sequence(sequence[:]) self.assertIs(index_1, index_2) def test_complex_sequence_to_parent_row(self): - sequence = [5, 6, 7, 8, 9, 10] + sequence = (5, 6, 7, 8, 9, 10) index = self.index_manager.from_sequence(sequence) parent, row = self.index_manager.get_parent_and_row(index) @@ -134,11 +134,11 @@ def test_complex_sequence_to_parent_row(self): self.assertEqual(row, 10) self.assertIs( parent, - self.index_manager.from_sequence([5, 6, 7, 8, 9]) + self.index_manager.from_sequence((5, 6, 7, 8, 9)) ) def test_complex_index_round_trip(self): - sequence = [5, 6, 7, 8, 9, 10] + sequence = (5, 6, 7, 8, 9, 10) parent = Root for depth, row in enumerate(sequence): @@ -150,7 +150,7 @@ def test_complex_index_round_trip(self): parent = index def test_complex_index_create_index_identical(self): - sequence = [5, 6, 7, 8, 9, 10] + sequence = (5, 6, 7, 8, 9, 10) parent = Root for depth, row in enumerate(sequence): @@ -161,7 +161,7 @@ def test_complex_index_create_index_identical(self): parent = index_1 def test_complex_index_to_sequence(self): - sequence = [5, 6, 7, 8, 9, 10] + sequence = (5, 6, 7, 8, 9, 10) parent = Root for depth, row in enumerate(sequence): with self.subTest(depth=depth): @@ -181,7 +181,7 @@ def test_complex_index_sequence_round_trip(self): parent = index def test_complex_index_id_round_trip(self): - sequence = [5, 6, 7, 8, 9, 10] + sequence = (5, 6, 7, 8, 9, 10) parent = Root for depth, row in enumerate(sequence): with self.subTest(depth=depth): diff --git a/pyface/ui/qt4/data_view/data_view_item_model.py b/pyface/ui/qt4/data_view/data_view_item_model.py index ccadf8cc9..357263d79 100644 --- a/pyface/ui/qt4/data_view/data_view_item_model.py +++ b/pyface/ui/qt4/data_view/data_view_item_model.py @@ -73,10 +73,10 @@ def on_structure_changed(self, event): def on_values_changed(self, event): top, left, bottom, right = event.new - if top == [] and bottom == []: + if top == () and bottom == (): # this is a column header change self.headerDataChanged.emit(left[0], right[0]) - elif left == [] and right == []: + elif left == () and right == (): # this is a row header change # XXX this is currently not supported and not needed pass @@ -181,15 +181,15 @@ def setData(self, index, value, role=Qt.EditRole): def headerData(self, section, orientation, role=Qt.DisplayRole): if orientation == Qt.Horizontal: - row = [] + row = () if section == 0: - column = [] + column = () else: - column = [section - 1] + column = (section - 1,) else: # XXX not currently used, but here for symmetry and completeness - row = [section] - column = [] + row = (section,) + column = () value_type = self.model.get_value_type(row, column) @@ -201,25 +201,25 @@ def headerData(self, section, orientation, role=Qt.DisplayRole): def _to_row_index(self, index): if not index.isValid(): - row_index = [] + row_index = () else: parent = index.internalPointer() if parent == Root: - row_index = [] + row_index = () else: row_index = self.model.index_manager.to_sequence(parent) - row_index.append(index.row()) + row_index += (index.row(),) return row_index def _to_column_index(self, index): if not index.isValid(): - return [] + return () else: column = index.column() if column == 0: - return [] + return () else: - return [column - 1] + return (column - 1,) def _to_model_index(self, row_index, column_index): if len(row_index) == 0: diff --git a/pyface/ui/wx/data_view/data_view_model.py b/pyface/ui/wx/data_view/data_view_model.py index 22a0e843c..6d18546e4 100644 --- a/pyface/ui/wx/data_view/data_view_model.py +++ b/pyface/ui/wx/data_view/data_view_model.py @@ -62,10 +62,10 @@ def on_structure_changed(self, event): def on_values_changed(self, event): top, left, bottom, right = event.new - if top == [] and bottom == []: + if top == () and bottom == (): # this is a column header change, reset everything self.Cleared() - elif left == [] and right == []: + elif left == () and right == (): # this is a row header change # XXX this is currently not supported and not needed pass @@ -124,9 +124,9 @@ def HasChildren(self, item): def GetValue(self, item, column): row_index = self._to_row_index(item) if column == 0: - column_index = [] + column_index = () else: - column_index = [column - 1] + column_index = (column - 1,) value_type = self.model.get_value_type(row_index, column_index) if value_type.has_text(self.model, row_index, column_index): return value_type.get_text(self.model, row_index, column_index) @@ -135,9 +135,9 @@ def GetValue(self, item, column): def SetValue(self, value, item, column): row_index = self._to_row_index(item) if column == 0: - column_index = [] + column_index = () else: - column_index = [column - 1] + column_index = (column - 1,) try: result = self.model.set_text(row_index, column_index, value) except Exception as exc: @@ -147,10 +147,10 @@ def SetValue(self, value, item, column): return result def GetColumnCount(self): - return self.model.get_column_count([]) + 1 + return self.model.get_column_count(()) + 1 def GetColumnType(self, column): - value_type = self.model.get_value_type([], [column-1]) + value_type = self.model.get_value_type((), (column-1,)) return type_hint_to_variant.get(value_type.type_hint, "string") def _to_row_index(self, item): From 483db31542607472dd7a0fb8c96e9b53336077d6 Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Wed, 1 Jul 2020 18:57:50 +0100 Subject: [PATCH 23/52] Fix bug in set method. --- pyface/ui/qt4/data_view/data_view_item_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyface/ui/qt4/data_view/data_view_item_model.py b/pyface/ui/qt4/data_view/data_view_item_model.py index 357263d79..4d5303700 100644 --- a/pyface/ui/qt4/data_view/data_view_item_model.py +++ b/pyface/ui/qt4/data_view/data_view_item_model.py @@ -173,7 +173,7 @@ def setData(self, index, value, role=Qt.EditRole): if role == Qt.EditRole: if value_type.can_edit(self.model, row, column): return value_type.set_editable(self.model, row, column, value) - elif role == Qt.TextRole: + elif role == Qt.DisplayRole: if value_type.has_text(self.model, row, column): return value_type.set_text(self.model, row, column, value) From 7e8ba1e5576ef9573e80d5fffb16280de5cc4d0e Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Fri, 3 Jul 2020 10:37:00 +0100 Subject: [PATCH 24/52] Make column count constant, plus some small fixes. --- docs/source/data_view.rst | 2 +- examples/data_view/column_data_model.py | 2 +- pyface/data_view/abstract_data_model.py | 17 +++++------------ .../data_view/data_models/array_data_model.py | 16 ++++++---------- .../data_models/tests/test_array_data_model.py | 6 ++---- pyface/ui/qt4/data_view/data_view_item_model.py | 6 +++++- pyface/ui/wx/data_view/data_view_model.py | 2 +- 7 files changed, 21 insertions(+), 30 deletions(-) diff --git a/docs/source/data_view.rst b/docs/source/data_view.rst index f9771a9df..b1470e60c 100644 --- a/docs/source/data_view.rst +++ b/docs/source/data_view.rst @@ -29,7 +29,7 @@ Data Structure The keys are displayed in the row headers, so each row has one column displaying the value:: - def get_column_count(self, row): + def get_column_count(self): return 1 The data is non-heirarchical, so the root has children and no other diff --git a/examples/data_view/column_data_model.py b/examples/data_view/column_data_model.py index 4e6edd8c5..78bbe18ab 100644 --- a/examples/data_view/column_data_model.py +++ b/examples/data_view/column_data_model.py @@ -169,7 +169,7 @@ class ColumnDataModel(AbstractDataModel): #: indices. index_manager = Instance(TupleIndexManager, args=()) - def get_column_count(self, row): + def get_column_count(self): return len(self.data) def can_have_children(self, row): diff --git a/pyface/data_view/abstract_data_model.py b/pyface/data_view/abstract_data_model.py index 65c62f1a4..00b0cc729 100644 --- a/pyface/data_view/abstract_data_model.py +++ b/pyface/data_view/abstract_data_model.py @@ -80,21 +80,14 @@ class AbstractDataModel(ABCHasStrictTraits): # Data structure methods @abstractmethod - def get_column_count(self, row): - """ How many columns in the row of the data view model. - - The total number of columns in the table is given by the column - count of the root row. - - Parameters - ---------- - row : sequence of int - The indices of the row as a sequence from root to leaf. + def get_column_count(self): + """ How many columns in the data view model. Returns ------- column_count : non-negative int - The number of columns that the row provides. + The number of columns that the data view provides. This count + should not include the row header. """ raise NotImplementedError @@ -260,7 +253,7 @@ def iter_items(self, start_row=()): Parameters ---------- - start : row index + start_row : row index The row to start iteration from. Yields diff --git a/pyface/data_view/data_models/array_data_model.py b/pyface/data_view/data_models/array_data_model.py index e2f2dd20b..177dd92f0 100644 --- a/pyface/data_view/data_models/array_data_model.py +++ b/pyface/data_view/data_models/array_data_model.py @@ -73,24 +73,20 @@ class ArrayDataModel(AbstractDataModel): # Data structure methods - def get_column_count(self, row): - """ How many columns in a row of the data view model. + def get_column_count(self): + """ How many columns in the data view model. - The number of columns is always the size of the last dimension of the - array. - - Parameters - ---------- - row : sequence of int - The indices of the row as a sequence from root to leaf. + The number of columns is the size of the last dimension of the array. Returns ------- column_count : non-negative int - The number of columns that the row provides. + The number of columns in the data view model, which is the size of + the last dimension of the array. """ return self.data.shape[-1] + def can_have_children(self, row): """ Whether or not a row can have child rows. diff --git a/pyface/data_view/data_models/tests/test_array_data_model.py b/pyface/data_view/data_models/tests/test_array_data_model.py index 1524ddba3..399a0df97 100644 --- a/pyface/data_view/data_models/tests/test_array_data_model.py +++ b/pyface/data_view/data_models/tests/test_array_data_model.py @@ -48,10 +48,8 @@ def model_structure_changed(self, event): self.structure_changed_event = event def test_get_column_count(self): - for row in self.model.iter_rows(): - with self.subTest(row=row): - result = self.model.get_column_count(row) - self.assertEqual(result, 3) + result = self.model.get_column_count() + self.assertEqual(result, 3) def test_can_have_children(self): for row in self.model.iter_rows(): diff --git a/pyface/ui/qt4/data_view/data_view_item_model.py b/pyface/ui/qt4/data_view/data_view_item_model.py index 4d5303700..e57fd3d49 100644 --- a/pyface/ui/qt4/data_view/data_view_item_model.py +++ b/pyface/ui/qt4/data_view/data_view_item_model.py @@ -127,7 +127,11 @@ def rowCount(self, index): def columnCount(self, index): row_index = self._to_row_index(index) try: - return self.model.get_column_count(row_index) + 1 + # the number of columns is constant; leaf rows return 0 + if self.model.can_have_children(row_index): + return self.model.get_column_count() + 1 + else: + return 0 except Exception: logger.exception("Error in columnCount") diff --git a/pyface/ui/wx/data_view/data_view_model.py b/pyface/ui/wx/data_view/data_view_model.py index 6d18546e4..f25dc140f 100644 --- a/pyface/ui/wx/data_view/data_view_model.py +++ b/pyface/ui/wx/data_view/data_view_model.py @@ -147,7 +147,7 @@ def SetValue(self, value, item, column): return result def GetColumnCount(self): - return self.model.get_column_count(()) + 1 + return self.model.get_column_count() + 1 def GetColumnType(self, column): value_type = self.model.get_value_type((), (column-1,)) From a96ac32d0a0800255543f6fca7159d3588f73294 Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Tue, 7 Jul 2020 12:20:49 +0100 Subject: [PATCH 25/52] Release model when destroying widgets. --- examples/data_view/array_example.py | 4 ++++ pyface/ui/qt4/data_view/data_view_widget.py | 1 + 2 files changed, 5 insertions(+) diff --git a/examples/data_view/array_example.py b/examples/data_view/array_example.py index 84f07659e..765d3ff38 100644 --- a/examples/data_view/array_example.py +++ b/examples/data_view/array_example.py @@ -37,6 +37,10 @@ def _data_default(self): import numpy return numpy.random.uniform(size=(10000, 10, 10))*1000000 + def destroy(self): + self.data_view.destroy() + super().destroy() + # Application entry point. if __name__ == "__main__": diff --git a/pyface/ui/qt4/data_view/data_view_widget.py b/pyface/ui/qt4/data_view/data_view_widget.py index 28ba09823..f636ff0dd 100644 --- a/pyface/ui/qt4/data_view/data_view_widget.py +++ b/pyface/ui/qt4/data_view/data_view_widget.py @@ -47,6 +47,7 @@ def _create_item_model(self): def destroy(self): """ Perform any actions required to destroy the control. """ + self.control.setModel(None) super().destroy() # ensure that we release the reference to the item model self._item_model = None From 1cc2cafcadf77c60a9a1e72ae9ea362bdffb2556 Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Tue, 7 Jul 2020 12:21:46 +0100 Subject: [PATCH 26/52] Fix docstrings. --- pyface/data_view/index_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyface/data_view/index_manager.py b/pyface/data_view/index_manager.py index 64783cc5d..06dddc8a7 100644 --- a/pyface/data_view/index_manager.py +++ b/pyface/data_view/index_manager.py @@ -162,7 +162,7 @@ def to_sequence(self, index): Returns ------- - sequence : list of int + sequence : tuple of int The row location at each level of the heirarchy. """ result = () From 3defcee4a3a58e5d1160bd1dfd04233c63f15b37 Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Tue, 7 Jul 2020 12:22:39 +0100 Subject: [PATCH 27/52] Rename "editable" -> "editor_value". --- pyface/data_view/abstract_value_type.py | 10 +++++----- pyface/data_view/value_types/editable_value.py | 8 ++++---- pyface/ui/qt4/data_view/data_view_item_model.py | 10 +++++----- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/pyface/data_view/abstract_value_type.py b/pyface/data_view/abstract_value_type.py index 86ed58eb0..f5d6de494 100644 --- a/pyface/data_view/abstract_value_type.py +++ b/pyface/data_view/abstract_value_type.py @@ -43,7 +43,7 @@ class AbstractValueType(ABCHasStrictTraits): #: Fired when a change occurs that requires updating values. updated = Event - def can_edit(self, model, row, column): + def has_editor_value(self, model, row, column): """ Return whether or not the value can be edited. The default implementation is that cells that can be set are @@ -60,12 +60,12 @@ def can_edit(self, model, row, column): Returns ------- - can_edit : bool + has_editor_value : bool Whether or not the value is editable. """ return model.can_set_value(row, column) - def get_editable(self, model, row, column): + def get_editor_value(self, model, row, column): """ Return a value suitable for editing. The default implementation is to return the underlying data value @@ -87,7 +87,7 @@ def get_editable(self, model, row, column): """ return model.get_value(row, column) - def set_editable(self, model, row, column, value): + def set_editor_value(self, model, row, column, value): """ Set a value that is returned from editing. The default implementation is to set the value directly from the @@ -109,7 +109,7 @@ def set_editable(self, model, row, column, value): success : bool Whether or not the value was successfully set. """ - if not self.can_edit(model, row, column): + if not self.has_editor_value(model, row, column): return False return model.set_value(row, column, value) diff --git a/pyface/data_view/value_types/editable_value.py b/pyface/data_view/value_types/editable_value.py index 627488171..5ed864da6 100644 --- a/pyface/data_view/value_types/editable_value.py +++ b/pyface/data_view/value_types/editable_value.py @@ -51,7 +51,7 @@ def is_valid(self, model, row, column, value): # AbstractValueType Interface -------------------------------------------- - def can_edit(self, model, row, column): + def has_editor_value(self, model, row, column): """ Return whether or not the value can be edited. A cell is editable if the underlying data can be set, and the @@ -68,12 +68,12 @@ def can_edit(self, model, row, column): Returns ------- - can_edit : bool + has_editor_value : bool Whether or not the value is editable. """ return model.can_set_value(row, column) and self.is_editable - def set_editable(self, model, row, column, value): + def set_editor_value(self, model, row, column, value): """ Set the edited value. Parameters @@ -92,7 +92,7 @@ def set_editable(self, model, row, column, value): success : bool Whether or not the value was set successfully. """ - if not (self.can_edit(model, row, column) + if not (self.has_editor_value(model, row, column) and self.is_valid(model, row, column, value)): return False return model.set_value(row, column, value) diff --git a/pyface/ui/qt4/data_view/data_view_item_model.py b/pyface/ui/qt4/data_view/data_view_item_model.py index e57fd3d49..5e762c7a3 100644 --- a/pyface/ui/qt4/data_view/data_view_item_model.py +++ b/pyface/ui/qt4/data_view/data_view_item_model.py @@ -146,7 +146,7 @@ def flags(self, index): if is_qt5 and not self.model.can_have_children(row): flags |= Qt.ItemNeverHasChildren - if value_type and value_type.can_edit(self.model, row, column): + if value_type and value_type.has_editor_value(self.model, row, column): flags |= Qt.ItemIsEditable return flags @@ -162,8 +162,8 @@ def data(self, index, role=Qt.DisplayRole): if value_type.has_text(self.model, row, column): return value_type.get_text(self.model, row, column) elif role == Qt.EditRole: - if value_type.can_edit(self.model, row, column): - return value_type.get_editable(self.model, row, column) + if value_type.has_editor_value(self.model, row, column): + return value_type.get_editor_value(self.model, row, column) return None @@ -175,8 +175,8 @@ def setData(self, index, value, role=Qt.EditRole): return False if role == Qt.EditRole: - if value_type.can_edit(self.model, row, column): - return value_type.set_editable(self.model, row, column, value) + if value_type.has_editor_value(self.model, row, column): + return value_type.set_editor_value(self.model, row, column, value) elif role == Qt.DisplayRole: if value_type.has_text(self.model, row, column): return value_type.set_text(self.model, row, column, value) From e9d680dc172287b95978393a74466d3bfb179271 Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Tue, 7 Jul 2020 12:26:25 +0100 Subject: [PATCH 28/52] And "editable" -> "editro_value" in other files. --- .../tests/test_abstract_value_type.py | 16 +++++++-------- .../data_view/value_types/constant_value.py | 2 +- pyface/data_view/value_types/no_value.py | 2 +- pyface/data_view/value_types/numeric_value.py | 4 ++-- .../value_types/tests/test_constant_value.py | 4 ++-- .../value_types/tests/test_editable_value.py | 20 +++++++++---------- .../value_types/tests/test_no_value.py | 4 ++-- .../value_types/tests/test_numeric_value.py | 16 +++++++-------- .../value_types/tests/test_text_value.py | 8 ++++---- 9 files changed, 38 insertions(+), 38 deletions(-) diff --git a/pyface/data_view/tests/test_abstract_value_type.py b/pyface/data_view/tests/test_abstract_value_type.py index 493fbdce4..54154af4f 100644 --- a/pyface/data_view/tests/test_abstract_value_type.py +++ b/pyface/data_view/tests/test_abstract_value_type.py @@ -31,25 +31,25 @@ def setUp(self): self.model.can_set_value = Mock(return_value=True) self.model.set_value = Mock(return_value=True) - def test_can_edit(self): + def test_has_editor_value(self): value_type = ValueType() - result = value_type.can_edit(self.model, [0], [0]) + result = value_type.has_editor_value(self.model, [0], [0]) self.assertTrue(result) - def test_get_editable(self): + def test_get_editor_value(self): value_type = ValueType() - result = value_type.get_editable(self.model, [0], [0]) + result = value_type.get_editor_value(self.model, [0], [0]) self.assertEqual(result, 1.0) - def test_set_editable(self): + def test_set_editor_value(self): value_type = ValueType() - result = value_type.set_editable(self.model, [0], [0], 2.0) + result = value_type.set_editor_value(self.model, [0], [0], 2.0) self.assertTrue(result) - def test_set_editable_can_edit_false(self): + def test_set_editor_value_has_editor_value_false(self): self.model.can_set_value = Mock(return_value=False) value_type = ValueType() - result = value_type.set_editable(self.model, [0], [0], 2.0) + result = value_type.set_editor_value(self.model, [0], [0], 2.0) self.assertFalse(result) def test_has_text(self): diff --git a/pyface/data_view/value_types/constant_value.py b/pyface/data_view/value_types/constant_value.py index 52925beee..c12436b5b 100644 --- a/pyface/data_view/value_types/constant_value.py +++ b/pyface/data_view/value_types/constant_value.py @@ -24,7 +24,7 @@ class ConstantValue(AbstractValueType): #: The text value to display. text = Str(update=True) - def can_edit(self, model, row, column): + def has_editor_value(self, model, row, column): return False def get_text(self, model, row, column): diff --git a/pyface/data_view/value_types/no_value.py b/pyface/data_view/value_types/no_value.py index 9172afdf7..085a9be8a 100644 --- a/pyface/data_view/value_types/no_value.py +++ b/pyface/data_view/value_types/no_value.py @@ -14,7 +14,7 @@ class NoValue(AbstractValueType): """ A ValueType that has no data in any channel. """ - def can_edit(self, model, row, column): + def has_editor_value(self, model, row, column): return False def has_text(self, model, row, column): diff --git a/pyface/data_view/value_types/numeric_value.py b/pyface/data_view/value_types/numeric_value.py index 86dca8ecb..e8373f0a7 100644 --- a/pyface/data_view/value_types/numeric_value.py +++ b/pyface/data_view/value_types/numeric_value.py @@ -45,7 +45,7 @@ def is_valid(self, model, row, column, value): except Exception: return False - def get_editable(self, model, row, column): + def get_editor_value(self, model, row, column): # evaluate is needed to convert numpy types to python types so # Qt recognises them return self.evaluate(model.get_value(row, column)) @@ -58,7 +58,7 @@ def set_text(self, model, row, column, text): value = self.evaluate(self.unformat(text)) except ValueError: return False - return self.set_editable(model, row, column, value) + return self.set_editor_value(model, row, column, value) class IntValue(NumericValue): diff --git a/pyface/data_view/value_types/tests/test_constant_value.py b/pyface/data_view/value_types/tests/test_constant_value.py index eeca97939..025dc1298 100644 --- a/pyface/data_view/value_types/tests/test_constant_value.py +++ b/pyface/data_view/value_types/tests/test_constant_value.py @@ -25,9 +25,9 @@ def test_defaults(self): value_type = ConstantValue() self.assertEqual(value_type.text, "") - def test_can_edit(self): + def test_has_editor_value(self): value_type = ConstantValue() - self.assertFalse(value_type.can_edit(self.model, [0], [0])) + self.assertFalse(value_type.has_editor_value(self.model, [0], [0])) def test_has_text(self): value_type = ConstantValue() diff --git a/pyface/data_view/value_types/tests/test_editable_value.py b/pyface/data_view/value_types/tests/test_editable_value.py index 0b433e7b2..632389cfb 100644 --- a/pyface/data_view/value_types/tests/test_editable_value.py +++ b/pyface/data_view/value_types/tests/test_editable_value.py @@ -39,29 +39,29 @@ def test_is_valid(self): result = value_type.is_valid(self.model, [0], [0], 2.0) self.assertTrue(result) - def test_can_edit(self): + def test_has_editor_value(self): value_type = EditableValue() - result = value_type.can_edit(self.model, [0], [0]) + result = value_type.has_editor_value(self.model, [0], [0]) self.assertTrue(result) - def test_can_edit_not_editable(self): + def test_has_editor_value_not_editable(self): value_type = EditableValue(is_editable=False) - result = value_type.can_edit(self.model, [0], [0]) + result = value_type.has_editor_value(self.model, [0], [0]) self.assertFalse(result) - def test_set_editable(self): + def test_set_editor_value(self): value_type = EditableValue() - result = value_type.set_editable(self.model, [0], [0], 2.0) + result = value_type.set_editor_value(self.model, [0], [0], 2.0) self.assertTrue(result) - def test_set_editable_not_editable(self): + def test_set_editor_value_not_editable(self): value_type = EditableValue(is_editable=False) - result = value_type.set_editable(self.model, [0], [0], 2.0) + result = value_type.set_editor_value(self.model, [0], [0], 2.0) self.assertFalse(result) - def test_set_editable_not_valid(self): + def test_set_editor_value_not_valid(self): value_type = EditableWithValid() - result = value_type.set_editable(self.model, [0], [0], -1.0) + result = value_type.set_editor_value(self.model, [0], [0], -1.0) self.assertFalse(result) def test_is_editable_update(self): diff --git a/pyface/data_view/value_types/tests/test_no_value.py b/pyface/data_view/value_types/tests/test_no_value.py index 8e49c821d..1e4a7659e 100644 --- a/pyface/data_view/value_types/tests/test_no_value.py +++ b/pyface/data_view/value_types/tests/test_no_value.py @@ -19,9 +19,9 @@ class TestNoValue(TestCase): def setUp(self): self.model = Mock() - def test_can_edit(self): + def test_has_editor_value(self): value_type = NoValue() - self.assertFalse(value_type.can_edit(self.model, [0], [0])) + self.assertFalse(value_type.has_editor_value(self.model, [0], [0])) def test_has_text(self): value_type = NoValue() diff --git a/pyface/data_view/value_types/tests/test_numeric_value.py b/pyface/data_view/value_types/tests/test_numeric_value.py index 301e87dce..b89d9fe56 100644 --- a/pyface/data_view/value_types/tests/test_numeric_value.py +++ b/pyface/data_view/value_types/tests/test_numeric_value.py @@ -40,29 +40,29 @@ def test_is_valid_error(self): value = NumericValue() self.assertFalse(value.is_valid(None, [0], [0], 'invalid')) - def test_get_editable(self): + def test_get_editor_value(self): value = NumericValue(evaluate=float) - editable = value.get_editable(self.model, [0], [0]) + editable = value.get_editor_value(self.model, [0], [0]) self.assertEqual(editable, 1.0) - def test_set_editable(self): + def test_set_editor_value(self): value = NumericValue(evaluate=float) - success = value.set_editable(self.model, [0], [0], 1.0) + success = value.set_editor_value(self.model, [0], [0], 1.0) self.assertTrue(success) self.model.set_value.assert_called_once_with([0], [0], 1.0) - def test_set_editable_invalid(self): + def test_set_editor_value_invalid(self): value = NumericValue(minimum=0.0, maximum=1.0) - success = value.set_editable(self.model, [0], [0], -1.0) + success = value.set_editor_value(self.model, [0], [0], -1.0) self.assertFalse(success) self.model.set_value.assert_not_called() - def test_set_editable_error(self): + def test_set_editor_value_error(self): value = NumericValue(minimum=0.0, maximum=1.0) - success = value.set_editable(self.model, [0], [0], 'invalid') + success = value.set_editor_value(self.model, [0], [0], 'invalid') self.assertFalse(success) self.model.set_value.assert_not_called() diff --git a/pyface/data_view/value_types/tests/test_text_value.py b/pyface/data_view/value_types/tests/test_text_value.py index a885b98e1..e6a4023ef 100644 --- a/pyface/data_view/value_types/tests/test_text_value.py +++ b/pyface/data_view/value_types/tests/test_text_value.py @@ -30,15 +30,15 @@ def test_is_valid(self): value = TextValue() self.assertTrue(value.is_valid(None, [0], [0], "test")) - def test_get_editable(self): + def test_get_editor_value(self): value = TextValue() - editable = value.get_editable(self.model, [0], [0]) + editable = value.get_editor_value(self.model, [0], [0]) self.assertEqual(editable, "test") - def test_set_editable(self): + def test_set_editor_value(self): value = TextValue() - success = value.set_editable(self.model, [0], [0], "test") + success = value.set_editor_value(self.model, [0], [0], "test") self.assertTrue(success) self.model.set_value.assert_called_once_with([0], [0], "test") From 9be4d981a3b86ad74634f04fe74251b514866572 Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Tue, 7 Jul 2020 13:15:30 +0100 Subject: [PATCH 29/52] Improve TextValue and NumericValues. --- pyface/data_view/value_types/numeric_value.py | 75 +++++++++++++++++++ pyface/data_view/value_types/text_value.py | 33 +++++++- 2 files changed, 104 insertions(+), 4 deletions(-) diff --git a/pyface/data_view/value_types/numeric_value.py b/pyface/data_view/value_types/numeric_value.py index e8373f0a7..28a2bdd4b 100644 --- a/pyface/data_view/value_types/numeric_value.py +++ b/pyface/data_view/value_types/numeric_value.py @@ -40,20 +40,91 @@ class NumericValue(EditableValue): unformat = Callable(locale.delocalize) def is_valid(self, model, row, column, value): + """ Whether or not the value within the specified range. + + Parameters + ---------- + model : AbstractDataModel + The data model holding the data. + row : sequence of int + The row in the data model being queried. + column : sequence of int + The column in the data model being queried. + value : any + The value to validate. + + Returns + ------- + is_valid : bool + Whether or not the value is valid. + """ try: return self.minimum <= value <= self.maximum except Exception: return False def get_editor_value(self, model, row, column): + """ Get the numerical value for the editor to use. + + This uses the evaluate method to convert the underlying value to a + number. + + Parameters + ---------- + model : AbstractDataModel + The data model holding the data. + row : sequence of int + The row in the data model being queried. + column : sequence of int + The column in the data model being queried. + + Returns + ------- + editor_value : number + Whether or not the value is editable. + """ # evaluate is needed to convert numpy types to python types so # Qt recognises them return self.evaluate(model.get_value(row, column)) def get_text(self, model, row, column): + """ Get the display text from the underlying value. + + Parameters + ---------- + model : AbstractDataModel + The data model holding the data. + row : sequence of int + The row in the data model being queried. + column : sequence of int + The column in the data model being queried. + + Returns + ------- + text : str + The text to display. + """ return self.format(model.get_value(row, column)) def set_text(self, model, row, column, text): + """ Set the text of the underlying value. + + Parameters + ---------- + model : AbstractDataModel + The data model holding the data. + row : sequence of int + The row in the data model being queried. + column : sequence of int + The column in the data model being queried. + text : str + The text to set. + + Returns + ------- + success : bool + Whether or not the value was successfully set. + """ try: value = self.evaluate(self.unformat(text)) except ValueError: @@ -62,10 +133,14 @@ def set_text(self, model, row, column, text): class IntValue(NumericValue): + """ Data channels for an integer value. + """ evaluate = Callable(int) class FloatValue(NumericValue): + """ Data channels for a floating point value. + """ evaluate = Callable(float) diff --git a/pyface/data_view/value_types/text_value.py b/pyface/data_view/value_types/text_value.py index 33e739eb6..2069c5f94 100644 --- a/pyface/data_view/value_types/text_value.py +++ b/pyface/data_view/value_types/text_value.py @@ -8,6 +8,8 @@ # # Thanks for using Enthought open source! +from traits.api import Callable + from .editable_value import EditableValue @@ -15,6 +17,31 @@ class TextValue(EditableValue): """ Editable value that presents a string value. """ + #: A function that converts the value to a string for display. + format = Callable(str, update=True) + + #: A function that converts to a value from a display string. + unformat = Callable(str) + + def get_text(self, model, row, column): + """ Get the display text from the underlying value. + + Parameters + ---------- + model : AbstractDataModel + The data model holding the data. + row : sequence of int + The row in the data model being queried. + column : sequence of int + The column in the data model being queried. + + Returns + ------- + text : str + The text to display. + """ + return self.format(model.get_value(row, column)) + def set_text(self, model, row, column, text): """ Set the text of the underlying value. @@ -34,7 +61,5 @@ def set_text(self, model, row, column, text): success : bool Whether or not the value was successfully set. """ - if model.can_set_value(row, column): - return model.set_value(row, column, text) - - return False + value = self.unformat(text) + return self.set_editor_value(model, row, column, value) From 8341a2d9e970f0f43fe28434136aa76dfb6db97c Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Tue, 7 Jul 2020 13:20:36 +0100 Subject: [PATCH 30/52] Fix spelling. --- docs/source/data_view.rst | 8 ++++---- pyface/data_view/abstract_data_model.py | 4 ++-- pyface/data_view/data_models/array_data_model.py | 2 +- pyface/data_view/index_manager.py | 6 +++--- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/source/data_view.rst b/docs/source/data_view.rst index b1470e60c..382428bd5 100644 --- a/docs/source/data_view.rst +++ b/docs/source/data_view.rst @@ -1,8 +1,8 @@ Pyface DataViews ================= -The Pyface DataView API allows visualization of heirarchical and -non-heirarchical tabular data. +The Pyface DataView API allows visualization of hierarchical and +non-hierarchical tabular data. Data Models ----------- @@ -20,7 +20,7 @@ A data model for a dictionary could be implemented like this:: index_manager = Instance(IntIndexManager, ()) The index manager is an ``IntIndexManager`` because the data is -non-heirarchical and this is more memory-efficient than the alternative +non-hierarchical and this is more memory-efficient than the alternative ``TupleIndexManager``. Data Structure @@ -32,7 +32,7 @@ displaying the value:: def get_column_count(self): return 1 -The data is non-heirarchical, so the root has children and no other +The data is non-hierarchical, so the root has children and no other rows have children:: def can_have_children(self, row): diff --git a/pyface/data_view/abstract_data_model.py b/pyface/data_view/abstract_data_model.py index 00b0cc729..a630bb904 100644 --- a/pyface/data_view/abstract_data_model.py +++ b/pyface/data_view/abstract_data_model.py @@ -26,7 +26,7 @@ class AbstractDataModel(ABCHasStrictTraits): """ Abstract base class for Pyface data models. - The data model API is intended to provide a common API for heirarchical + The data model API is intended to provide a common API for hierarchical and tabular data. This class is concerned with the structure, type and values provided by the data, but not with how the data is presented. @@ -36,7 +36,7 @@ class AbstractDataModel(ABCHasStrictTraits): Subclasses need to implement the ``get_column_count``, ``can_have_children`` and ``get_row_count`` methods to return the number - of columns in a particular row, as well as the heirarchical structure of + of columns in a particular row, as well as the hierarchical structure of the rows. Appropriate observers should be set up on the underlaying data so that the ``structure_changed`` event is fired when the values returned by these methods would change. diff --git a/pyface/data_view/data_models/array_data_model.py b/pyface/data_view/data_models/array_data_model.py index 177dd92f0..aca0fbd68 100644 --- a/pyface/data_view/data_models/array_data_model.py +++ b/pyface/data_view/data_models/array_data_model.py @@ -26,7 +26,7 @@ class ArrayDataModel(AbstractDataModel): """ A data model for an n-dim array. This data model presents the data from a multidimensional array - heirarchically by dimension. The underlying array must be at least 2 + hierarchically by dimension. The underlying array must be at least 2 dimensional. Values are adapted by the ``value_type`` trait. This provides sensible diff --git a/pyface/data_view/index_manager.py b/pyface/data_view/index_manager.py index 06dddc8a7..4583ba51a 100644 --- a/pyface/data_view/index_manager.py +++ b/pyface/data_view/index_manager.py @@ -38,11 +38,11 @@ An ABC that defines the API IntIndexManager - An efficient index manager for non-heirarchical data, such as + An efficient index manager for non-hierarchical data, such as lists and arrays. TupleIndexManager - An index manager that handles non-heirarchical data while trying + An index manager that handles non-hierarchical data while trying to be fast and memory efficient. The two concrete subclasses should be sufficient for most cases, but advanced @@ -223,7 +223,7 @@ def reset(self): class IntIndexManager(AbstractIndexManager): - """ Efficient IndexManager for non-heirarchical indexes. + """ Efficient IndexManager for non-hierarchical indexes. This is a simple index manager for flat data structures. The index values returned are either the Root, or simple integers From 277a966db3b399abc28f10f0106c54420922984e Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Tue, 7 Jul 2020 13:28:29 +0100 Subject: [PATCH 31/52] Instantiate exceptions. --- examples/data_view/column_data_model.py | 6 +++--- pyface/data_view/abstract_data_model.py | 10 +++++----- pyface/data_view/index_manager.py | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/examples/data_view/column_data_model.py b/examples/data_view/column_data_model.py index 78bbe18ab..cc1888eaf 100644 --- a/examples/data_view/column_data_model.py +++ b/examples/data_view/column_data_model.py @@ -57,18 +57,18 @@ def __iter__(self): @abstractmethod def get_value(self, obj): - raise NotImplementedError + raise NotImplementedError() @abstractmethod def can_set_value(self, obj): - raise NotImplementedError + raise NotImplementedError() def set_value(self, obj): return False @abstractmethod def get_observable(self, obj): - raise NotImplementedError + raise NotImplementedError() # trait observers diff --git a/pyface/data_view/abstract_data_model.py b/pyface/data_view/abstract_data_model.py index a630bb904..7947eecd0 100644 --- a/pyface/data_view/abstract_data_model.py +++ b/pyface/data_view/abstract_data_model.py @@ -89,7 +89,7 @@ def get_column_count(self): The number of columns that the data view provides. This count should not include the row header. """ - raise NotImplementedError + raise NotImplementedError() @abstractmethod def can_have_children(self, row): @@ -107,7 +107,7 @@ def can_have_children(self, row): can_have_children : bool Whether or not the row can ever have child rows. """ - raise NotImplementedError + raise NotImplementedError() @abstractmethod def get_row_count(self, row): @@ -123,7 +123,7 @@ def get_row_count(self, row): row_count : non-negative int The number of child rows that the row has. """ - raise NotImplementedError + raise NotImplementedError() # Data value methods @@ -147,7 +147,7 @@ def get_value(self, row, column): value : any The value represented by the given row and column. """ - raise NotImplementedError + raise NotImplementedError() def can_set_value(self, row, column): """ Whether the value in the indicated row and column can be set. @@ -220,7 +220,7 @@ def get_value_type(self, row, column): The value type of the given row and column, or None if no value should be displayed. """ - raise NotImplementedError + raise NotImplementedError() # Convenience iterator methods diff --git a/pyface/data_view/index_manager.py b/pyface/data_view/index_manager.py index 4583ba51a..edefed59d 100644 --- a/pyface/data_view/index_manager.py +++ b/pyface/data_view/index_manager.py @@ -94,7 +94,7 @@ def create_index(self, parent, row): If asked to create a persistent index for a parent and row where that is not possible, a RuntimeError will be raised. """ - raise NotImplementedError + raise NotImplementedError() @abstractmethod def get_parent_and_row(self, index): @@ -118,7 +118,7 @@ def get_parent_and_row(self, index): If the Root object is passed as the index, this method will raise an IndexError, as it has no parent. """ - raise NotImplementedError + raise NotImplementedError() def from_sequence(self, indices): """ Given a sequence of indices, return the index object. @@ -185,7 +185,7 @@ def from_id(self, id): index : index object The persistent index object associated with this id. """ - raise NotImplementedError + raise NotImplementedError() @abstractmethod def id(self, index): @@ -201,7 +201,7 @@ def id(self, index): index : index object The persistent index object associated with this id. """ - raise NotImplementedError + raise NotImplementedError() def reset(self): """ Reset any caches and other state. From a02a48bbb61612fe789b307f601faf21c89cb92c Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Tue, 7 Jul 2020 15:43:21 +0100 Subject: [PATCH 32/52] Move to using exceptions to handle failure to set values. --- pyface/data_view/abstract_data_model.py | 13 ++++--- pyface/data_view/abstract_value_type.py | 8 ++--- .../tests/test_abstract_value_type.py | 22 ++++++++---- .../data_view/value_types/editable_value.py | 15 ++++---- pyface/data_view/value_types/numeric_value.py | 13 ++++--- .../value_types/tests/test_editable_value.py | 20 ++++++----- .../value_types/tests/test_numeric_value.py | 30 ++++++---------- .../value_types/tests/test_text_value.py | 20 ++++------- pyface/data_view/value_types/text_value.py | 8 ++--- .../ui/qt4/data_view/data_view_item_model.py | 30 +++++++++++----- pyface/ui/wx/data_view/data_view_model.py | 36 +++++++++++++++---- 11 files changed, 126 insertions(+), 89 deletions(-) diff --git a/pyface/data_view/abstract_data_model.py b/pyface/data_view/abstract_data_model.py index 7947eecd0..0e751eeba 100644 --- a/pyface/data_view/abstract_data_model.py +++ b/pyface/data_view/abstract_data_model.py @@ -23,6 +23,11 @@ from .index_manager import AbstractIndexManager +class DataViewSetError(ValueError): + """ An exception raised when setting a value fails. """ + pass + + class AbstractDataModel(ABCHasStrictTraits): """ Abstract base class for Pyface data models. @@ -192,12 +197,12 @@ def set_value(self, row, column, value): value : any The new value for the given row and column. - Returns + Raises ------- - success : bool - Whether or not the value was set successfully. + DataViewSetError + If the value cannot be set. """ - return False + raise DataViewSetError() @abstractmethod def get_value_type(self, row, column): diff --git a/pyface/data_view/abstract_value_type.py b/pyface/data_view/abstract_value_type.py index f5d6de494..8e80e3396 100644 --- a/pyface/data_view/abstract_value_type.py +++ b/pyface/data_view/abstract_value_type.py @@ -104,13 +104,11 @@ def set_editor_value(self, model, row, column, value): value : any The value to set. - Returns + Raises ------- - success : bool - Whether or not the value was successfully set. + DataViewSetError + If the value cannot be set. """ - if not self.has_editor_value(model, row, column): - return False return model.set_value(row, column, value) def has_text(self, model, row, column): diff --git a/pyface/data_view/tests/test_abstract_value_type.py b/pyface/data_view/tests/test_abstract_value_type.py index 54154af4f..a4d2afb9d 100644 --- a/pyface/data_view/tests/test_abstract_value_type.py +++ b/pyface/data_view/tests/test_abstract_value_type.py @@ -14,6 +14,7 @@ from traits.api import Str from traits.testing.unittest_tools import UnittestTools +from pyface.data_view.abstract_data_model import DataViewSetError from pyface.data_view.abstract_value_type import AbstractValueType @@ -29,13 +30,19 @@ def setUp(self): self.model = Mock() self.model.get_value = Mock(return_value=1.0) self.model.can_set_value = Mock(return_value=True) - self.model.set_value = Mock(return_value=True) + self.model.set_value = Mock() def test_has_editor_value(self): value_type = ValueType() result = value_type.has_editor_value(self.model, [0], [0]) self.assertTrue(result) + def test_has_editor_value_can_set_value_false(self): + self.model.can_set_value = Mock(return_value=False) + value_type = ValueType() + result = value_type.has_editor_value(self.model, [0], [0]) + self.assertFalse(result) + def test_get_editor_value(self): value_type = ValueType() result = value_type.get_editor_value(self.model, [0], [0]) @@ -43,14 +50,15 @@ def test_get_editor_value(self): def test_set_editor_value(self): value_type = ValueType() - result = value_type.set_editor_value(self.model, [0], [0], 2.0) - self.assertTrue(result) + value_type.set_editor_value(self.model, [0], [0], 2.0) + self.model.set_value.assert_called_once_with([0], [0], 2.0) - def test_set_editor_value_has_editor_value_false(self): - self.model.can_set_value = Mock(return_value=False) + def test_set_editor_value_set_value_raises(self): + self.model.set_value = Mock(side_effect=DataViewSetError) value_type = ValueType() - result = value_type.set_editor_value(self.model, [0], [0], 2.0) - self.assertFalse(result) + with self.assertRaises(DataViewSetError): + value_type.set_editor_value(self.model, [0], [0], 2.0) + self.model.set_value.assert_called_once_with([0], [0], 2.0) def test_has_text(self): value_type = ValueType() diff --git a/pyface/data_view/value_types/editable_value.py b/pyface/data_view/value_types/editable_value.py index 5ed864da6..cc7596556 100644 --- a/pyface/data_view/value_types/editable_value.py +++ b/pyface/data_view/value_types/editable_value.py @@ -10,6 +10,7 @@ from traits.api import Bool +from pyface.data_view.abstract_data_model import DataViewSetError from pyface.data_view.abstract_value_type import AbstractValueType @@ -87,12 +88,12 @@ def set_editor_value(self, model, row, column, value): value : any The value being set. - Returns + Raises ------- - success : bool - Whether or not the value was set successfully. + DataViewSetError + If the value cannot be set. """ - if not (self.has_editor_value(model, row, column) - and self.is_valid(model, row, column, value)): - return False - return model.set_value(row, column, value) + if self.is_valid(model, row, column, value): + model.set_value(row, column, value) + else: + raise DataViewSetError("Invalid value set: {!r}".format(value)) diff --git a/pyface/data_view/value_types/numeric_value.py b/pyface/data_view/value_types/numeric_value.py index 28a2bdd4b..ac1067095 100644 --- a/pyface/data_view/value_types/numeric_value.py +++ b/pyface/data_view/value_types/numeric_value.py @@ -13,6 +13,7 @@ from traits.api import Callable, Float +from pyface.data_view.abstract_data_model import DataViewSetError from .editable_value import EditableValue @@ -120,16 +121,18 @@ def set_text(self, model, row, column, text): text : str The text to set. - Returns + Raises ------- - success : bool - Whether or not the value was successfully set. + DataViewSetError + If the value cannot be set. """ try: value = self.evaluate(self.unformat(text)) except ValueError: - return False - return self.set_editor_value(model, row, column, value) + raise DataViewSetError( + "Can't evaluate value: {!r}".format(text) + ) + self.set_editor_value(model, row, column, value) class IntValue(NumericValue): diff --git a/pyface/data_view/value_types/tests/test_editable_value.py b/pyface/data_view/value_types/tests/test_editable_value.py index 632389cfb..c6a28bd49 100644 --- a/pyface/data_view/value_types/tests/test_editable_value.py +++ b/pyface/data_view/value_types/tests/test_editable_value.py @@ -13,6 +13,7 @@ from traits.testing.unittest_tools import UnittestTools +from pyface.data_view.abstract_data_model import DataViewSetError from pyface.data_view.value_types.editable_value import EditableValue @@ -28,7 +29,7 @@ def setUp(self): self.model = Mock() self.model.get_value = Mock(return_value=1.0) self.model.can_set_value = Mock(return_value=True) - self.model.set_value = Mock(return_value=True) + self.model.set_value = Mock() def test_default(self): value_type = EditableValue() @@ -51,18 +52,21 @@ def test_has_editor_value_not_editable(self): def test_set_editor_value(self): value_type = EditableValue() - result = value_type.set_editor_value(self.model, [0], [0], 2.0) - self.assertTrue(result) + value_type.set_editor_value(self.model, [0], [0], 2.0) + self.model.set_value.assert_called_once_with([0], [0], 2.0) - def test_set_editor_value_not_editable(self): + def test_set_editor_value_set_value_raises(self): + self.model.set_value = Mock(side_effect=DataViewSetError) value_type = EditableValue(is_editable=False) - result = value_type.set_editor_value(self.model, [0], [0], 2.0) - self.assertFalse(result) + with self.assertRaises(DataViewSetError): + value_type.set_editor_value(self.model, [0], [0], 2.0) + self.model.set_value.assert_called_once_with([0], [0], 2.0) def test_set_editor_value_not_valid(self): value_type = EditableWithValid() - result = value_type.set_editor_value(self.model, [0], [0], -1.0) - self.assertFalse(result) + with self.assertRaises(DataViewSetError): + value_type.set_editor_value(self.model, [0], [0], -1.0) + self.model.set_value.assert_not_called() def test_is_editable_update(self): value_type = EditableValue() diff --git a/pyface/data_view/value_types/tests/test_numeric_value.py b/pyface/data_view/value_types/tests/test_numeric_value.py index b89d9fe56..7611be9c4 100644 --- a/pyface/data_view/value_types/tests/test_numeric_value.py +++ b/pyface/data_view/value_types/tests/test_numeric_value.py @@ -11,6 +11,7 @@ from unittest import TestCase from unittest.mock import Mock +from pyface.data_view.abstract_data_model import DataViewSetError from pyface.data_view.value_types.numeric_value import ( FloatValue, IntValue, NumericValue, format_locale ) @@ -48,50 +49,41 @@ def test_get_editor_value(self): def test_set_editor_value(self): value = NumericValue(evaluate=float) - success = value.set_editor_value(self.model, [0], [0], 1.0) - - self.assertTrue(success) + value.set_editor_value(self.model, [0], [0], 1.0) self.model.set_value.assert_called_once_with([0], [0], 1.0) def test_set_editor_value_invalid(self): value = NumericValue(minimum=0.0, maximum=1.0) - success = value.set_editor_value(self.model, [0], [0], -1.0) - - self.assertFalse(success) + with self.assertRaises(DataViewSetError): + value.set_editor_value(self.model, [0], [0], -1.0) self.model.set_value.assert_not_called() def test_set_editor_value_error(self): value = NumericValue(minimum=0.0, maximum=1.0) - success = value.set_editor_value(self.model, [0], [0], 'invalid') - - self.assertFalse(success) + with self.assertRaises(DataViewSetError): + value.set_editor_value(self.model, [0], [0], 'invalid') self.model.set_value.assert_not_called() def test_get_text(self): value = NumericValue() text = value.get_text(self.model, [0], [0]) - self.assertEqual(text, format_locale(1.0)) def test_set_text(self): value = NumericValue(evaluate=float) - success = value.set_text(self.model, [0], [0], format_locale(1.1)) - - self.assertTrue(success) + value.set_text(self.model, [0], [0], format_locale(1.1)) self.model.set_value.assert_called_once_with([0], [0], 1.1) def test_set_text_invalid(self): value = NumericValue(evaluate=float, minimum=0.0, maximum=1.0) - success = value.set_text(self.model, [0], [0], format_locale(1.1)) - - self.assertFalse(success) + with self.assertRaises(DataViewSetError): + value.set_text(self.model, [0], [0], format_locale(1.1)) self.model.set_value.assert_not_called() def test_set_text_error(self): value = NumericValue(evaluate=float) - success = value.set_text(self.model, [0], [0], "invalid") - - self.assertFalse(success) + with self.assertRaises(DataViewSetError): + value.set_text(self.model, [0], [0], "invalid") self.model.set_value.assert_not_called() diff --git a/pyface/data_view/value_types/tests/test_text_value.py b/pyface/data_view/value_types/tests/test_text_value.py index e6a4023ef..78736e829 100644 --- a/pyface/data_view/value_types/tests/test_text_value.py +++ b/pyface/data_view/value_types/tests/test_text_value.py @@ -11,6 +11,7 @@ from unittest import TestCase from unittest.mock import Mock +from pyface.data_view.abstract_data_model import DataViewSetError from pyface.data_view.value_types.text_value import TextValue @@ -20,7 +21,7 @@ def setUp(self): self.model = Mock() self.model.get_value = Mock(return_value="test") self.model.can_set_value = Mock(return_value=True) - self.model.set_value = Mock(return_value=True) + self.model.set_value = Mock() def test_defaults(self): value = TextValue() @@ -33,34 +34,25 @@ def test_is_valid(self): def test_get_editor_value(self): value = TextValue() editable = value.get_editor_value(self.model, [0], [0]) - self.assertEqual(editable, "test") def test_set_editor_value(self): value = TextValue() - success = value.set_editor_value(self.model, [0], [0], "test") - - self.assertTrue(success) + value.set_editor_value(self.model, [0], [0], "test") self.model.set_value.assert_called_once_with([0], [0], "test") def test_get_text(self): value = TextValue() editable = value.get_text(self.model, [0], [0]) - self.assertEqual(editable, "test") def test_set_text(self): value = TextValue() - success = value.set_text(self.model, [0], [0], "test") - - self.assertTrue(success) + value.set_text(self.model, [0], [0], "test") self.model.set_value.assert_called_once_with([0], [0], "test") def test_set_text_no_set_value(self): self.model.can_set_value = Mock(return_value=False) - value = TextValue() - success = value.set_text(self.model, [0], [0], "test") - - self.assertFalse(success) - self.model.set_value.assert_not_called() + value.set_text(self.model, [0], [0], "test") + self.model.set_value.assert_called_once_with([0], [0], "test") diff --git a/pyface/data_view/value_types/text_value.py b/pyface/data_view/value_types/text_value.py index 2069c5f94..92329c9b2 100644 --- a/pyface/data_view/value_types/text_value.py +++ b/pyface/data_view/value_types/text_value.py @@ -56,10 +56,10 @@ def set_text(self, model, row, column, text): text : str The text to set. - Returns + Raises ------- - success : bool - Whether or not the value was successfully set. + DataViewSetError + If the value cannot be set. """ value = self.unformat(text) - return self.set_editor_value(model, row, column, value) + self.set_editor_value(model, row, column, value) diff --git a/pyface/ui/qt4/data_view/data_view_item_model.py b/pyface/ui/qt4/data_view/data_view_item_model.py index 5e762c7a3..887572287 100644 --- a/pyface/ui/qt4/data_view/data_view_item_model.py +++ b/pyface/ui/qt4/data_view/data_view_item_model.py @@ -13,7 +13,7 @@ from pyface.qt import is_qt5 from pyface.qt.QtCore import QAbstractItemModel, QModelIndex, Qt from pyface.data_view.index_manager import Root -from pyface.data_view.abstract_data_model import AbstractDataModel +from pyface.data_view.abstract_data_model import AbstractDataModel, DataViewSetError logger = logging.getLogger(__name__) @@ -174,14 +174,26 @@ def setData(self, index, value, role=Qt.EditRole): if not value_type: return False - if role == Qt.EditRole: - if value_type.has_editor_value(self.model, row, column): - return value_type.set_editor_value(self.model, row, column, value) - elif role == Qt.DisplayRole: - if value_type.has_text(self.model, row, column): - return value_type.set_text(self.model, row, column, value) - - return False + try: + if role == Qt.EditRole: + if value_type.has_editor_value(self.model, row, column): + value_type.set_editor_value(self.model, row, column, value) + elif role == Qt.DisplayRole: + if value_type.has_text(self.model, row, column): + value_type.set_text(self.model, row, column, value) + except DataViewSetError: + return False + except Exception: + # unexpected error, log and persevere + logger.exception( + "setData failed: row %r, column %r, value %r", + row, + column, + value, + ) + return False + else: + return True def headerData(self, section, orientation, role=Qt.DisplayRole): if orientation == Qt.Horizontal: diff --git a/pyface/ui/wx/data_view/data_view_model.py b/pyface/ui/wx/data_view/data_view_model.py index f25dc140f..e7c4538c4 100644 --- a/pyface/ui/wx/data_view/data_view_model.py +++ b/pyface/ui/wx/data_view/data_view_model.py @@ -1,9 +1,23 @@ - - +# (C) Copyright 2005-2020 Enthought, Inc., Austin, TX +# All rights reserved. +# +# This software is provided without warranty under the terms of the BSD +# license included in LICENSE.txt and may be redistributed only under +# the conditions described in the aforementioned license. The license +# is also available online at http://www.enthought.com/licenses/BSD.txt +# +# Thanks for using Enthought open source! + +import logging + +from pyface.data_view.abstract_data_model import DataViewSetError from pyface.data_view.index_manager import Root from wx.dataview import DataViewItem, DataViewModel as wxDataViewModel +logger = logging.getLogger(__name__) + + type_hint_to_variant = { 'str': "string", 'int': "longlong", @@ -139,12 +153,20 @@ def SetValue(self, value, item, column): else: column_index = (column - 1,) try: - result = self.model.set_text(row_index, column_index, value) - except Exception as exc: - print(exc) - # XXX log it + value_type = self.model.get_value_type(row_index, column_index) + value_type.set_text(self.model, row_index, column_index, value) + except DataViewSetError: return False - return result + except Exception: + logger.exception( + "SetValue failed: row %r, column %r, value %r", + row_index, + column_index, + value, + ) + return False + else: + return True def GetColumnCount(self): return self.model.get_column_count() + 1 From 4929afe3448f01f1a07e3d2a8689cc49f4dbfe73 Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Tue, 7 Jul 2020 15:47:49 +0100 Subject: [PATCH 33/52] Fix iteration over columns. --- pyface/data_view/abstract_data_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyface/data_view/abstract_data_model.py b/pyface/data_view/abstract_data_model.py index 0e751eeba..8f697a725 100644 --- a/pyface/data_view/abstract_data_model.py +++ b/pyface/data_view/abstract_data_model.py @@ -268,5 +268,5 @@ def iter_items(self, start_row=()): """ for row in self.iter_rows(start_row): yield row, () - for column in range(self.get_column_count(row)): + for column in range(self.get_column_count()): yield row, (column,) From 4b0efee3fbcb6c015f4a84a967c91b65a4e39db0 Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Tue, 7 Jul 2020 15:59:10 +0100 Subject: [PATCH 34/52] Use more descriptive metadata for updating value types. --- pyface/data_view/abstract_value_type.py | 4 ++-- pyface/data_view/tests/test_abstract_value_type.py | 2 +- pyface/data_view/value_types/constant_value.py | 2 +- pyface/data_view/value_types/editable_value.py | 2 +- pyface/data_view/value_types/numeric_value.py | 2 +- pyface/data_view/value_types/text_value.py | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pyface/data_view/abstract_value_type.py b/pyface/data_view/abstract_value_type.py index 8e80e3396..c8d999054 100644 --- a/pyface/data_view/abstract_value_type.py +++ b/pyface/data_view/abstract_value_type.py @@ -30,7 +30,7 @@ class AbstractValueType(ABCHasStrictTraits): display. Subclasses should mark traits that potentially affect the display of values - with ``update=True`` metdadata, or alternatively fire the ``updated`` + with ``update_value_type=True`` metdadata, or alternatively fire the ``updated`` event when the state of the value type changes. Each data channel is set up to have a method which returns whether there @@ -180,7 +180,7 @@ def set_text(self, model, row, column, text): """ return False - @observe('+update') + @observe('+update_value_type') def update_value_type(self, event=None): """ Fire update event when marked traits change. """ self.updated = True diff --git a/pyface/data_view/tests/test_abstract_value_type.py b/pyface/data_view/tests/test_abstract_value_type.py index a4d2afb9d..5e40be28d 100644 --- a/pyface/data_view/tests/test_abstract_value_type.py +++ b/pyface/data_view/tests/test_abstract_value_type.py @@ -21,7 +21,7 @@ class ValueType(AbstractValueType): #: a parameter which should fire the update trait - sample_parameter = Str(update=True) + sample_parameter = Str(update_value_type=True) class TestAbstractValueType(UnittestTools, TestCase): diff --git a/pyface/data_view/value_types/constant_value.py b/pyface/data_view/value_types/constant_value.py index c12436b5b..7b6146a16 100644 --- a/pyface/data_view/value_types/constant_value.py +++ b/pyface/data_view/value_types/constant_value.py @@ -22,7 +22,7 @@ class ConstantValue(AbstractValueType): """ #: The text value to display. - text = Str(update=True) + text = Str(update_value_type=True) def has_editor_value(self, model, row, column): return False diff --git a/pyface/data_view/value_types/editable_value.py b/pyface/data_view/value_types/editable_value.py index cc7596556..73c3ac7a6 100644 --- a/pyface/data_view/value_types/editable_value.py +++ b/pyface/data_view/value_types/editable_value.py @@ -25,7 +25,7 @@ class EditableValue(AbstractValueType): #: Whether or not the value is editable, assuming the underlying data can #: be set. - is_editable = Bool(True, update=True) + is_editable = Bool(True, update_value_type=True) def is_valid(self, model, row, column, value): """ Whether or not the value is valid for the data item specified. diff --git a/pyface/data_view/value_types/numeric_value.py b/pyface/data_view/value_types/numeric_value.py index ac1067095..d1c8e161d 100644 --- a/pyface/data_view/value_types/numeric_value.py +++ b/pyface/data_view/value_types/numeric_value.py @@ -35,7 +35,7 @@ class NumericValue(EditableValue): evaluate = Callable() #: A function that converts the required type to a string for display. - format = Callable(format_locale, update=True) + format = Callable(format_locale, update_value_type=True) #: A function that converts the required type from a display string. unformat = Callable(locale.delocalize) diff --git a/pyface/data_view/value_types/text_value.py b/pyface/data_view/value_types/text_value.py index 92329c9b2..0f42f4c81 100644 --- a/pyface/data_view/value_types/text_value.py +++ b/pyface/data_view/value_types/text_value.py @@ -18,7 +18,7 @@ class TextValue(EditableValue): """ #: A function that converts the value to a string for display. - format = Callable(str, update=True) + format = Callable(str, update_value_type=True) #: A function that converts to a value from a display string. unformat = Callable(str) From 7d12b2a9d8c2501c7b60a08172f7121a40a1ae96 Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Tue, 7 Jul 2020 16:45:55 +0100 Subject: [PATCH 35/52] Don't try to guess value type for array model, ensure 2d arrays. --- examples/data_view/array_example.py | 6 +- .../data_view/data_models/array_data_model.py | 58 ++++++++++++----- .../tests/test_array_data_model.py | 25 ++----- .../data_view/tests/test_data_view_widget.py | 3 +- pyface/data_view/value_types/dtype_value.py | 65 +++++++++++++++++++ 5 files changed, 120 insertions(+), 37 deletions(-) create mode 100644 pyface/data_view/value_types/dtype_value.py diff --git a/examples/data_view/array_example.py b/examples/data_view/array_example.py index 765d3ff38..4d5c368b6 100644 --- a/examples/data_view/array_example.py +++ b/examples/data_view/array_example.py @@ -14,6 +14,7 @@ from pyface.data_view.data_models.array_data_model import ArrayDataModel from pyface.data_view.i_data_view_widget import IDataViewWidget from pyface.data_view.data_view_widget import DataViewWidget +from pyface.data_view.value_types.api import FloatValue class MainWindow(ApplicationWindow): @@ -28,7 +29,10 @@ def _create_contents(self, parent): self.data_view = DataViewWidget( parent=parent, - data_model=ArrayDataModel(data=self.data), + data_model=ArrayDataModel( + data=self.data, + value_type=FloatValue(), + ), ) self.data_view._create() return self.data_view.control diff --git a/pyface/data_view/data_models/array_data_model.py b/pyface/data_view/data_models/array_data_model.py index aca0fbd68..c89d9f152 100644 --- a/pyface/data_view/data_models/array_data_model.py +++ b/pyface/data_view/data_models/array_data_model.py @@ -12,7 +12,9 @@ This module provides a concrete implementation of a data model for an n-dim numpy array. """ -from traits.api import Array, Instance, observe +from collections.abc import Sequence + +from traits.api import Array, HasRequiredTraits, Instance, observe from pyface.data_view.abstract_data_model import AbstractDataModel from pyface.data_view.abstract_value_type import AbstractValueType @@ -22,7 +24,39 @@ from pyface.data_view.index_manager import TupleIndexManager -class ArrayDataModel(AbstractDataModel): +class _AtLeastTwoDArray(Array): + """ Trait type that holds an array that at least two dimensional. + + This calls numpy.atleast_2d during validation to ensure that the value is + good. + """ + + def validate(self, object, name, value): + from numpy import atleast_2d + value = super().validate(object, name, value) + return atleast_2d(value) + + def _default_for_dtype_and_shape(self, dtype, shape): + """ Invent a suitable default value for a given dtype and shape. """ + from numpy import zeros + + if shape is None: + value = zeros((0, 0), dtype) + else: + size = [] + for item in shape: + if item is None: + item = 0 + elif isinstance(item, Sequence): + # Given a (minimum-allowed-length, maximum-allowed_length) + # pair for a particular axis, use the minimum. + item = item[0] + size.append(item) + value = zeros(size, dtype) + return value + + +class ArrayDataModel(AbstractDataModel, HasRequiredTraits): """ A data model for an n-dim array. This data model presents the data from a multidimensional array @@ -41,7 +75,7 @@ class ArrayDataModel(AbstractDataModel): """ #: The array being displayed. This must have dimension at least 2. - data = Array() + data = _AtLeastTwoDArray() #: The index manager that helps convert toolkit indices to data view #: indices. @@ -52,6 +86,7 @@ class ArrayDataModel(AbstractDataModel): AbstractValueType, factory=ConstantValue, kw={'text': "Index"}, + allow_none=False, ) #: The value type of the column titles. @@ -59,6 +94,7 @@ class ArrayDataModel(AbstractDataModel): AbstractValueType, factory=IntValue, kw={'is_editable': False}, + allow_none=False, ) #: The value type of the row titles. @@ -66,10 +102,11 @@ class ArrayDataModel(AbstractDataModel): AbstractValueType, factory=IntValue, kw={'is_editable': False}, + allow_none=False, ) #: The type of value being displayed in the data model. - value_type = Instance(AbstractValueType) + value_type = Instance(AbstractValueType, allow_none=False, required=True) # Data structure methods @@ -86,7 +123,6 @@ def get_column_count(self): """ return self.data.shape[-1] - def can_have_children(self, row): """ Whether or not a row can have child rows. @@ -267,15 +303,3 @@ def label_header_type_updated(self, event): self.values_changed = ( ((), (), (), ()) ) - - def _value_type_default(self): - import numpy as np - scalar_type = self.data.dtype - if np.issubdtype(scalar_type, np.integer): - return IntValue() - elif np.issubdtype(scalar_type, np.floating): - return FloatValue() - elif np.issubdtype(scalar_type, np.character): - return TextValue() - - return TextValue(is_editable=False) diff --git a/pyface/data_view/data_models/tests/test_array_data_model.py b/pyface/data_view/data_models/tests/test_array_data_model.py index 399a0df97..f2a2ea1d0 100644 --- a/pyface/data_view/data_models/tests/test_array_data_model.py +++ b/pyface/data_view/data_models/tests/test_array_data_model.py @@ -26,7 +26,7 @@ class TestArrayDataModel(UnittestTools, TestCase): def setUp(self): super().setUp() self.array = np.arange(30.0).reshape(5, 2, 3) - self.model = ArrayDataModel(data=self.array) + self.model = ArrayDataModel(data=self.array, value_type=FloatValue()) self.values_changed_event = None self.structure_changed_event = None self.model.observe(self.model_values_changed, 'values_changed') @@ -47,6 +47,12 @@ def model_values_changed(self, event): def model_structure_changed(self, event): self.structure_changed_event = event + def test_data_1d(self): + array = np.arange(30.0) + model = ArrayDataModel(data=array, value_type=FloatValue()) + self.assertEqual(model.data.ndim, 2) + self.assertEqual(model.data.shape, (1, 30)) + def test_get_column_count(self): result = self.model.get_column_count() self.assertEqual(result, 3) @@ -314,20 +320,3 @@ def test_iter_items_leaf(self): ((2, 0), (0,)), ((2, 0), (1,)), ((2, 0), (2,)), ] ) - - def test_default_value_type(self): - data = np.arange(15).reshape(5, 3) - model = ArrayDataModel(data=data) - self.assertIsInstance(model.value_type, IntValue) - - data = np.arange(15.0).reshape(5, 3) - model = ArrayDataModel(data=data) - self.assertIsInstance(model.value_type, FloatValue) - - data = np.array([['a', 'b', 'c'], ['e', 'f', 'g']]) - model = ArrayDataModel(data=data) - self.assertIsInstance(model.value_type, TextValue) - - data = np.array([['a', 'b', 'c'], ['e', 'f', 'g']], dtype=object) - model = ArrayDataModel(data=data) - self.assertIsInstance(model.value_type, TextValue) diff --git a/pyface/data_view/tests/test_data_view_widget.py b/pyface/data_view/tests/test_data_view_widget.py index 0c968f1b0..c3487e232 100644 --- a/pyface/data_view/tests/test_data_view_widget.py +++ b/pyface/data_view/tests/test_data_view_widget.py @@ -19,6 +19,7 @@ from pyface.data_view.data_models.api import ArrayDataModel from pyface.data_view.data_view_widget import DataViewWidget +from pyface.data_view.value_types.api import FloatValue @requires_numpy @@ -38,7 +39,7 @@ def setUp(self): def _create_widget(self): self.data = np.arange(30.0).reshape(5, 6) - self.model = ArrayDataModel(data=self.data) + self.model = ArrayDataModel(data=self.data, value_type=FloatValue()) return DataViewWidget( parent=self.parent.control, data_model=self.model diff --git a/pyface/data_view/value_types/dtype_value.py b/pyface/data_view/value_types/dtype_value.py new file mode 100644 index 000000000..d21ea0d0f --- /dev/null +++ b/pyface/data_view/value_types/dtype_value.py @@ -0,0 +1,65 @@ +# (C) Copyright 2005-2020 Enthought, Inc., Austin, TX +# All rights reserved. +# +# This software is provided without warranty under the terms of the BSD +# license included in LICENSE.txt and may be redistributed only under +# the conditions described in the aforementioned license. The license +# is also available online at http://www.enthought.com/licenses/BSD.txt +# +# Thanks for using Enthought open source! + +from traits.api import Callable + +from .editable_value import EditableValue + + +class DtypeValue(EditableValue): + """ Editable value that presents value depending on numpy dtype. + """ + + #: A function that converts the value to a string for display. + format = Callable(str, update_value_type=True) + + #: A function that converts to a value from a display string. + unformat = Callable(str) + + def get_text(self, model, row, column): + """ Get the display text from the underlying value. + + Parameters + ---------- + model : AbstractDataModel + The data model holding the data. + row : sequence of int + The row in the data model being queried. + column : sequence of int + The column in the data model being queried. + + Returns + ------- + text : str + The text to display. + """ + return self.format(model.get_value(row, column)) + + def set_text(self, model, row, column, text): + """ Set the text of the underlying value. + + Parameters + ---------- + model : AbstractDataModel + The data model holding the data. + row : sequence of int + The row in the data model being queried. + column : sequence of int + The column in the data model being queried. + text : str + The text to set. + + Raises + ------- + DataViewSetError + If the value cannot be set. + """ + value = self.unformat(text) + self.set_editor_value(model, row, column, value) From 262bc5b60893269de3c623d17158aa42a33693ad Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Tue, 7 Jul 2020 16:47:38 +0100 Subject: [PATCH 36/52] Add a test for setting 1d data. --- pyface/data_view/data_models/tests/test_array_data_model.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pyface/data_view/data_models/tests/test_array_data_model.py b/pyface/data_view/data_models/tests/test_array_data_model.py index f2a2ea1d0..6b7a6b788 100644 --- a/pyface/data_view/data_models/tests/test_array_data_model.py +++ b/pyface/data_view/data_models/tests/test_array_data_model.py @@ -53,6 +53,11 @@ def test_data_1d(self): self.assertEqual(model.data.ndim, 2) self.assertEqual(model.data.shape, (1, 30)) + def test_set_data_1d(self): + self.model.data = np.arange(30.0) + self.assertEqual(self.model.data.ndim, 2) + self.assertEqual(self.model.data.shape, (1, 30)) + def test_get_column_count(self): result = self.model.get_column_count() self.assertEqual(result, 3) From fde28c205218dcfb14803b5519feafb13d8489c8 Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Tue, 7 Jul 2020 16:49:49 +0100 Subject: [PATCH 37/52] And test default data is 2d. --- pyface/data_view/data_models/tests/test_array_data_model.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pyface/data_view/data_models/tests/test_array_data_model.py b/pyface/data_view/data_models/tests/test_array_data_model.py index 6b7a6b788..a438a4d9e 100644 --- a/pyface/data_view/data_models/tests/test_array_data_model.py +++ b/pyface/data_view/data_models/tests/test_array_data_model.py @@ -47,6 +47,11 @@ def model_values_changed(self, event): def model_structure_changed(self, event): self.structure_changed_event = event + def test_no_data(self): + model = ArrayDataModel(value_type=FloatValue()) + self.assertEqual(model.data.ndim, 2) + self.assertEqual(model.data.shape, (0, 0)) + def test_data_1d(self): array = np.arange(30.0) model = ArrayDataModel(data=array, value_type=FloatValue()) From 62a38c709b378bf42fc191799fea9a45e577ff8c Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Tue, 7 Jul 2020 16:51:51 +0100 Subject: [PATCH 38/52] Improve tests of empty data. --- pyface/data_view/data_models/tests/test_array_data_model.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyface/data_view/data_models/tests/test_array_data_model.py b/pyface/data_view/data_models/tests/test_array_data_model.py index a438a4d9e..c18d36275 100644 --- a/pyface/data_view/data_models/tests/test_array_data_model.py +++ b/pyface/data_view/data_models/tests/test_array_data_model.py @@ -51,6 +51,9 @@ def test_no_data(self): model = ArrayDataModel(value_type=FloatValue()) self.assertEqual(model.data.ndim, 2) self.assertEqual(model.data.shape, (0, 0)) + self.assertEqual(model.get_column_count(), 0) + self.assertTrue(model.can_have_children(())) + self.assertEqual(model.get_row_count(()), 0) def test_data_1d(self): array = np.arange(30.0) From 2aa4c11e00ea1addfbd9a110274ba5a50a22bab2 Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Tue, 7 Jul 2020 17:22:10 +0100 Subject: [PATCH 39/52] Assorted fixes from PR review. --- pyface/data_view/index_manager.py | 10 +++++----- pyface/ui/qt4/data_view/data_view_item_model.py | 12 +++++++----- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/pyface/data_view/index_manager.py b/pyface/data_view/index_manager.py index edefed59d..c17a6c506 100644 --- a/pyface/data_view/index_manager.py +++ b/pyface/data_view/index_manager.py @@ -39,7 +39,7 @@ IntIndexManager An efficient index manager for non-hierarchical data, such as - lists and arrays. + lists, tables and 2D arrays. TupleIndexManager An index manager that handles non-hierarchical data while trying @@ -193,13 +193,13 @@ def id(self, index): Parameters ---------- - id : int - An integer object id value. + index : index object + The persistent index object associated with this id. Returns ------- - index : index object - The persistent index object associated with this id. + id : int + An integer object id value. """ raise NotImplementedError() diff --git a/pyface/ui/qt4/data_view/data_view_item_model.py b/pyface/ui/qt4/data_view/data_view_item_model.py index 887572287..0df52bfe2 100644 --- a/pyface/ui/qt4/data_view/data_view_item_model.py +++ b/pyface/ui/qt4/data_view/data_view_item_model.py @@ -75,7 +75,7 @@ def on_values_changed(self, event): top, left, bottom, right = event.new if top == () and bottom == (): # this is a column header change - self.headerDataChanged.emit(left[0], right[0]) + self.headerDataChanged.emit(Qt.Horizontal, left[0], right[0]) elif left == () and right == (): # this is a row header change # XXX this is currently not supported and not needed @@ -114,7 +114,7 @@ def index(self, row, column, parent): index = self.createIndex(row, column, parent_index) return index - def rowCount(self, index): + def rowCount(self, index=QModelIndex()): row_index = self._to_row_index(index) try: if self.model.can_have_children(row_index): @@ -124,23 +124,25 @@ def rowCount(self, index): return 0 - def columnCount(self, index): + def columnCount(self, index=QModelIndex()): row_index = self._to_row_index(index) try: # the number of columns is constant; leaf rows return 0 if self.model.can_have_children(row_index): return self.model.get_column_count() + 1 - else: - return 0 except Exception: logger.exception("Error in columnCount") + return 0 + # Data methods def flags(self, index): row = self._to_row_index(index) column = self._to_column_index(index) value_type = self.model.get_value_type(row, column) + if row == () and column == (): + return 0 flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable if is_qt5 and not self.model.can_have_children(row): From 25009ef31bb75d1742ed917dff2f3d8fd1ed461d Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Tue, 7 Jul 2020 18:01:25 +0100 Subject: [PATCH 40/52] Dispatch model changes on the ui thread. --- pyface/ui/qt4/data_view/data_view_item_model.py | 4 ++++ pyface/ui/wx/data_view/data_view_model.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/pyface/ui/qt4/data_view/data_view_item_model.py b/pyface/ui/qt4/data_view/data_view_item_model.py index 0df52bfe2..9b0f142e7 100644 --- a/pyface/ui/qt4/data_view/data_view_item_model.py +++ b/pyface/ui/qt4/data_view/data_view_item_model.py @@ -40,11 +40,13 @@ def model(self, model: AbstractDataModel): self._model.observe( self.on_structure_changed, 'structure_changed', + dispatch='ui', remove=True, ) self._model.observe( self.on_values_changed, 'values_changed', + dispatch='ui', remove=True, ) @@ -59,10 +61,12 @@ def model(self, model: AbstractDataModel): self._model.observe( self.on_structure_changed, 'structure_changed', + dispatch='ui', ) self._model.observe( self.on_values_changed, 'values_changed', + dispatch='ui', ) # model event listeners diff --git a/pyface/ui/wx/data_view/data_view_model.py b/pyface/ui/wx/data_view/data_view_model.py index e7c4538c4..bc9d55f1e 100644 --- a/pyface/ui/wx/data_view/data_view_model.py +++ b/pyface/ui/wx/data_view/data_view_model.py @@ -49,11 +49,13 @@ def model(self, model): self._model.observe( self.on_structure_changed, 'structure_changed', + dispatch='ui', remove=True, ) self._model.observe( self.on_values_changed, 'values_changed', + dispatch='ui', remove=True, ) self._model = model @@ -65,10 +67,12 @@ def model(self, model): self._model.observe( self.on_structure_changed, 'structure_changed', + dispatch='ui', ) self._model.observe( self.on_values_changed, 'values_changed', + dispatch='ui', ) def on_structure_changed(self, event): From e3d625f1c9846797fe23b845fb6d929108e9dc2e Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Tue, 7 Jul 2020 18:02:04 +0100 Subject: [PATCH 41/52] Cleanup of confusing parentheses. --- .../data_view/data_models/array_data_model.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/pyface/data_view/data_models/array_data_model.py b/pyface/data_view/data_models/array_data_model.py index c89d9f152..ee475f8ae 100644 --- a/pyface/data_view/data_models/array_data_model.py +++ b/pyface/data_view/data_models/array_data_model.py @@ -269,9 +269,8 @@ def data_updated(self, event): """ Handle the array being replaced with a new array. """ if event.new.shape == event.old.shape: self.values_changed = ( - ( (0,), (0,), - (event.old.shape[0] - 1,), (event.old.shape[-1] - 1,)) + (event.old.shape[0] - 1,), (event.old.shape[-1] - 1,) ) else: self.structure_changed = True @@ -280,26 +279,20 @@ def data_updated(self, event): def value_type_updated(self, event): """ Handle the value type being updated. """ self.values_changed = ( - ((0,), (0,), (self.data.shape[0] - 1,), (self.data.shape[-1] - 1,)) + (0,), (0,), (self.data.shape[0] - 1,), (self.data.shape[-1] - 1,) ) @observe('column_header_type.updated', dispatch='ui') def column_header_type_updated(self, event): """ Handle the column header type being updated. """ - self.values_changed = ( - ((), (0,), (), (self.data.shape[-1] - 1,)) - ) + self.values_changed = ((), (0,), (), (self.data.shape[-1] - 1,)) @observe('row_header_type.updated', dispatch='ui') def value_header_type_updated(self, event): """ Handle the value header type being updated. """ - self.values_changed = ( - ((0,), (), (self.data.shape[0] - 1,), ()) - ) + self.values_changed = ((0,), (), (self.data.shape[0] - 1,), ()) @observe('label_header_type.updated', dispatch='ui') def label_header_type_updated(self, event): """ Handle the label header type being updated. """ - self.values_changed = ( - ((), (), (), ()) - ) + self.values_changed = ((), (), (), ()) From bb5c38040ddd29f5e2e0be254bcc2c9713675130 Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Wed, 8 Jul 2020 13:28:23 +0100 Subject: [PATCH 42/52] Improve documentation for data view. --- docs/source/data_view.rst | 306 +++++++++++++-------- docs/source/examples/data_model_indices.py | 78 ++++++ docs/source/examples/dict_data_model.py | 156 +++++++++++ docs/source/images/data_view_indices.png | Bin 0 -> 206956 bytes 4 files changed, 420 insertions(+), 120 deletions(-) create mode 100644 docs/source/examples/data_model_indices.py create mode 100644 docs/source/examples/dict_data_model.py create mode 100644 docs/source/images/data_view_indices.png diff --git a/docs/source/data_view.rst b/docs/source/data_view.rst index 382428bd5..39fcbb352 100644 --- a/docs/source/data_view.rst +++ b/docs/source/data_view.rst @@ -4,6 +4,49 @@ Pyface DataViews The Pyface DataView API allows visualization of hierarchical and non-hierarchical tabular data. +Indexing +-------- + +The DataView API has a consistent way of indexing that uses tuples of integers +to represent the rows and columns, as illustrated below: + +.. figure:: images/data_view_indices.png + :alt: an illustration of data view indices + + How DataView Indexing Works. + +A row index corresponds to a list of integer indexes at each level of the +heirarchy, so the empty tuple ``()`` represents the root of the heirarchy, +the tuples ``(0,)`` and ``(1,)`` give the two child rows of the root, while +``(0, 1)`` is the second child row of the first child of the root, and so on. + +Column indices follow a similar pattern, but only have the root and one level +if child indices. + +When interpreting these values, the root row ``()`` corresponds to the +*column* headers, the root column ``()`` corresponds to the *row* headers. +The root row and column indices together refer to the cell in the top-left +corner. + + +Index Managers +-------------- + +These indices need to be converted to and from whatever system the backend +toolkit uses for indexing and tracking rows. This conversion is handled +by an |AbstractIndexManager| instance. Pyface provides two of these which +efficiently handle the two common cases: |TupleIndexManager| is designed to +handle general heirarchical models, but needs to cache mementos for all rows +with children (and on Wx, for all rows); the |IntIndexManager| can only handle +non-hierarchical tables, but does so without needing any additional memory +allocation. + +Unless you are creating a toolkit model or widget that uses the DataView +infrastructure it is sufficient to simply know to use the |IntIndexManager| +when you know that the data will always be a flat table, and |TupleIndexManager| +otherwise. + + Data Models ----------- @@ -11,149 +54,172 @@ Data to be viewed needs to be exposed to the DataView infrastructure by creating a data model for it. This is a class that implements the interface of |AbstractDataModel|. -A data model for a dictionary could be implemented like this:: +A data model for a dictionary could be implemented like this: - class DictDataModel(AbstractDataModel): +.. include:: examples/dict_data_model.py + :code: python + :start-line: 19 + :end-line: 27 - data = Dict - - index_manager = Instance(IntIndexManager, ()) - -The index manager is an ``IntIndexManager`` because the data is -non-hierarchical and this is more memory-efficient than the alternative -``TupleIndexManager``. +The base |AbstractDataModel| class requires you to provide an index manager +so we use an |IntIndexManager| because the data is always non-hierarchical +for this model. Data Structure ~~~~~~~~~~~~~~ -The keys are displayed in the row headers, so each row has one column -displaying the value:: +The |get_column_count| method needs to be implemented to tell the toolkit +how many columns are in the data model. For the dict model, keys are +displayed in the row headers, so there is just one column displaying the +value: - def get_column_count(self): - return 1 +.. include:: examples/dict_data_model.py + :code: python + :start-line: 44 + :end-line: 45 -The data is non-hierarchical, so the root has children and no other -rows have children:: +We can signal to the toolkit that certain rows can never have children +via the |can_have_children| method. The dict data model is +non-hierarchical, so the root has children but no other rows will ever +have children: - def can_have_children(self, row): - if len(row) == 0: - return True - return False +.. include:: examples/dict_data_model.py + :code: python + :start-line: 47 + :end-line: 48 -The number of child rows of the root is the length of the dictionary:: +We need to tell the toolkit how many child rows a particular row has, +which is done via the |get_row_count| method. In this example, only the +root has children, and the number of child rows of the root is the length +of the dictionary: - def get_row_count(self, row): - if len(row) == 0: - return len(self.data) - return 0 +.. include:: examples/dict_data_model.py + :code: python + :start-line: 50 + :end-line: 53 Data Values ~~~~~~~~~~~ -To get the values of the data model, we need to find the apprpriate value -from the dictionary:: - - keys_header = Str("Keys") - values_header = Str("Values") - - def get_value(self, row, column): - if len(row) == 0: - # this is a column header - if len(column) == 0: - # title of the row headers - return self.keys_header - else: - return self.values_header - else: - row_index = row[0] - key, value = list(self.data.items())[row_index] - if len(column) == 0: - # the is a row header, so get the key - return key - else: - return value - -In this case, all of the values are text, and read-only, so we can have a -trait holding the value type, and return that for every item:: - - header_value_type = Instance(AbstractValueType) - key_value_type = Instance(AbstractValueType) - value_type = Instance(AbstractValueType) - - def _default_header_value_type(self): - return TextValue(is_editable=False) - - def _default_key_value_type(self): - return TextValue(is_editable=False) - - def _default_value_type(self): - return TextValue(is_editable=False) - - def get_value_type(self, row, column): - if len(row) == 0: - return self.header_value_type - elif len(column) == 0: - return self.key_value_type - else: - return self.value_type - -The default assumes that values representable as text, but if the values were -ints, for example then the class could be instantiated with:: - - model = DictDataModel(value_type=IntValue(is_editable=False)) - -The ``is_editable`` arguments are not strictly needed, as the default -implementation of |can_set_value| returns False, but if we wanted to make the -data model read-write we would need to write an implementation of -|can_set_value| which returns True for editable items, and an implementation -of |set_value| that updates the data in-place. This would look something like:: - - def can_set_value(self, row, column): - return len(row) != 0 and len(column) != 0: - - def set_value(self, row, column, value): - if self.can_set_value(row, column): - row_index = row[0] - key = list(self.data)[row_index] - self.data[key] = value - return True - return False - -Update Events -------------- - -Finally, when the underlying data changes, the DataView infrastructure expects -certain event traits to be fired. If a value is changed, or the value type is +The |get_value| method is used to return the raw value for each location. +To get the values of the dict data model, we need to determine from the row +and column index whether or not the cell is a column header and whether +it corresponds to the keys or the values. The code looks like this: + +.. include:: examples/dict_data_model.py + :code: python + :start-line: 55 + :end-line: 70 + +Conversion of values into data channels is done by providing a value type +for each cell. The |get_value_type| method provides an appropriate data +type for each item in the table. For this data model we have three value +types: the column headers, the keys and the values. + +.. include:: examples/dict_data_model.py + :code: python + :start-line: 35 + :end-line: 42 + +The default values of these traits are defined to be |TextValue| instances. +Users of the model can provide different value types when instantiating, +for example if the values are known to all be integers then |IntValue| +could be used instead for the ``value_type`` trait:: + + model = DictDataModel(value_type=IntValue()) + +The |get_value_type| method uses the indices to select the appropriate +value types: + +.. include:: examples/dict_data_model.py + :code: python + :start-line: 72 + :end-line: 78 + +Handling Updates +~~~~~~~~~~~~~~~~ + +The |AbstractDataModel| class expects that when the data changes, one of +two trait Events are fired. If a value is changed, or the value type is updated, but the number of rows and columns is unaffected, then the -``values_changed`` trait should be fired with a tuple of ``(start_row_index, -start_column_index, end_row_index, end_column_index)``. If a major change has -occurred, or if the size, shape or layout of the data has changed, then -the ``structure_changed`` event should be fired with a simple ``True`` value. +``values_changed`` trait should be fired with a tuple:: + + (start_row_index, start_column_index, end_row_index, end_column_index) -So for example, if the value types change, only the displayed values need to be -updated:: +If a major change has occurred, or if the size, shape or layout of the data +has changed, then the ``structure_changed`` event should be fired with a +simple ``True`` value. + +While it is possible that a data model could require users of the model to +manually fire these events (and for some opaque, non-traits data structures, +this may be necessary), where possible it makes sense to use trait observers +to automatically fire these events when a change occurs. + +For example, we want to listen for changes in the dictionary and its items. +It is simplest in this case to just indicate that the entire model needs +updating by firing the ``structure_changed`` event [#]_: + +.. include:: examples/dict_data_model.py + :code: python + :start-line: 103 + :end-line: 106 + +Changes to the value types also should fire update events, but usually +these are simply changes to the data, rather than changes to the structure +of the table. All value types have an updated event which is fired when +any state of the type changes. We can observe these, compute which +indices are affected, and fire the appropriate event. + +.. include:: examples/dict_data_model.py + :code: python + :start-line: 91 + :end-line: 101 + +Editing Values +~~~~~~~~~~~~~~ - @observe('header_value_type.updated') - def header_values_updated(self, event): - self.values_changed = ([], [], [], [0]) +A model can flag values as being modifiable by implementing the +|can_set_value| function. The default implementation simply returns +``False`` for all items, but subclasses can override this to permit +modification of the values. For example, to allow modification of the +values of the dictionary, we could write: - @observe('key_value_type.updated') - def key_values_updated(self, event): - self.values_changed = ([0], [], [len(self.data) - 1], []) +.. include:: examples/dict_data_model.py + :code: python + :start-line: 80 + :end-line: 81 - @observe('value_type.updated') - def values_updated(self, event): - self.values_changed = ([0], [0], [len(self.data) - 1], [0]) +A corresponding |set_value| method is needed to actually perform the changes +to the underlying values. If for some reason it is impossible to set the +value (eg. an invalid value is supplied, or |set_value| is called with an +inappropriate row or column value, then a |DataViewSetError| should be +raised: -On the other hand, if the dictionary or its items change, then it is simplest -to just indicate that the entire view needs updating:: +.. include:: examples/dict_data_model.py + :code: python + :start-line: 82 + :end-line: 89 - @observe('data.items') - def data_updated(self, event): - self.structure_changed = True +.. rubric:: Footnotes +.. [#] A more sophisticated implementation might try to work out + whether the total number of items has changed, and if not, the + location of the first and last changes in at least some of the + change events, and then fire ``values_changed``. For simplicty + we don't try to do that in this example. +.. |AbstractIndexManager| replace:: :py:class:`~pyface.data_view.index_manager.AbstractIndexManager` .. |AbstractDataModel| replace:: :py:class:`~pyface.data_view.abstract_data_model.AbstractDataModel` -.. |can_set_value| replace:: :py:class:`~pyface.data_view.abstract_data_model.AbstractDataModel.can_set_value` -.. |set_value| replace:: :py:class:`~pyface.data_view.abstract_data_model.AbstractDataModel.set_value` +.. |DataViewSetError| replace:: :py:class:`~pyface.data_view.abstract_data_model.DataViewSetError` +.. |IntIndexManager| replace:: :py:class:`~pyface.data_view.index_manager.IntIndexManager` +.. |IntValue| replace:: :py:class:`~pyface.data_view.value_types.numeric_value.IntValue` +.. |TextValue| replace:: :py:class:`~pyface.data_view.value_types.text_value.TextValue` +.. |TupleIndexManager| replace:: :py:class:`~pyface.data_view.index_manager.TupleIndexManager` +.. |can_have_children| replace:: :py:meth:`~pyface.data_view.abstract_data_model.AbstractDataModel.can_have_children` +.. |can_set_value| replace:: :py:meth:`~pyface.data_view.abstract_data_model.AbstractDataModel.can_set_value` +.. |get_column_count| replace:: :py:meth:`~pyface.data_view.abstract_data_model.AbstractDataModel.get_column_count` +.. |get_row_count| replace:: :py:meth:`~pyface.data_view.abstract_data_model.AbstractDataModel.get_row_count` +.. |get_value| replace:: :py:meth:`~pyface.data_view.abstract_data_model.AbstractDataModel.get_value` +.. |get_value_type| replace:: :py:meth:`~pyface.data_view.abstract_data_model.AbstractDataModel.get_value` +.. |set_value| replace:: :py:meth:`~pyface.data_view.abstract_data_model.AbstractDataModel.set_value` diff --git a/docs/source/examples/data_model_indices.py b/docs/source/examples/data_model_indices.py new file mode 100644 index 000000000..29a1b1211 --- /dev/null +++ b/docs/source/examples/data_model_indices.py @@ -0,0 +1,78 @@ +# (C) Copyright 2005-2020 Enthought, Inc., Austin, TX +# All rights reserved. +# +# This software is provided without warranty under the terms of the BSD +# license included in LICENSE.txt and may be redistributed only under +# the conditions described in the aforementioned license. The license +# is also available online at http://www.enthought.com/licenses/BSD.txt +# +# Thanks for using Enthought open source! + +from traits.api import Instance, Int, List + +from pyface.data_view.abstract_data_model import AbstractDataModel +from pyface.data_view.index_manager import TupleIndexManager + + +class IndexDataModel(AbstractDataModel): + """ A data model that displays the indices of the cell. """ + + index_manager = Instance(TupleIndexManager, ()) + + shape = List(Int, [2, 3, 4]) + + def get_column_count(self): + return self.shape[-1] + + def can_have_children(self, row): + return len(row) < len(self.shape) - 1 + + def get_row_count(self, row): + if len(row) == len(self.shape) - 1: + return 0 + else: + return self.shape[len(row)] + + def get_value(self, row, column): + return "{} {}".format(row, column) + + def get_value_type(self, row, column): + return TextValue(is_editable=False) + + +if __name__ == '__main__': + from pyface.api import ApplicationWindow, GUI + from pyface.data_view.i_data_view_widget import IDataViewWidget + from pyface.data_view.data_view_widget import DataViewWidget + from pyface.data_view.value_types.api import TextValue + + + class MainWindow(ApplicationWindow): + """ The main application window. """ + + data_view = Instance(IDataViewWidget) + + def _create_contents(self, parent): + """ Creates the left hand side or top depending on the style. """ + + self.data_view = DataViewWidget( + parent=parent, + data_model=IndexDataModel(), + ) + self.data_view._create() + return self.data_view.control + + def destroy(self): + self.data_view.destroy() + super().destroy() + + + # Create the GUI (this does NOT start the GUI event loop). + gui = GUI() + + # Create and open the main window. + window = MainWindow() + window.open() + + # Start the GUI event loop! + gui.start_event_loop() diff --git a/docs/source/examples/dict_data_model.py b/docs/source/examples/dict_data_model.py new file mode 100644 index 000000000..66f3019fa --- /dev/null +++ b/docs/source/examples/dict_data_model.py @@ -0,0 +1,156 @@ +# (C) Copyright 2005-2020 Enthought, Inc., Austin, TX +# All rights reserved. +# +# This software is provided without warranty under the terms of the BSD +# license included in LICENSE.txt and may be redistributed only under +# the conditions described in the aforementioned license. The license +# is also available online at http://www.enthought.com/licenses/BSD.txt +# +# Thanks for using Enthought open source! + +from traits.api import ComparisonMode, Dict, Instance, Str, observe + +from pyface.data_view.abstract_data_model import ( + AbstractDataModel, DataViewSetError +) +from pyface.data_view.abstract_value_type import AbstractValueType +from pyface.data_view.index_manager import IntIndexManager + + +class DictDataModel(AbstractDataModel): + """ A data model that provides data from a dictionary. """ + + #: The dictionary containing the data. + data = Dict(comparison_mode=ComparisonMode.identity) + + #: The index manager. Because the data is flat, we use the + #: IntIndexManager. + index_manager = Instance(IntIndexManager, ()) + + #: The text to display in the key column header. + keys_header = Str("Keys") + + #: The text to display in the values column header. + values_header = Str("Values") + + #: The header data channels. + header_value_type = Instance(AbstractValueType) + + #: The key column data channels. + key_value_type = Instance(AbstractValueType) + + #: The value column data channels. + value_type = Instance(AbstractValueType) + + def get_column_count(self): + return 1 + + def can_have_children(self, row): + return len(row) == 0 + + def get_row_count(self, row): + if len(row) == 0: + return len(self.data) + return 0 + + def get_value(self, row, column): + if len(row) == 0: + # this is a column header + if len(column) == 0: + # title of the row headers + return self.keys_header + else: + return self.values_header + else: + row_index = row[0] + key, value = list(self.data.items())[row_index] + if len(column) == 0: + # the is a row header, so get the key + return key + else: + return value + + def get_value_type(self, row, column): + if len(row) == 0: + return self.header_value_type + elif len(column) == 0: + return self.key_value_type + else: + return self.value_type + + def can_set_value(self, row, column): + return len(row) != 0 and len(column) != 0 + + def set_value(self, row, column, value): + if self.can_set_value(row, column): + row_index = row[0] + key = list(self.data)[row_index] + self.data[key] = value + else: + raise DataViewSetError() + + @observe('header_value_type.updated') + def header_values_updated(self, event): + self.values_changed = ([], [], [], [0]) + + @observe('key_value_type.updated') + def key_values_updated(self, event): + self.values_changed = ([0], [], [len(self.data) - 1], []) + + @observe('value_type.updated') + def values_updated(self, event): + self.values_changed = ([0], [0], [len(self.data) - 1], [0]) + + @observe('data.items') + def data_updated(self, event): + self.structure_changed = True + + def _header_value_type_default(self): + return TextValue(is_editable=False) + + def _key_value_type_default(self): + return TextValue(is_editable=False) + + def _value_type_default(self): + return TextValue(is_editable=False) + + +if __name__ == '__main__': + from pyface.api import ApplicationWindow, GUI + from pyface.data_view.i_data_view_widget import IDataViewWidget + from pyface.data_view.data_view_widget import DataViewWidget + from pyface.data_view.value_types.api import IntValue, TextValue + + + class MainWindow(ApplicationWindow): + """ The main application window. """ + + data_view = Instance(IDataViewWidget) + + def _create_contents(self, parent): + """ Creates the left hand side or top depending on the style. """ + + self.data_view = DataViewWidget( + parent=parent, + data_model=DictDataModel( + data={'one': 1, 'two': 2, 'three': 3}, + value_type=IntValue(), + ), + ) + self.data_view._create() + return self.data_view.control + + def destroy(self): + self.data_view.destroy() + super().destroy() + + + # Create the GUI (this does NOT start the GUI event loop). + gui = GUI() + + # Create and open the main window. + window = MainWindow() + window.open() + + # Start the GUI event loop! + gui.start_event_loop() diff --git a/docs/source/images/data_view_indices.png b/docs/source/images/data_view_indices.png new file mode 100644 index 0000000000000000000000000000000000000000..b0b6ca95d7c5406e9d25c6561f47651e0f66d830 GIT binary patch literal 206956 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�xLMe^|l1WauIQTtjb+lv?2Ti3XgpJXsgh60>a~10LmBV}F>%SZI za#zs%^^#Tx;xGDyA@}gmj?~_-qW53WlYB(|@nK=Lw?F}h6=iypASaPK-sh;J4Ai+6 z3>{F$IXE1LHch*z95&x=6yC@`YNhkr#2#<5(GVT`_HqfJXQi8sLtW#MCV7_Xi=#AQjMubL6h zyi5g2kX9E8)DsG|H-y6%Yu;33+_BGTNnoqXc|^C~6a8&(TBsD@VNk~{k|D1tN1OT^^&Y1qR`?s-4HRuvJQK>BA}Ud8rporxn3+fM`I+#eyU@_U$o7o;!Nx zT`+wVe0xQtJe`!w|A=8~+kwzpP-hAi4DdigGVfsmDl*F7jvhiX%1_ca)5FcQ*iASa zTVKE?$kxbUZ_(*wyGCM+%-}2&_fM`c$`=?ECkNf#qOq}sX|Z9-kPyNi6~HE-pc_Sy zqK_=HBC9c2kAPy|7Sj6DLN~^q!FdB>$(SonQFY-A((lozHy$(jhzzbTonILRiQ;aC zMnlDf`xh%ggGrUZgt-_o=&i4aWPq-=s^YF48d0bZ^Vn{S)_rwRb%P0*GynRLFx2SF zUq9qXTmcrCoiM6vuOize)7)?d=OV?}9 zlP6y(?*%2JeJEB<$+>TJ5uwjKQIl~0NWmiQFr+`tMeSEEWK~_&-0y16!^Fz%uIWC= z20#x)?FnAT!Z~c{uc3RJ*-6MF_(F{V#K;;4CbW%K3iiEHa_hB7Wp?6j!Eh&Z{sCilQc|_QJ=;HPyRWz>t zA1%sG?w>9&>S<%eQ84R?AnUdA8woz`q}7CycnW=}5hsw;0!7u>Alv8lWv~Bp&ShO5 zOj~`b(#GE48DC9bq~o?htS3DMUXRa>qkhGEU#M}L` zSDG5P=u&Tl_;pDz!OwlWC#qIc^%r8h*Qn;LuHtoxDq++H_gT4UkMEMR@4X*I zfgHwW_SbRKK6d)PZ)o5B#K3xYH!_c$a~J`IWCA=WmlHdu%zm?LH<0k|@c@a6K3Jqu z!;V49?Cw%gR|MDwl^s3&K--__(r))`a1?bO(zPyJJC5^;>NqS#g| zD2DzL8mW!PO((VI)`QKy-N~BZ0SC=mvhS?+9^7V{r5KOrB!&&PtVKdcR9$+ECBhejliBI)Fup8G3QGp#MgnXh13B9 zozh8P??Yda&K_cN%+-X-oa_qvyl$}^^WA`SJrKtm*UI &$d^_K?+FD_&04`alz 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 z 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%0DWfd3!iwXmiam_e2NjMmNyPz z;`(-V!XuS^^2#MF#tyXMIf&V?hEOeto#B}PG^Dz^1}jfSvr~;pv6ELXg(EP{Twec``qR=Z;K^6u<>H=~y*HEHZCM{|uXkyO;I{oaIZI9;E z@|mP~7_%9W24_@J!5*#tMd`|~AjOy5(yXK7rcdRhotp}VyPB_#e7UalH!mL#TgvXr z#MzJUd5@-0Wt)Vv8pjIE_Wtq)Al;v1@D18#F)RLC??HEWuD+0o(3n4haJgS3J+0k5 z?A+fVwQQo(mA~+A+)4``H8xmAyrJRVPns>*tQ*)deFf~OsktOZ*Mk9D47pc$?VB`O@IEfw-USg<_sQZa6cpk)$M$ zsrotGKl?auBItpzl2^+Zq*ZvAYNFbg=4L38_F9uqXef&<3Pj}QFJ}3X$F2O$K;?8> zDFL_M_K$DhrNkBQUgg$Vdop&o^U>r9(BbFr<5o3gh4uwHca@*R28%G=xb4mwTux#U zu-4++VD4w1Q&Bj0&{hU7$qLAf=@05mFYe>a^16D&CxkXmUSt8T8MM6RKJzv9Ie~!< z=h~oG4BW4~Kx9@xD@Jm!iC2JXY`O#6Z32AY+XINh3BOE7Z<1Fkl<&=|8*0o zkU$XSq8KAcrym0_`gsw(YppXeEmAc7wl(6!-!<8&dIzZeTH8D0N08*cp8SX@DgO?r zVlJ;K%7$g9l8<*MtZ>{r&1NXF+o{8Z9l!0=M}-eU1@rcqDzPY2pxC6?HC81p52qt! zc^uaZ7lce!v@oek0#DYsbBj-WI676{j=Mhbu3+quFQ@j@0PghAr9D@f z{=RpaPd-rdbyt*WPN%Q4*j0UO_KkI`kVlwj^9bFd_wD(T>&mw;BtlK<5Mrq8<@@yj z&e!K^?9>z8Py2+-C$&NK?CM+S@{L?bNO6~K(EySD>B`jcig3M+z1rAXdkJ=SOxM`JrY_@7%6XM9)m?n9@B2 z=%FK&%jh*=N}1YxPG5-UpngNKwvoRc+RgQ*=ip=-$Jb_l|D?Z8gt@SCUP!wv)F&eH z18&rR=5u4t&wcvECPwTVpWmL&M&bsNqbK#f zC;S9B1hZ0A@to)*&rAB=z(oMAC1FeBKt!;*qrOJFBXxiF|gR;5&eu zDAFG!yYsJ{irAXjP+@dNcehY#vQ{Wc-91B@;ZRhO&zYgNtemvg<1$-B%pFH(TsJ}H9v^v0tbFdhUzj?W@2=gXc8#g$ElD9n3SiM4de@u9QbJpQ7W z5vcTUtj|<|mv4vClZ*^ZDT>zt0ZU7^Mpc_@}#Vr7L-rkR!7%TjC&w?Y`$TQB)qgI1-%lzPi3PY0=6J_OoXh4*B zk%U$?g{g?UC;M9Q?$cc;i^+VNxqaI+NwA@ceVwr2jyzIB%aZ5{t3H95re{nCO<3<< zb!U%y4g|!tnjiA$O9^g?*tG}L^VHBWC`({zpc<*bnSBrf(T zsiIJD2ufpg`Ynz%2ZJl6T|Hv$gv~T?$iEowRLqAz8=KH;9Y53bb@^SbngOK$6Q|ZB zL2_BFT@bn~{2N2N?5zqDOT-V#CZm>2c6$o!A?45PmwV`LAyr*@Bk{+J2hy37R+4Qb z+LkycXLtWfV0zPAlt&`dr8{2IOWv(K-R{kdp;f$5st8Rx3hN)t!j0B1?q^{v@TDZT zL5D#4!Ev!U!OX-T42CO8>My3f z3%B%znr$Y8>@X14)r=8_9^PUz84VCw5*FYI{858hslSF>mE8wuu=`Rf<&(q&WOBkb zVnum>fSKg#j(|)&*ML+_cVG~-eTr!63gru>m&>`%zXN1$+N8Lq}^A+fK&zm>WJ{`GW_;lZSL5Cv1%VoTR!_aQv~EW!PO(u≧Q-NA2dm=VTb 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 literal 0 HcmV?d00001 diff --git a/pyface/data_view/abstract_data_model.py b/pyface/data_view/abstract_data_model.py index fe3506b9b..9593dff4e 100644 --- a/pyface/data_view/abstract_data_model.py +++ b/pyface/data_view/abstract_data_model.py @@ -58,13 +58,18 @@ class AbstractDataModel(ABCHasStrictTraits): Implementations should ensure that the ``values_changed`` event fires whenever the data, or the way the data is presented, is updated. - If the data is to be editable then the subclass should override the ``set_value`` method. It should attempt to change the underlying data as a - side-effect, and return True on success and False on failure (for example, + side-effect or raise DataViewSetError on failure (for example, setting an invalid value). If the underlying data structure cannot be - listened to internally (such as a numpy array or Pandas data frame), this - method should also fire the values changed event with appropriate values. + listened to internally (such as a numpy array or Pandas data frame), + ``set_value`` should also fire the ``values_changed`` event with + appropriate values. + + In the cases where the underlying data structure cannot be observed by + the usual traits mechanisms, the end-user of the code may be responsible + for ensuring that the ``structure_changed`` and ``values_changed`` events + are fired appropriately. """ #: The index manager that helps convert toolkit indices to data view diff --git a/pyface/data_view/abstract_value_type.py b/pyface/data_view/abstract_value_type.py index 136b82388..a81281aba 100644 --- a/pyface/data_view/abstract_value_type.py +++ b/pyface/data_view/abstract_value_type.py @@ -30,8 +30,8 @@ class AbstractValueType(ABCHasStrictTraits): display. Subclasses should mark traits that potentially affect the display of values - with ``update_value_type=True`` metdadata, or alternatively fire the ``updated`` - event when the state of the value type changes. + with ``update_value_type=True`` metdadata, or alternatively fire the + ``updated`` event when the state of the value type changes. Each data channel is set up to have a method which returns whether there is a value for the channel, a second method which returns the value, @@ -109,7 +109,7 @@ def set_editor_value(self, model, row, column, value): DataViewSetError If the value cannot be set. """ - model.set_value(row, column, value) + model.set_value(row, column, value) def has_text(self, model, row, column): """ Whether or not the value has a textual representation. From 516cc22f604941faf9eaeb0599c1abd6daa4d373 Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Tue, 14 Jul 2020 10:28:01 +0100 Subject: [PATCH 52/52] Add missing import. --- pyface/data_view/abstract_value_type.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyface/data_view/abstract_value_type.py b/pyface/data_view/abstract_value_type.py index a81281aba..59d600732 100644 --- a/pyface/data_view/abstract_value_type.py +++ b/pyface/data_view/abstract_value_type.py @@ -21,6 +21,8 @@ from traits.api import ABCHasStrictTraits, Event, observe +from .abstract_data_model import DataViewSetError + class AbstractValueType(ABCHasStrictTraits): """ A value type converts raw data into data channels.

NGiTbrD1Eaiw7H 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 CCYuKU literal 0 HcmV?d00001 From aab38544ce943efa7fff00d882af9b7f11794894 Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Wed, 8 Jul 2020 13:56:39 +0100 Subject: [PATCH 43/52] Minro fixes and improvements to documentation. --- docs/source/data_view.rst | 12 +++++++++++- docs/source/submodules.rst | 1 + 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/source/data_view.rst b/docs/source/data_view.rst index 39fcbb352..9f583653c 100644 --- a/docs/source/data_view.rst +++ b/docs/source/data_view.rst @@ -21,7 +21,7 @@ the tuples ``(0,)`` and ``(1,)`` give the two child rows of the root, while ``(0, 1)`` is the second child row of the first child of the root, and so on. Column indices follow a similar pattern, but only have the root and one level -if child indices. +of child indices. When interpreting these values, the root row ``()`` corresponds to the *column* headers, the root column ``()`` corresponds to the *row* headers. @@ -200,6 +200,14 @@ raised: :start-line: 82 :end-line: 89 +Even though a data value may be modifiable at the data model level, the +value types also have the ability to control whether or not the value is +editable. For example, subclasses of |EditableValue|, such as |TextValue| +and |IntValue| have an ``is_editable`` trait that controls whether the +value should be editable in the view (presuming that the underlying value +can be set). Other value types can simply prevent editing by ensuring that +the |has_editor_value| method returns ``False``. + .. rubric:: Footnotes .. [#] A more sophisticated implementation might try to work out @@ -212,6 +220,7 @@ raised: .. |AbstractIndexManager| replace:: :py:class:`~pyface.data_view.index_manager.AbstractIndexManager` .. |AbstractDataModel| replace:: :py:class:`~pyface.data_view.abstract_data_model.AbstractDataModel` .. |DataViewSetError| replace:: :py:class:`~pyface.data_view.abstract_data_model.DataViewSetError` +.. |EditableValue| replace:: :py:class:`~pyface.data_view.value_types.editable_value.EditableValue` .. |IntIndexManager| replace:: :py:class:`~pyface.data_view.index_manager.IntIndexManager` .. |IntValue| replace:: :py:class:`~pyface.data_view.value_types.numeric_value.IntValue` .. |TextValue| replace:: :py:class:`~pyface.data_view.value_types.text_value.TextValue` @@ -222,4 +231,5 @@ raised: .. |get_row_count| replace:: :py:meth:`~pyface.data_view.abstract_data_model.AbstractDataModel.get_row_count` .. |get_value| replace:: :py:meth:`~pyface.data_view.abstract_data_model.AbstractDataModel.get_value` .. |get_value_type| replace:: :py:meth:`~pyface.data_view.abstract_data_model.AbstractDataModel.get_value` +.. |has_editor_value| replace:: :py:meth:`~pyface.data_view.abstract_value_type.AbstractValueType.has_editor_value` .. |set_value| replace:: :py:meth:`~pyface.data_view.abstract_data_model.AbstractDataModel.set_value` diff --git a/docs/source/submodules.rst b/docs/source/submodules.rst index 17c3d4888..b9e698805 100644 --- a/docs/source/submodules.rst +++ b/docs/source/submodules.rst @@ -5,5 +5,6 @@ Submodules .. toctree:: :maxdepth: 2 + Data View Fields Timers From b4ccec7bfd47ea9b59bb2dbeba0316caac28c90e Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Wed, 8 Jul 2020 14:00:47 +0100 Subject: [PATCH 44/52] Remove ui dispatch from pure data models. --- examples/data_view/column_data_model.py | 12 ++++++------ pyface/data_view/data_models/array_data_model.py | 10 +++++----- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/examples/data_view/column_data_model.py b/examples/data_view/column_data_model.py index cc1888eaf..71458b6c5 100644 --- a/examples/data_view/column_data_model.py +++ b/examples/data_view/column_data_model.py @@ -72,19 +72,19 @@ def get_observable(self, obj): # trait observers - @observe('title,title_type.updated', dispatch='ui') + @observe('title,title_type.updated') def title_updated(self, event): self.updated = (self, 'title', []) - @observe('value_type.updated', dispatch='ui') + @observe('value_type.updated') def value_type_updated(self, event): self.updated = (self, 'value', []) - @observe('rows.items', dispatch='ui') + @observe('rows.items') def rows_updated(self, event): self.updated = (self, 'rows', []) - @observe('rows:items:updated', dispatch='ui') + @observe('rows:items:updated') def row_item_updated(self, event): row = event.object row_info, part, row_index = event.new @@ -114,7 +114,7 @@ def set_value(self, obj, value): def get_observable(self): return self.value - @observe('value', dispatch='ui') + @observe('value') def value_type_updated(self, event): self.updated = (self, 'value', []) @@ -147,7 +147,7 @@ def set_value(self, obj, value): def get_observable(self): return self.value + '.items' - @observe('value,key', dispatch='ui') + @observe('value,key') def value_type_updated(self, event): self.updated = (self, 'value', []) diff --git a/pyface/data_view/data_models/array_data_model.py b/pyface/data_view/data_models/array_data_model.py index ee475f8ae..ef917d1f6 100644 --- a/pyface/data_view/data_models/array_data_model.py +++ b/pyface/data_view/data_models/array_data_model.py @@ -264,7 +264,7 @@ def get_value_type(self, row, column): # data update methods - @observe('data', dispatch='ui') + @observe('data') def data_updated(self, event): """ Handle the array being replaced with a new array. """ if event.new.shape == event.old.shape: @@ -275,24 +275,24 @@ def data_updated(self, event): else: self.structure_changed = True - @observe('value_type.updated', dispatch='ui') + @observe('value_type.updated') def value_type_updated(self, event): """ Handle the value type being updated. """ self.values_changed = ( (0,), (0,), (self.data.shape[0] - 1,), (self.data.shape[-1] - 1,) ) - @observe('column_header_type.updated', dispatch='ui') + @observe('column_header_type.updated') def column_header_type_updated(self, event): """ Handle the column header type being updated. """ self.values_changed = ((), (0,), (), (self.data.shape[-1] - 1,)) - @observe('row_header_type.updated', dispatch='ui') + @observe('row_header_type.updated') def value_header_type_updated(self, event): """ Handle the value header type being updated. """ self.values_changed = ((0,), (), (self.data.shape[0] - 1,), ()) - @observe('label_header_type.updated', dispatch='ui') + @observe('label_header_type.updated') def label_header_type_updated(self, event): """ Handle the label header type being updated. """ self.values_changed = ((), (), (), ()) From f382d71f4d979a44f2aac7a9936786683822b82f Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Wed, 8 Jul 2020 14:58:32 +0100 Subject: [PATCH 45/52] Docstring fixes. --- pyface/data_view/abstract_value_type.py | 6 +++--- pyface/data_view/index_manager.py | 20 ++++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/pyface/data_view/abstract_value_type.py b/pyface/data_view/abstract_value_type.py index c8d999054..985d1fe9c 100644 --- a/pyface/data_view/abstract_value_type.py +++ b/pyface/data_view/abstract_value_type.py @@ -173,10 +173,10 @@ def set_text(self, model, row, column, text): text : str The text to set. - Returns + Raises ------- - success : bool - Whether or not the value was successfully set. + DataViewSetError + If the value cannot be set. """ return False diff --git a/pyface/data_view/index_manager.py b/pyface/data_view/index_manager.py index c17a6c506..e87a787b5 100644 --- a/pyface/data_view/index_manager.py +++ b/pyface/data_view/index_manager.py @@ -194,12 +194,12 @@ def id(self, index): Parameters ---------- index : index object - The persistent index object associated with this id. + The persistent index object. Returns ------- id : int - An integer object id value. + The associated integer object id value. """ raise NotImplementedError() @@ -316,13 +316,13 @@ def id(self, index): Parameters ---------- - id : int - An integer object id value. + index : index object + The persistent index object. Returns ------- - index : index object - The persistent index object associated with this id. + id : int + The associated integer object id value. """ if index == Root: return 0 @@ -410,13 +410,13 @@ def id(self, index): Parameters ---------- - id : int - An integer object id value. + index : index object + The persistent index object. Returns ------- - index : index object - The persistent index object associated with this id. + id : int + The associated integer object id value. """ if index == Root: return 0 From 65a1ebeb03c0b8dfbf7980dc7ce38f94de75ddd1 Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Wed, 8 Jul 2020 17:47:39 +0100 Subject: [PATCH 46/52] More fixes from PR review. --- pyface/data_view/abstract_data_model.py | 4 ++-- .../data_view/data_models/array_data_model.py | 19 ++++++++++++------- .../tests/test_array_data_model.py | 19 ++++++++++--------- 3 files changed, 24 insertions(+), 18 deletions(-) diff --git a/pyface/data_view/abstract_data_model.py b/pyface/data_view/abstract_data_model.py index 8f697a725..8d7dcaa20 100644 --- a/pyface/data_view/abstract_data_model.py +++ b/pyface/data_view/abstract_data_model.py @@ -209,8 +209,8 @@ def get_value_type(self, row, column): """ Return the value type of the given row and column. The value type for column headers are returned by calling this method - with row equal to []. The value types for row headers are returned - by calling this method with column equal to []. + with row equal to (). The value types for row headers are returned + by calling this method with column equal to (). Parameters ---------- diff --git a/pyface/data_view/data_models/array_data_model.py b/pyface/data_view/data_models/array_data_model.py index ef917d1f6..4c81fcf4f 100644 --- a/pyface/data_view/data_models/array_data_model.py +++ b/pyface/data_view/data_models/array_data_model.py @@ -16,7 +16,7 @@ from traits.api import Array, HasRequiredTraits, Instance, observe -from pyface.data_view.abstract_data_model import AbstractDataModel +from pyface.data_view.abstract_data_model import AbstractDataModel, DataViewSetError from pyface.data_view.abstract_value_type import AbstractValueType from pyface.data_view.value_types.api import ( ConstantValue, FloatValue, IntValue, TextValue, no_value @@ -232,12 +232,17 @@ def set_value(self, row, column, value): index = tuple(row + column) self.data[index] = value self.values_changed = (row, column, row, column) - return True - - return False + else: + raise DataViewSetError() def get_value_type(self, row, column): - """ Return the text value for the row and column. + """ Return the value type of the given row and column. + + This method returns the value of ``column_header_type`` for column + headers, the value of ``row_header_type`` for row headers, the value + of ``label_header_type`` for the top-left corner value, the value of + ``value_type`` for all array values, and ``no_value`` for everything + else. Parameters ---------- @@ -248,8 +253,8 @@ def get_value_type(self, row, column): Returns ------- - text : str - The text to display in the given row and column. + value_type : AbstractValueType + The value type of the given row and column. """ if len(row) == 0: if len(column) == 0: diff --git a/pyface/data_view/data_models/tests/test_array_data_model.py b/pyface/data_view/data_models/tests/test_array_data_model.py index c18d36275..961e25add 100644 --- a/pyface/data_view/data_models/tests/test_array_data_model.py +++ b/pyface/data_view/data_models/tests/test_array_data_model.py @@ -13,6 +13,7 @@ from traits.testing.unittest_tools import UnittestTools from traits.testing.optional_dependencies import numpy as np, requires_numpy +from pyface.data_view.abstract_data_model import DataViewSetError from pyface.data_view.abstract_value_type import AbstractValueType from pyface.data_view.value_types.api import ( FloatValue, IntValue, TextValue, no_value @@ -112,24 +113,24 @@ def test_set_value(self): for row, column in self.model.iter_items(): with self.subTest(row=row, column=column): if len(row) == 0 and len(column) == 0: - result = self.model.set_value(row, column, 0) - self.assertFalse(result) + with self.assertRaises(DataViewSetError): + self.model.set_value(row, column, 0) elif len(row) == 0: - result = self.model.set_value(row, column, column[0] + 1) - self.assertFalse(result) + with self.assertRaises(DataViewSetError): + self.model.set_value(row, column, column[0] + 1) elif len(column) == 0: - result = self.model.set_value(row, column, row[-1] + 1) - self.assertFalse(result) + with self.assertRaises(DataViewSetError): + self.model.set_value(row, column, row[-1] + 1) elif len(row) == 1: value = 6.0 * row[-1] + 2 * column[0] with self.assertTraitDoesNotChange( self.model, "values_changed"): - result = self.model.set_value(row, column, value) + with self.assertRaises(DataViewSetError): + self.model.set_value(row, column, value) else: value = 6.0 * row[-1] + 2 * column[0] with self.assertTraitChanges(self.model, "values_changed"): - result = self.model.set_value(row, column, value) - self.assertTrue(result) + self.model.set_value(row, column, value) self.assertEqual( self.array[row[0], row[1], column[0]], value, From 4b0f75aca0862295e61d726788abeb43672ea182 Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Fri, 10 Jul 2020 12:13:38 +0100 Subject: [PATCH 47/52] Change the way that we handle low-dimension arrays. --- .../data_view/data_models/array_data_model.py | 33 ++++++------------- .../tests/test_array_data_model.py | 20 +++++++++-- 2 files changed, 27 insertions(+), 26 deletions(-) diff --git a/pyface/data_view/data_models/array_data_model.py b/pyface/data_view/data_models/array_data_model.py index 4c81fcf4f..04c21280c 100644 --- a/pyface/data_view/data_models/array_data_model.py +++ b/pyface/data_view/data_models/array_data_model.py @@ -26,33 +26,14 @@ class _AtLeastTwoDArray(Array): """ Trait type that holds an array that at least two dimensional. - - This calls numpy.atleast_2d during validation to ensure that the value is - good. """ def validate(self, object, name, value): - from numpy import atleast_2d value = super().validate(object, name, value) - return atleast_2d(value) - - def _default_for_dtype_and_shape(self, dtype, shape): - """ Invent a suitable default value for a given dtype and shape. """ - from numpy import zeros - - if shape is None: - value = zeros((0, 0), dtype) - else: - size = [] - for item in shape: - if item is None: - item = 0 - elif isinstance(item, Sequence): - # Given a (minimum-allowed-length, maximum-allowed_length) - # pair for a particular axis, use the minimum. - item = item[0] - size.append(item) - value = zeros(size, dtype) + if value.ndim == 0: + value = value.reshape((0, 0)) + elif value.ndim == 1: + value = value.reshape((-1, 1)) return value @@ -301,3 +282,9 @@ def value_header_type_updated(self, event): def label_header_type_updated(self, event): """ Handle the label header type being updated. """ self.values_changed = ((), (), (), ()) + + # default array value + + def _data_default(self): + from numpy import zeros + return zeros(shape=(0, 0)) diff --git a/pyface/data_view/data_models/tests/test_array_data_model.py b/pyface/data_view/data_models/tests/test_array_data_model.py index 961e25add..805331ef6 100644 --- a/pyface/data_view/data_models/tests/test_array_data_model.py +++ b/pyface/data_view/data_models/tests/test_array_data_model.py @@ -52,6 +52,7 @@ def test_no_data(self): model = ArrayDataModel(value_type=FloatValue()) self.assertEqual(model.data.ndim, 2) self.assertEqual(model.data.shape, (0, 0)) + self.assertEqual(model.data.dtype, np.float) self.assertEqual(model.get_column_count(), 0) self.assertTrue(model.can_have_children(())) self.assertEqual(model.get_row_count(()), 0) @@ -60,12 +61,25 @@ def test_data_1d(self): array = np.arange(30.0) model = ArrayDataModel(data=array, value_type=FloatValue()) self.assertEqual(model.data.ndim, 2) - self.assertEqual(model.data.shape, (1, 30)) + self.assertEqual(model.data.shape, (30, 1)) + + def test_data_list(self): + data = list(range(30)) + model = ArrayDataModel(data=data, value_type=FloatValue()) + self.assertEqual(model.data.ndim, 2) + self.assertEqual(model.data.shape, (30, 1)) def test_set_data_1d(self): - self.model.data = np.arange(30.0) + with self.assertTraitChanges(self.model, 'structure_changed'): + self.model.data = np.arange(30.0) + self.assertEqual(self.model.data.ndim, 2) + self.assertEqual(self.model.data.shape, (30, 1)) + + def test_set_data_list(self): + with self.assertTraitChanges(self.model, 'structure_changed'): + self.model.data = list(range(30)) self.assertEqual(self.model.data.ndim, 2) - self.assertEqual(self.model.data.shape, (1, 30)) + self.assertEqual(self.model.data.shape, (30, 1)) def test_get_column_count(self): result = self.model.get_column_count() From 89748c7cd0e2d1bfef2e9aca9dfc6ca175e5d1dd Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Fri, 10 Jul 2020 12:14:26 +0100 Subject: [PATCH 48/52] Handle unusual life-cycle issues. --- .../ui/qt4/data_view/data_view_item_model.py | 66 +++++++++++-------- 1 file changed, 38 insertions(+), 28 deletions(-) diff --git a/pyface/ui/qt4/data_view/data_view_item_model.py b/pyface/ui/qt4/data_view/data_view_item_model.py index 9b0f142e7..67839276d 100644 --- a/pyface/ui/qt4/data_view/data_view_item_model.py +++ b/pyface/ui/qt4/data_view/data_view_item_model.py @@ -13,7 +13,9 @@ from pyface.qt import is_qt5 from pyface.qt.QtCore import QAbstractItemModel, QModelIndex, Qt from pyface.data_view.index_manager import Root -from pyface.data_view.abstract_data_model import AbstractDataModel, DataViewSetError +from pyface.data_view.abstract_data_model import ( + AbstractDataModel, DataViewSetError +) logger = logging.getLogger(__name__) @@ -27,7 +29,7 @@ class DataViewItemModel(QAbstractItemModel): def __init__(self, model, parent=None): super().__init__(parent) self.model = model - self.showRowHeader = True + self.destroyed.connect(self._on_destroyed) @property def model(self): @@ -35,39 +37,15 @@ def model(self): @model.setter def model(self, model: AbstractDataModel): + self._disconnect_model_observers() if hasattr(self, '_model'): - # disconnect trait listeners - self._model.observe( - self.on_structure_changed, - 'structure_changed', - dispatch='ui', - remove=True, - ) - self._model.observe( - self.on_values_changed, - 'values_changed', - dispatch='ui', - remove=True, - ) - self.beginResetModel() self._model = model self.endResetModel() else: # model is being initialized self._model = model - - # hook up trait listeners - self._model.observe( - self.on_structure_changed, - 'structure_changed', - dispatch='ui', - ) - self._model.observe( - self.on_values_changed, - 'values_changed', - dispatch='ui', - ) + self._connect_model_observers() # model event listeners @@ -221,6 +199,38 @@ def headerData(self, section, orientation, role=Qt.DisplayRole): # Private utility methods + def _on_destroyed(self): + self._disconnect_model_observers() + self._model = None + + def _connect_model_observers(self): + if getattr(self, "_model", None) is not None: + self._model.observe( + self.on_structure_changed, + 'structure_changed', + dispatch='ui', + ) + self._model.observe( + self.on_values_changed, + 'values_changed', + dispatch='ui', + ) + + def _disconnect_model_observers(self): + if getattr(self, "_model", None) is not None: + self._model.observe( + self.on_structure_changed, + 'structure_changed', + dispatch='ui', + remove=True, + ) + self._model.observe( + self.on_values_changed, + 'values_changed', + dispatch='ui', + remove=True, + ) + def _to_row_index(self, index): if not index.isValid(): row_index = () From b559ac5ae320265c2aac1ba0cb61bff6a7cd99d6 Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Mon, 13 Jul 2020 09:25:11 +0100 Subject: [PATCH 49/52] Remove extraneous file commited. --- pyface/data_view/value_types/dtype_value.py | 65 --------------------- 1 file changed, 65 deletions(-) delete mode 100644 pyface/data_view/value_types/dtype_value.py diff --git a/pyface/data_view/value_types/dtype_value.py b/pyface/data_view/value_types/dtype_value.py deleted file mode 100644 index d21ea0d0f..000000000 --- a/pyface/data_view/value_types/dtype_value.py +++ /dev/null @@ -1,65 +0,0 @@ -# (C) Copyright 2005-2020 Enthought, Inc., Austin, TX -# All rights reserved. -# -# This software is provided without warranty under the terms of the BSD -# license included in LICENSE.txt and may be redistributed only under -# the conditions described in the aforementioned license. The license -# is also available online at http://www.enthought.com/licenses/BSD.txt -# -# Thanks for using Enthought open source! - -from traits.api import Callable - -from .editable_value import EditableValue - - -class DtypeValue(EditableValue): - """ Editable value that presents value depending on numpy dtype. - """ - - #: A function that converts the value to a string for display. - format = Callable(str, update_value_type=True) - - #: A function that converts to a value from a display string. - unformat = Callable(str) - - def get_text(self, model, row, column): - """ Get the display text from the underlying value. - - Parameters - ---------- - model : AbstractDataModel - The data model holding the data. - row : sequence of int - The row in the data model being queried. - column : sequence of int - The column in the data model being queried. - - Returns - ------- - text : str - The text to display. - """ - return self.format(model.get_value(row, column)) - - def set_text(self, model, row, column, text): - """ Set the text of the underlying value. - - Parameters - ---------- - model : AbstractDataModel - The data model holding the data. - row : sequence of int - The row in the data model being queried. - column : sequence of int - The column in the data model being queried. - text : str - The text to set. - - Raises - ------- - DataViewSetError - If the value cannot be set. - """ - value = self.unformat(text) - self.set_editor_value(model, row, column, value) From b92e062c011d97b2585d79c3f4a0dddc1996c7cc Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Tue, 14 Jul 2020 09:09:31 +0100 Subject: [PATCH 50/52] Apply suggestions from code review Co-authored-by: Kit Choi --- docs/source/data_view.rst | 81 ++++++++----------- examples/data_view/column_data_model.py | 10 +-- examples/data_view/column_example.py | 3 + pyface/data_view/abstract_data_model.py | 16 ++-- pyface/data_view/abstract_value_type.py | 4 +- pyface/data_view/index_manager.py | 8 +- .../tests/test_abstract_value_type.py | 4 +- 7 files changed, 58 insertions(+), 68 deletions(-) diff --git a/docs/source/data_view.rst b/docs/source/data_view.rst index 9f583653c..15a581e45 100644 --- a/docs/source/data_view.rst +++ b/docs/source/data_view.rst @@ -16,7 +16,7 @@ to represent the rows and columns, as illustrated below: How DataView Indexing Works. A row index corresponds to a list of integer indexes at each level of the -heirarchy, so the empty tuple ``()`` represents the root of the heirarchy, +hierarchy, so the empty tuple ``()`` represents the root of the hierarchy, the tuples ``(0,)`` and ``(1,)`` give the two child rows of the root, while ``(0, 1)`` is the second child row of the first child of the root, and so on. @@ -36,7 +36,7 @@ These indices need to be converted to and from whatever system the backend toolkit uses for indexing and tracking rows. This conversion is handled by an |AbstractIndexManager| instance. Pyface provides two of these which efficiently handle the two common cases: |TupleIndexManager| is designed to -handle general heirarchical models, but needs to cache mementos for all rows +handle general hierarchical models, but needs to cache mementos for all rows with children (and on Wx, for all rows); the |IntIndexManager| can only handle non-hierarchical tables, but does so without needing any additional memory allocation. @@ -56,10 +56,9 @@ interface of |AbstractDataModel|. A data model for a dictionary could be implemented like this: -.. include:: examples/dict_data_model.py - :code: python - :start-line: 19 - :end-line: 27 +.. literalinclude:: examples/dict_data_model.py + :start-at: class DictDataModel + :end-at: index_manager = The base |AbstractDataModel| class requires you to provide an index manager so we use an |IntIndexManager| because the data is always non-hierarchical @@ -73,30 +72,27 @@ how many columns are in the data model. For the dict model, keys are displayed in the row headers, so there is just one column displaying the value: -.. include:: examples/dict_data_model.py - :code: python - :start-line: 44 - :end-line: 45 +.. literalinclude:: examples/dict_data_model.py + :start-at: def get_column_count + :end-at: return We can signal to the toolkit that certain rows can never have children via the |can_have_children| method. The dict data model is non-hierarchical, so the root has children but no other rows will ever have children: -.. include:: examples/dict_data_model.py - :code: python - :start-line: 47 - :end-line: 48 +.. literalinclude:: examples/dict_data_model.py + :start-at: def can_have_children + :end-at: return We need to tell the toolkit how many child rows a particular row has, which is done via the |get_row_count| method. In this example, only the root has children, and the number of child rows of the root is the length of the dictionary: -.. include:: examples/dict_data_model.py - :code: python - :start-line: 50 - :end-line: 53 +.. literalinclude:: examples/dict_data_model.py + :start-at: def get_row_count + :end-at: return 0 Data Values ~~~~~~~~~~~ @@ -106,20 +102,18 @@ To get the values of the dict data model, we need to determine from the row and column index whether or not the cell is a column header and whether it corresponds to the keys or the values. The code looks like this: -.. include:: examples/dict_data_model.py - :code: python - :start-line: 55 - :end-line: 70 +.. literalinclude:: examples/dict_data_model.py + :start-at: def get_value + :end-at: return value Conversion of values into data channels is done by providing a value type for each cell. The |get_value_type| method provides an appropriate data type for each item in the table. For this data model we have three value types: the column headers, the keys and the values. -.. include:: examples/dict_data_model.py - :code: python - :start-line: 35 - :end-line: 42 +.. literalinclude:: examples/dict_data_model.py + :start-at: #: The header data + :lines: 1-8 The default values of these traits are defined to be |TextValue| instances. Users of the model can provide different value types when instantiating, @@ -131,10 +125,9 @@ could be used instead for the ``value_type`` trait:: The |get_value_type| method uses the indices to select the appropriate value types: -.. include:: examples/dict_data_model.py - :code: python - :start-line: 72 - :end-line: 78 +.. literalinclude:: examples/dict_data_model.py + :start-at: def get_value_type + :end-at: return self.value_type Handling Updates ~~~~~~~~~~~~~~~~ @@ -159,10 +152,9 @@ For example, we want to listen for changes in the dictionary and its items. It is simplest in this case to just indicate that the entire model needs updating by firing the ``structure_changed`` event [#]_: -.. include:: examples/dict_data_model.py - :code: python - :start-line: 103 - :end-line: 106 +.. literalinclude:: examples/dict_data_model.py + :start-at: @observe('data.items') + :end-at: self.structure_changed Changes to the value types also should fire update events, but usually these are simply changes to the data, rather than changes to the structure @@ -170,10 +162,9 @@ of the table. All value types have an updated event which is fired when any state of the type changes. We can observe these, compute which indices are affected, and fire the appropriate event. -.. include:: examples/dict_data_model.py - :code: python - :start-line: 91 - :end-line: 101 +.. literalinclude:: examples/dict_data_model.py + :start-at: @observe('header_value_type.updated') + :lines: 1-11 Editing Values ~~~~~~~~~~~~~~ @@ -184,10 +175,9 @@ A model can flag values as being modifiable by implementing the modification of the values. For example, to allow modification of the values of the dictionary, we could write: -.. include:: examples/dict_data_model.py - :code: python - :start-line: 80 - :end-line: 81 +.. literalinclude:: examples/dict_data_model.py + :start-at: def can_set_value + :end-at: return A corresponding |set_value| method is needed to actually perform the changes to the underlying values. If for some reason it is impossible to set the @@ -195,10 +185,9 @@ value (eg. an invalid value is supplied, or |set_value| is called with an inappropriate row or column value, then a |DataViewSetError| should be raised: -.. include:: examples/dict_data_model.py - :code: python - :start-line: 82 - :end-line: 89 +.. literalinclude:: examples/dict_data_model.py + :start-at: def set_value + :end-at: raise Even though a data value may be modifiable at the data model level, the value types also have the ability to control whether or not the value is diff --git a/examples/data_view/column_data_model.py b/examples/data_view/column_data_model.py index 71458b6c5..151de5b8f 100644 --- a/examples/data_view/column_data_model.py +++ b/examples/data_view/column_data_model.py @@ -64,7 +64,7 @@ def can_set_value(self, obj): raise NotImplementedError() def set_value(self, obj): - return False + return @abstractmethod def get_observable(self, obj): @@ -107,9 +107,8 @@ def can_set_value(self, obj): def set_value(self, obj, value): if not self.value: - return False + return xsetattr(obj, self.value, value) - return True def get_observable(self): return self.value @@ -142,7 +141,6 @@ def can_set_value(self, obj): def set_value(self, obj, value): data = xgetattr(obj, self.value, None) data[self.key] = value - return True def get_observable(self): return self.value + '.items' @@ -214,9 +212,9 @@ def can_set_value(self, row, column): def set_value(self, row, column, value): row_info = self._row_info_object(row) if len(column) == 0: - return False + raise DataViewSetError("Cannot set value for row header.") obj = self.data[column[0]] - return row_info.set_value(obj, value) + row_info.set_value(obj, value) def get_value_type(self, row, column): row_info = self._row_info_object(row) diff --git a/examples/data_view/column_example.py b/examples/data_view/column_example.py index 3f066fdac..059ab9d12 100644 --- a/examples/data_view/column_example.py +++ b/examples/data_view/column_example.py @@ -103,6 +103,9 @@ def _data_default(self): import numpy return numpy.random.uniform(size=(100000, 10)) + def destroy(self): + self.data_view.destroy() + super().destroy() male_names = [ 'Michael', 'Edward', 'Timothy', 'James', 'George', 'Ralph', 'David', diff --git a/pyface/data_view/abstract_data_model.py b/pyface/data_view/abstract_data_model.py index 8d7dcaa20..fe3506b9b 100644 --- a/pyface/data_view/abstract_data_model.py +++ b/pyface/data_view/abstract_data_model.py @@ -36,7 +36,7 @@ class AbstractDataModel(ABCHasStrictTraits): values provided by the data, but not with how the data is presented. Row and column indices are represented by sequences (usually lists) of - integers, specifying the index at each level of the heirarchy. The root + integers, specifying the index at each level of the hierarchy. The root row and column are represented by empty lists. Subclasses need to implement the ``get_column_count``, @@ -137,8 +137,8 @@ def get_value(self, row, column): """ Return the Python value for the row and column. The values for column headers are returned by calling this method with - row equal to []. The values for row headers are returned by calling - this method with column equal to []. + row equal to (). The values for row headers are returned by calling + this method with column equal to (). Parameters ---------- @@ -161,8 +161,8 @@ def can_set_value(self, row, column): returns False. Whether or a column header can be set is returned by calling this - method with row equal to []. Whether or a row header can be set - is returned by calling this method with column equal to []. + method with row equal to (). Whether or a row header can be set + is returned by calling this method with column equal to (). Parameters ---------- @@ -185,8 +185,8 @@ def set_value(self, row, column, value): returns False. The values for column headers can be set by calling this method with - row equal to []. The values for row headers can be set by calling - this method with column equal to []. + row equal to (). The values for row headers can be set by calling + this method with column equal to (). Parameters ---------- @@ -253,7 +253,7 @@ def iter_items(self, start_row=()): """ Iterator that yields rows and columns in preorder. This yields pairs of row, column for all rows in preorder - and and all column indices for all rows, including []. + and and all column indices for all rows, including (). Columns are iterated in order. Parameters diff --git a/pyface/data_view/abstract_value_type.py b/pyface/data_view/abstract_value_type.py index 985d1fe9c..136b82388 100644 --- a/pyface/data_view/abstract_value_type.py +++ b/pyface/data_view/abstract_value_type.py @@ -109,7 +109,7 @@ def set_editor_value(self, model, row, column, value): DataViewSetError If the value cannot be set. """ - return model.set_value(row, column, value) + model.set_value(row, column, value) def has_text(self, model, row, column): """ Whether or not the value has a textual representation. @@ -178,7 +178,7 @@ def set_text(self, model, row, column, text): DataViewSetError If the value cannot be set. """ - return False + raise DataViewSetError("Cannot set value.") @observe('+update_value_type') def update_value_type(self, event=None): diff --git a/pyface/data_view/index_manager.py b/pyface/data_view/index_manager.py index e87a787b5..3e08e8d46 100644 --- a/pyface/data_view/index_manager.py +++ b/pyface/data_view/index_manager.py @@ -28,7 +28,7 @@ The default representation of an index from the point of view of the data view infrastructure is a sequence of integers, giving the index at -each level of the heirarchy. DataViewModel classes can then use these +each level of the hierarchy. DataViewModel classes can then use these indices to identify objects in the underlying data model. There are three main classes defined in the module: AbstractIndexManager, @@ -130,7 +130,7 @@ def from_sequence(self, indices): Parameters ---------- indices : sequence of int - The row location at each level of the heirarchy. + The row location at each level of the hierarchy. Returns ------- @@ -152,7 +152,7 @@ def to_sequence(self, index): """ Given an index, return the corresponding sequence of row values. The default implementation repeatedly calls get_parent_and_row() - to walk up the heirarchy and push the row values into the start + to walk up the hierarchy and push the row values into the start of the sequence. Parameters @@ -163,7 +163,7 @@ def to_sequence(self, index): Returns ------- sequence : tuple of int - The row location at each level of the heirarchy. + The row location at each level of the hierarchy. """ result = () while index != Root: diff --git a/pyface/data_view/tests/test_abstract_value_type.py b/pyface/data_view/tests/test_abstract_value_type.py index 5e40be28d..b3cea4e52 100644 --- a/pyface/data_view/tests/test_abstract_value_type.py +++ b/pyface/data_view/tests/test_abstract_value_type.py @@ -72,8 +72,8 @@ def test_get_text(self): def test_set_text(self): value_type = ValueType() - result = value_type.set_text(self.model, [0], [0], "2.0") - self.assertFalse(result) + with self.assertRaises(DataViewSetError): + value_type.set_text(self.model, [0], [0], "2.0") def test_parameter_update(self): value_type = ValueType() From 83a80cac137a1f2cdb0987d5f2f94e1e89464df3 Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Tue, 14 Jul 2020 09:46:32 +0100 Subject: [PATCH 51/52] various fixes and improvements from review. --- docs/source/data_view.rst | 15 +++++++++++---- docs/source/images/dict_data_model.png | Bin 0 -> 112179 bytes pyface/data_view/abstract_data_model.py | 13 +++++++++---- pyface/data_view/abstract_value_type.py | 6 +++--- 4 files changed, 23 insertions(+), 11 deletions(-) create mode 100644 docs/source/images/dict_data_model.png diff --git a/docs/source/data_view.rst b/docs/source/data_view.rst index 15a581e45..5f2781b98 100644 --- a/docs/source/data_view.rst +++ b/docs/source/data_view.rst @@ -11,6 +11,7 @@ The DataView API has a consistent way of indexing that uses tuples of integers to represent the rows and columns, as illustrated below: .. figure:: images/data_view_indices.png + :scale: 50 :alt: an illustration of data view indices How DataView Indexing Works. @@ -52,17 +53,23 @@ Data Models Data to be viewed needs to be exposed to the DataView infrastructure by creating a data model for it. This is a class that implements the -interface of |AbstractDataModel|. +interface of |AbstractDataModel| to display values from a dictionary. -A data model for a dictionary could be implemented like this: +.. figure:: images/dict_data_model.png + :scale: 50 + :alt: an illustration of the DictDataModel + + The DictDataModel example. + +The basic traits for the model might look like this: .. literalinclude:: examples/dict_data_model.py :start-at: class DictDataModel :end-at: index_manager = The base |AbstractDataModel| class requires you to provide an index manager -so we use an |IntIndexManager| because the data is always non-hierarchical -for this model. +so we use an |IntIndexManager| because the data is non-hierarchical for this +model. Data Structure ~~~~~~~~~~~~~~ diff --git a/docs/source/images/dict_data_model.png b/docs/source/images/dict_data_model.png new file mode 100644 index 0000000000000000000000000000000000000000..202cad349a2af85b809d7f57b9ab42bde01336b1 GIT binary patch literal 112179 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