Improved hscommon doc

* Completed hscommon.gui.table's doc
* Use sphinx.ext.autosummary.
* Moved attribute docstrings directly into properties.
This commit is contained in:
Virgil Dupras 2013-11-30 12:15:03 -05:00
parent 31395d8794
commit 5a275db67d
9 changed files with 239 additions and 81 deletions

View File

@ -31,6 +31,8 @@ def fix_nulljob_in_sig(app, what, name, obj, options, signature, return_annotati
def setup(app):
app.connect('autodoc-process-signature', fix_nulljob_in_sig)
autodoc_member_order = 'groupwise'
# -- General configuration -----------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
@ -38,7 +40,7 @@ def setup(app):
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ['sphinx.ext.todo', 'sphinx.ext.autodoc']
extensions = ['sphinx.ext.todo', 'sphinx.ext.autodoc', 'sphinx.ext.autosummary']
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']

View File

@ -3,6 +3,10 @@ hscommon.gui.base
.. automodule:: hscommon.gui.base
.. autosummary::
GUIObject
.. autoclass:: GUIObject
:members:
:private-members:

View File

@ -3,6 +3,13 @@ hscommon.gui.selectable_list
.. automodule:: hscommon.gui.selectable_list
.. autosummary::
Selectable
SelectableList
GUISelectableList
GUISelectableListView
.. autoclass:: Selectable
:members:
:private-members:

View File

@ -2,7 +2,14 @@ hscommon.gui.table
==================
.. automodule:: hscommon.gui.table
.. autosummary::
Table
Row
GUITable
GUITableView
.. autoclass:: Table
:members:
:private-members:
@ -11,3 +18,9 @@ hscommon.gui.table
:members:
:private-members:
.. autoclass:: GUITable
:members:
:private-members:
.. autoclass:: GUITableView
:members:

View File

@ -3,6 +3,11 @@ hscommon.gui.text_field
.. automodule:: hscommon.gui.text_field
.. autosummary::
TextField
TextFieldView
.. autoclass:: TextField
:members:
:private-members:

View File

@ -28,16 +28,6 @@ class GUIObject:
When you subclass ``GUIObject``, you will likely want to update its view on instantiation. That
is why we call ``self.view.refresh()`` in :meth:`_view_updated`. If you need another type of
action on view instantiation, just override the method.
.. attribute:: view
A reference to our toolkit-specific view controller. This view starts as ``None`` and has to
be set "manually". There's two times at which we set the view property: On initialization,
where we set the view that we'll use for our lifetime, and just before the view is
deallocated. We need to unset our view at that time to avoid calls to a deallocated instance
(which means a crash).
To unset our view, we simple assign it to ``None``.
"""
def __init__(self):
self._view = None
@ -55,6 +45,17 @@ class GUIObject:
@property
def view(self):
"""A reference to our toolkit-specific view controller.
*view answering to GUIObject sublass's view protocol*. *get/set*
This view starts as ``None`` and has to be set "manually". There's two times at which we set
the view property: On initialization, where we set the view that we'll use for our lifetime,
and just before the view is deallocated. We need to unset our view at that time to avoid
calls to a deallocated instance (which means a crash).
To unset our view, we simple assign it to ``None``.
"""
return self._view
@view.setter

View File

@ -15,17 +15,6 @@ class Selectable(Sequence):
When mixed in with a ``Sequence``, we enable it to manage its selection status. The selection
is held as a list of ``int`` indexes. Multiple selection is supported.
.. attribute:: selected_index
*int*. *get/set*. Thin wrapper around :attr:`selected_indexes`. Points to the first selected
index or ``None`` if it's empty. Using this property only makes sense if your selectable
sequence supports single selection only.
.. attribute:: selected_indexes
*list*. *get/set*. List of selected indexes. When setting the value, automatically removes
out-of-bounds indexes. The list is kept sorted.
"""
def __init__(self):
self._selected_indexes = []
@ -74,6 +63,13 @@ class Selectable(Sequence):
#--- Properties
@property
def selected_index(self):
"""Points to the first selected index.
*int*. *get/set*.
Thin wrapper around :attr:`selected_indexes`. ``None`` if selection is empty. Using this
property only makes sense if your selectable sequence supports single selection only.
"""
return self._selected_indexes[0] if self._selected_indexes else None
@selected_index.setter
@ -82,6 +78,13 @@ class Selectable(Sequence):
@property
def selected_indexes(self):
"""List of selected indexes.
*list of int*. *get/set*.
When setting the value, automatically removes out-of-bounds indexes. The list is kept
sorted.
"""
return self._selected_indexes
@selected_indexes.setter
@ -170,7 +173,7 @@ class GUISelectableListView:
"""
class GUISelectableList(SelectableList, GUIObject):
"""Cross-toolkit list view.
"""Cross-toolkit GUI-enabled list view.
Represents a UI element presenting the user with a selectable list of items.

View File

@ -11,7 +11,6 @@ from collections import MutableSequence, namedtuple
from .base import GUIObject
from .selectable_list import Selectable
# We used to directly subclass list, but it caused problems at some point with deepcopy
class Table(MutableSequence, Selectable):
"""Sortable and selectable sequence of :class:`Row`.
@ -24,43 +23,6 @@ class Table(MutableSequence, Selectable):
Usually used with :class:`~hscommon.gui.column.Column`.
Subclasses :class:`~hscommon.gui.selectable_list.Selectable`.
.. attribute:: header
.. attribute:: footer
When set to something else than ``None``, represent rows that will always be kept in first
and/or last position, regardless of sorting. ``len()`` and indexing will include them, which
means that if there's a header, ``table[0]`` returns it and if there's a footer,
``table[-1]`` returns it. To make things short, all list-like functions work with header and
footer "on". But things get fuzzy for ``append()`` and ``insert()`` because these will
ensure that no "normal" row gets inserted before the header or after the footer.
Adding and removing footer here and there might seem (and is) hackish, but it's much simpler
than the alternative (when, of course, you need such a feature), which is to override magic
methods and adjust the results. When we do that, there the slice stuff that we have to
implement and it gets quite complex. Moreover, the most frequent operation on a table is
``__getitem__``, and making checks to know whether the key is a header or footer at each
call would make that operation, which is the most used, slower.
.. attribute:: row_count
Number or rows in the table (without counting header and footer).
.. attribute:: rows
List of rows in the table, excluding header and footer.
.. attribute:: selected_row
:class:`Row`. *get/set*. Selected row, based on
:attr:`~hscommon.gui.selectable_list.Selectable.selected_index`. When setting this
attribute, we look up the index of the row and set the selected index from there. If the
row isn't in the list, selection isn't changed.
.. attribute:: selected_rows
List of :class:`Row`. *read-only*. List of selected rows based on
:attr:`~hscommon.gui.selectable_list.Selectable.selected_indexes`.
"""
def __init__(self):
Selectable.__init__(self)
@ -142,6 +104,25 @@ class Table(MutableSequence, Selectable):
#--- Properties
@property
def footer(self):
"""If set, a row that always stay at the bottom of the table.
:class:`Row`. *get/set*.
When set to something else than ``None``, ``header`` and ``footer`` represent rows that will
always be kept in first and/or last position, regardless of sorting. ``len()`` and indexing
will include them, which means that if there's a header, ``table[0]`` returns it and if
there's a footer, ``table[-1]`` returns it. To make things short, all list-like functions
work with header and footer "on". But things get fuzzy for ``append()`` and ``insert()``
because these will ensure that no "normal" row gets inserted before the header or after the
footer.
Adding and removing footer here and there might seem (and is) hackish, but it's much simpler
than the alternative (when, of course, you need such a feature), which is to override magic
methods and adjust the results. When we do that, there the slice stuff that we have to
implement and it gets quite complex. Moreover, the most frequent operation on a table is
``__getitem__``, and making checks to know whether the key is a header or footer at each
call would make that operation, which is the most used, slower.
"""
return self._footer
@footer.setter
@ -154,6 +135,10 @@ class Table(MutableSequence, Selectable):
@property
def header(self):
"""If set, a row that always stay at the bottom of the table.
See :attr:`footer` for details.
"""
return self._header
@header.setter
@ -166,6 +151,10 @@ class Table(MutableSequence, Selectable):
@property
def row_count(self):
"""Number or rows in the table (without counting header and footer).
*int*. *read-only*.
"""
result = len(self)
if self._footer is not None:
result -= 1
@ -175,6 +164,10 @@ class Table(MutableSequence, Selectable):
@property
def rows(self):
"""List of rows in the table, excluding header and footer.
List of :class:`Row`. *read-only*.
"""
start = None
end = None
if self._footer is not None:
@ -185,6 +178,13 @@ class Table(MutableSequence, Selectable):
@property
def selected_row(self):
"""Selected row according to :attr:`~hscommon.gui.selectable_list.Selectable.selected_index`.
:class:`Row`. *get/set*.
When setting this attribute, we look up the index of the row and set the selected index from
there. If the row isn't in the list, selection isn't changed.
"""
return self[self.selected_index] if self.selected_index is not None else None
@selected_row.setter
@ -196,35 +196,114 @@ class Table(MutableSequence, Selectable):
@property
def selected_rows(self):
"""List of selected rows based on :attr:`~hscommon.gui.selectable_list.Selectable.selected_indexes`.
List of :class:`Row`. *read-only*.
"""
return [self[index] for index in self.selected_indexes]
class GUITableView:
"""Expected interface for :class:`GUITable`'s view.
*Not actually used in the code. For documentation purposes only.*
Our view, some kind of table view, is expected to sync with the table's contents by
appropriately behave to all callbacks in this interface.
When in edit mode, the content types by the user is expected to be sent as soon as possible
to the :class:`Row`.
Whenever the user changes the selection, we expect the view to call :meth:`Table.select`.
"""
def refresh(self):
"""Refreshes the contents of the table widget.
Ensures that the contents of the table widget is synced with the model. This includes
selection.
"""
def start_editing(self):
"""Start editing the currently selected row.
Begin whatever inline editing support that the view supports.
"""
def stop_editing(self):
"""Stop editing if there's an inline editing in effect.
There's no "aborting" implied in this call, so it's appropriate to send whatever the user
has typed and might not have been sent down to the :class:`Row` yet. After you've done that,
stop the editing mechanism.
"""
SortDescriptor = namedtuple('SortDescriptor', 'column desc')
class GUITable(Table, GUIObject):
"""Cross-toolkit GUI-enabled table view.
Represents a UI element presenting the user with a sortable, selectable, possibly editable,
table view.
Behaves like the :class:`Table` which it subclasses, but is more focused on being the presenter
of some model data to its :attr:`~hscommon.gui.base.GUIObject.view`. There's a :meth:`refresh`
mechanism which ensures fresh data while preserving sorting order and selection. There's also an
editing mechanism which tracks whether (and which) row is being edited (or added) and
save/cancel edits when appropriate.
Subclasses :class:`Table` and :class:`~hscommon.gui.base.GUIObject`. Expected view:
:class:`GUITableView`.
"""
def __init__(self):
GUIObject.__init__(self)
Table.__init__(self)
#: The row being currently edited by the user. ``None`` if no edit is taking place.
self.edited = None
self._sort_descriptor = None
#--- Virtual
def _do_add(self):
# Creates a new row, adds it in the table and returns (row, insert_index)
"""(Virtual) Creates a new row, adds it in the table.
Returns ``(row, insert_index)``.
"""
raise NotImplementedError()
def _do_delete(self):
# Delete the selected rows
"""(Virtual) Delete the selected rows.
"""
pass
def _fill(self):
# Called by refresh()
# Fills the table with all the rows that this table is supposed to have.
"""(Virtual/Required) Fills the table with all the rows that this table is supposed to have.
Called by :meth:`refresh`. Does nothing by default.
"""
pass
def _is_edited_new(self):
"""(Virtual) Returns whether the currently edited row should be considered "new".
This is used in :meth:`cancel_edits` to know whether the cancellation of the edit means a
revert of the row's value or the removal of the row.
By default, always false.
"""
return False
def _restore_selection(self, previous_selection):
"""(Virtual) Restores row selection after a contents-changing operation.
Before each contents changing operation, we store our previously selected indexes because in
many cases, such as in :meth:`refresh`, our selection will be lost. After the operation is
over, we call this method with our previously selected indexes (in ``previous_selection``).
The default behavior is (if we indeed have an empty
:attr:`~hscommon.gui.selectable_list.Selectable.selected_indexes`) to re-select
``previous_selection``. If it was empty, we select the last row of the table.
This behavior can, of course, be overriden.
"""
if not self.selected_indexes:
if previous_selection:
self.select(previous_selection)
@ -233,6 +312,11 @@ class GUITable(Table, GUIObject):
#--- Public
def add(self):
"""Add a new row in edit mode.
Requires :meth:`do_add` to be implemented. The newly added row will be selected and in edit
mode.
"""
self.view.stop_editing()
if self.edited is not None:
self.save_edits()
@ -244,13 +328,22 @@ class GUITable(Table, GUIObject):
self.view.start_editing()
def can_edit_cell(self, column_name, row_index):
# A row is, by default, editable as soon as it has an attr with the same name as `column`.
# If can_edit() returns False, the row is not editable at all. You can set editability of
# rows at the attribute level with can_edit_* properties
"""Returns whether the cell at ``row_index`` and ``column_name`` can be edited.
A row is, by default, editable as soon as it has an attr with the same name as `column`.
If :meth:`Row.can_edit` returns False, the row is not editable at all. You can set
editability of rows at the attribute level with can_edit_* properties.
Mostly just a shortcut to :meth:`Row.can_edit_cell`.
"""
row = self[row_index]
return row.can_edit_cell(column_name)
def cancel_edits(self):
"""Cancels the current edit operation.
If there's an :attr:`edited` row, it will be re-initialized (with :meth:`Row.load`).
"""
if self.edited is None:
return
self.view.stop_editing()
@ -265,6 +358,11 @@ class GUITable(Table, GUIObject):
self.view.refresh()
def delete(self):
"""Delete the currently selected rows.
Requires :meth:`_do_delete` for this to have any effect on the model. Cancels editing if
relevant.
"""
self.view.stop_editing()
if self.edited is not None:
self.cancel_edits()
@ -273,6 +371,16 @@ class GUITable(Table, GUIObject):
self._do_delete()
def refresh(self, refresh_view=True):
"""Empty the table and re-create its rows.
:meth:`_fill` is called after we emptied the table to create our rows. Previous sort order
will be preserved, regardless of the order in which the rows were filled. If there was any
edit operation taking place, it's cancelled.
:param bool refresh_view: Whether we tell our view to refresh after our refill operation.
Most of the time, it's what we want, but there's some cases where
we don't.
"""
self.cancel_edits()
previous_selection = self.selected_indexes
del self[:]
@ -285,6 +393,10 @@ class GUITable(Table, GUIObject):
self.view.refresh()
def save_edits(self):
"""Commit user edits to the model.
This is done by calling :meth:`Row.save`.
"""
if self.edited is None:
return
row = self.edited
@ -292,6 +404,15 @@ class GUITable(Table, GUIObject):
row.save()
def sort_by(self, column_name, desc=False):
"""Sort table by ``column_name``.
Overrides :meth:`Table.sort_by`. After having performed sorting, calls
:meth:`~hscommon.gui.selectable_list.Selectable._update_selection` to give you the chance,
if appropriate, to update your selected indexes according to, maybe, the selection that you
have in your model.
Then, we refresh our view.
"""
Table.sort_by(self, column_name=column_name, desc=desc)
self._sort_descriptor = SortDescriptor(column_name, desc)
self._update_selection()

View File

@ -33,18 +33,6 @@ class TextField(GUIObject):
us to directly retrieve/set our non-string value through :attr:`value`.
Subclasses :class:`hscommon.gui.base.GUIObject`. Expected view: :class:`TextFieldView`.
.. attribute:: text
*str*. The text that is currently displayed in the widget. This property can be set. When it
is, :meth:`refresh` is called and the view is synced with our value. Always in sync with
:attr:`value`.
.. attribute:: value
The "parsed" representation of :attr:`text`. By default, it's a mirror of :attr:`text`, but
a subclass can override :meth:`_parse` and :meth:`_format` to have anything else. Always in
sync with :attr:`text`.
"""
def __init__(self):
GUIObject.__init__(self)
@ -85,6 +73,13 @@ class TextField(GUIObject):
@property
def text(self):
"""The text that is currently displayed in the widget.
*str*. *get/set*.
This property can be set. When it is, :meth:`refresh` is called and the view is synced with
our value. Always in sync with :attr:`value`.
"""
return self._text
@text.setter
@ -93,6 +88,13 @@ class TextField(GUIObject):
@property
def value(self):
"""The "parsed" representation of :attr:`text`.
*arbitrary type*. *get/set*.
By default, it's a mirror of :attr:`text`, but a subclass can override :meth:`_parse` and
:meth:`_format` to have anything else. Always in sync with :attr:`text`.
"""
return self._value
@value.setter