Additional type hints in hscommon

This commit is contained in:
Andrew Senetar 2022-05-11 00:50:34 -05:00
parent 7865e4aeac
commit d5eeab4a17
Signed by: arsenetar
GPG Key ID: C63300DCE48AB2F1
6 changed files with 133 additions and 111 deletions

View File

@ -36,11 +36,11 @@ class GUIObject:
``multibind`` flag to ``True`` and the safeguard will be disabled. ``multibind`` flag to ``True`` and the safeguard will be disabled.
""" """
def __init__(self, multibind=False): def __init__(self, multibind: bool = False) -> None:
self._view = None self._view = None
self._multibind = multibind self._multibind = multibind
def _view_updated(self): def _view_updated(self) -> None:
"""(Virtual) Called after :attr:`view` has been set. """(Virtual) Called after :attr:`view` has been set.
Doing nothing by default, this method is called after :attr:`view` has been set (it isn't Doing nothing by default, this method is called after :attr:`view` has been set (it isn't
@ -48,7 +48,7 @@ class GUIObject:
(which is often the whole of the initialization code). (which is often the whole of the initialization code).
""" """
def has_view(self): def has_view(self) -> bool:
return (self._view is not None) and (not isinstance(self._view, NoopGUI)) return (self._view is not None) and (not isinstance(self._view, NoopGUI))
@property @property
@ -67,7 +67,7 @@ class GUIObject:
return self._view return self._view
@view.setter @view.setter
def view(self, value): def view(self, value) -> None:
if self._view is None and value is None: if self._view is None and value is None:
# Initial view assignment # Initial view assignment
return return

View File

@ -7,8 +7,10 @@
# http://www.gnu.org/licenses/gpl-3.0.html # http://www.gnu.org/licenses/gpl-3.0.html
import copy import copy
from typing import Any, List, Tuple, Union
from hscommon.gui.base import GUIObject from hscommon.gui.base import GUIObject
from hscommon.gui.table import GUITable
class Column: class Column:
@ -17,7 +19,7 @@ class Column:
These attributes are then used to correctly configure the column on the "view" side. These attributes are then used to correctly configure the column on the "view" side.
""" """
def __init__(self, name, display="", visible=True, optional=False): def __init__(self, name: str, display: str = "", visible: bool = True, optional: bool = False) -> None:
#: "programmatical" (not for display) name. Used as a reference in a couple of place, such #: "programmatical" (not for display) name. Used as a reference in a couple of place, such
#: as :meth:`Columns.column_by_name`. #: as :meth:`Columns.column_by_name`.
self.name = name self.name = name
@ -52,14 +54,14 @@ class ColumnsView:
callbacks. callbacks.
""" """
def restore_columns(self): def restore_columns(self) -> None:
"""Update all columns according to the model. """Update all columns according to the model.
When this is called, our view has to update the columns title, order and visibility of all When this is called, our view has to update the columns title, order and visibility of all
columns. columns.
""" """
def set_column_visible(self, colname, visible): def set_column_visible(self, colname: str, visible: bool) -> None:
"""Update visibility of column ``colname``. """Update visibility of column ``colname``.
Called when the user toggles the visibility of a column, we must update the column Called when the user toggles the visibility of a column, we must update the column
@ -73,13 +75,13 @@ class PrefAccessInterface:
*Not actually used in the code. For documentation purposes only.* *Not actually used in the code. For documentation purposes only.*
""" """
def get_default(self, key, fallback_value): def get_default(self, key: str, fallback_value: Union[Any, None]) -> Any:
"""Retrieve the value for ``key`` in the currently running app's preference store. """Retrieve the value for ``key`` in the currently running app's preference store.
If the key doesn't exist, return ``fallback_value``. If the key doesn't exist, return ``fallback_value``.
""" """
def set_default(self, key, value): def set_default(self, key: str, value: Any) -> None:
"""Set the value ``value`` for ``key`` in the currently running app's preference store.""" """Set the value ``value`` for ``key`` in the currently running app's preference store."""
@ -104,65 +106,65 @@ class Columns(GUIObject):
have that same prefix. have that same prefix.
""" """
def __init__(self, table, prefaccess=None, savename=None): def __init__(self, table: GUITable, prefaccess=None, savename: Union[str, None] = None):
GUIObject.__init__(self) GUIObject.__init__(self)
self.table = table self.table = table
self.prefaccess = prefaccess self.prefaccess = prefaccess
self.savename = savename self.savename = savename
# We use copy here for test isolation. If we don't, changing a column affects all tests. # We use copy here for test isolation. If we don't, changing a column affects all tests.
self.column_list = list(map(copy.copy, table.COLUMNS)) self.column_list: List[Column] = list(map(copy.copy, table.COLUMNS))
for i, column in enumerate(self.column_list): for i, column in enumerate(self.column_list):
column.logical_index = i column.logical_index = i
column.ordered_index = i column.ordered_index = i
self.coldata = {col.name: col for col in self.column_list} self.coldata = {col.name: col for col in self.column_list}
# --- Private # --- Private
def _get_colname_attr(self, colname, attrname, default): def _get_colname_attr(self, colname: str, attrname: str, default: Any) -> Any:
try: try:
return getattr(self.coldata[colname], attrname) return getattr(self.coldata[colname], attrname)
except KeyError: except KeyError:
return default return default
def _set_colname_attr(self, colname, attrname, value): def _set_colname_attr(self, colname: str, attrname: str, value: Any) -> None:
try: try:
col = self.coldata[colname] col = self.coldata[colname]
setattr(col, attrname, value) setattr(col, attrname, value)
except KeyError: except KeyError:
pass pass
def _optional_columns(self): def _optional_columns(self) -> List[Column]:
return [c for c in self.column_list if c.optional] return [c for c in self.column_list if c.optional]
# --- Override # --- Override
def _view_updated(self): def _view_updated(self) -> None:
self.restore_columns() self.restore_columns()
# --- Public # --- Public
def column_by_index(self, index): def column_by_index(self, index: int):
"""Return the :class:`Column` having the :attr:`~Column.logical_index` ``index``.""" """Return the :class:`Column` having the :attr:`~Column.logical_index` ``index``."""
return self.column_list[index] return self.column_list[index]
def column_by_name(self, name): def column_by_name(self, name: str):
"""Return the :class:`Column` having the :attr:`~Column.name` ``name``.""" """Return the :class:`Column` having the :attr:`~Column.name` ``name``."""
return self.coldata[name] return self.coldata[name]
def columns_count(self): def columns_count(self) -> int:
"""Returns the number of columns in our set.""" """Returns the number of columns in our set."""
return len(self.column_list) return len(self.column_list)
def column_display(self, colname): def column_display(self, colname: str) -> str:
"""Returns display name for column named ``colname``, or ``''`` if there's none.""" """Returns display name for column named ``colname``, or ``''`` if there's none."""
return self._get_colname_attr(colname, "display", "") return self._get_colname_attr(colname, "display", "")
def column_is_visible(self, colname): def column_is_visible(self, colname: str) -> bool:
"""Returns visibility for column named ``colname``, or ``True`` if there's none.""" """Returns visibility for column named ``colname``, or ``True`` if there's none."""
return self._get_colname_attr(colname, "visible", True) return self._get_colname_attr(colname, "visible", True)
def column_width(self, colname): def column_width(self, colname: str) -> int:
"""Returns width for column named ``colname``, or ``0`` if there's none.""" """Returns width for column named ``colname``, or ``0`` if there's none."""
return self._get_colname_attr(colname, "width", 0) return self._get_colname_attr(colname, "width", 0)
def columns_to_right(self, colname): def columns_to_right(self, colname: str) -> List[str]:
"""Returns the list of all columns to the right of ``colname``. """Returns the list of all columns to the right of ``colname``.
"right" meaning "having a higher :attr:`Column.ordered_index`" in our left-to-right "right" meaning "having a higher :attr:`Column.ordered_index`" in our left-to-right
@ -172,7 +174,7 @@ class Columns(GUIObject):
index = column.ordered_index index = column.ordered_index
return [col.name for col in self.column_list if (col.visible and col.ordered_index > index)] return [col.name for col in self.column_list if (col.visible and col.ordered_index > index)]
def menu_items(self): def menu_items(self) -> List[Tuple[str, bool]]:
"""Returns a list of items convenient for quick visibility menu generation. """Returns a list of items convenient for quick visibility menu generation.
Returns a list of ``(display_name, is_marked)`` items for each optional column in the Returns a list of ``(display_name, is_marked)`` items for each optional column in the
@ -184,7 +186,7 @@ class Columns(GUIObject):
""" """
return [(c.display, c.visible) for c in self._optional_columns()] return [(c.display, c.visible) for c in self._optional_columns()]
def move_column(self, colname, index): def move_column(self, colname: str, index: int) -> None:
"""Moves column ``colname`` to ``index``. """Moves column ``colname`` to ``index``.
The column will be placed just in front of the column currently having that index, or to the The column will be placed just in front of the column currently having that index, or to the
@ -195,7 +197,7 @@ class Columns(GUIObject):
colnames.insert(index, colname) colnames.insert(index, colname)
self.set_column_order(colnames) self.set_column_order(colnames)
def reset_to_defaults(self): def reset_to_defaults(self) -> None:
"""Reset all columns' width and visibility to their default values.""" """Reset all columns' width and visibility to their default values."""
self.set_column_order([col.name for col in self.column_list]) self.set_column_order([col.name for col in self.column_list])
for col in self._optional_columns(): for col in self._optional_columns():
@ -203,11 +205,11 @@ class Columns(GUIObject):
col.width = col.default_width col.width = col.default_width
self.view.restore_columns() self.view.restore_columns()
def resize_column(self, colname, newwidth): def resize_column(self, colname: str, newwidth: int) -> None:
"""Set column ``colname``'s width to ``newwidth``.""" """Set column ``colname``'s width to ``newwidth``."""
self._set_colname_attr(colname, "width", newwidth) self._set_colname_attr(colname, "width", newwidth)
def restore_columns(self): def restore_columns(self) -> None:
"""Restore's column persistent attributes from the last :meth:`save_columns`.""" """Restore's column persistent attributes from the last :meth:`save_columns`."""
if not (self.prefaccess and self.savename and self.coldata): if not (self.prefaccess and self.savename and self.coldata):
if (not self.savename) and (self.coldata): if (not self.savename) and (self.coldata):
@ -226,7 +228,7 @@ class Columns(GUIObject):
col.visible = coldata["visible"] col.visible = coldata["visible"]
self.view.restore_columns() self.view.restore_columns()
def save_columns(self): def save_columns(self) -> None:
"""Save column attributes in persistent storage for restoration in :meth:`restore_columns`.""" """Save column attributes in persistent storage for restoration in :meth:`restore_columns`."""
if not (self.prefaccess and self.savename and self.coldata): if not (self.prefaccess and self.savename and self.coldata):
return return
@ -237,7 +239,8 @@ class Columns(GUIObject):
coldata["visible"] = col.visible coldata["visible"] = col.visible
self.prefaccess.set_default(pref_name, coldata) self.prefaccess.set_default(pref_name, coldata)
def set_column_order(self, colnames): # TODO annotate colnames
def set_column_order(self, colnames) -> None:
"""Change the columns order so it matches the order in ``colnames``. """Change the columns order so it matches the order in ``colnames``.
:param colnames: A list of column names in the desired order. :param colnames: A list of column names in the desired order.
@ -247,17 +250,17 @@ class Columns(GUIObject):
col = self.coldata[colname] col = self.coldata[colname]
col.ordered_index = i col.ordered_index = i
def set_column_visible(self, colname, visible): def set_column_visible(self, colname: str, visible: bool) -> None:
"""Set the visibility of column ``colname``.""" """Set the visibility of column ``colname``."""
self.table.save_edits() # the table on the GUI side will stop editing when the columns change self.table.save_edits() # the table on the GUI side will stop editing when the columns change
self._set_colname_attr(colname, "visible", visible) self._set_colname_attr(colname, "visible", visible)
self.view.set_column_visible(colname, visible) self.view.set_column_visible(colname, visible)
def set_default_width(self, colname, width): def set_default_width(self, colname: str, width: int) -> None:
"""Set the default width or column ``colname``.""" """Set the default width or column ``colname``."""
self._set_colname_attr(colname, "default_width", width) self._set_colname_attr(colname, "default_width", width)
def toggle_menu_item(self, index): def toggle_menu_item(self, index: int) -> bool:
"""Toggles the visibility of an optional column. """Toggles the visibility of an optional column.
You know, that optional column menu you've generated in :meth:`menu_items`? Well, ``index`` You know, that optional column menu you've generated in :meth:`menu_items`? Well, ``index``
@ -271,11 +274,11 @@ class Columns(GUIObject):
# --- Properties # --- Properties
@property @property
def ordered_columns(self): def ordered_columns(self) -> List[Column]:
"""List of :class:`Column` in visible order.""" """List of :class:`Column` in visible order."""
return [col for col in sorted(self.column_list, key=lambda col: col.ordered_index)] return [col for col in sorted(self.column_list, key=lambda col: col.ordered_index)]
@property @property
def colnames(self): def colnames(self) -> List[str]:
"""List of column names in visible order.""" """List of column names in visible order."""
return [col.name for col in self.ordered_columns] return [col.name for col in self.ordered_columns]

View File

@ -4,6 +4,7 @@
# which should be included with this package. The terms are also available at # which should be included with this package. The terms are also available at
# http://www.gnu.org/licenses/gpl-3.0.html # http://www.gnu.org/licenses/gpl-3.0.html
from typing import Callable, Tuple, Union
from hscommon.jobprogress.performer import ThreadedJobPerformer from hscommon.jobprogress.performer import ThreadedJobPerformer
from hscommon.gui.base import GUIObject from hscommon.gui.base import GUIObject
from hscommon.gui.text_field import TextField from hscommon.gui.text_field import TextField
@ -20,13 +21,13 @@ class ProgressWindowView:
It's also expected to call :meth:`ProgressWindow.cancel` when the cancel button is clicked. It's also expected to call :meth:`ProgressWindow.cancel` when the cancel button is clicked.
""" """
def show(self): def show(self) -> None:
"""Show the dialog.""" """Show the dialog."""
def close(self): def close(self) -> None:
"""Close the dialog.""" """Close the dialog."""
def set_progress(self, progress): def set_progress(self, progress: int) -> None:
"""Set the progress of the progress bar to ``progress``. """Set the progress of the progress bar to ``progress``.
Not all jobs are equally responsive on their job progress report and it is recommended that Not all jobs are equally responsive on their job progress report and it is recommended that
@ -60,7 +61,11 @@ class ProgressWindow(GUIObject, ThreadedJobPerformer):
called as if the job terminated normally. called as if the job terminated normally.
""" """
def __init__(self, finish_func, error_func=None): def __init__(
self,
finish_func: Callable[[Union[str, None]], None],
error_func: Callable[[Union[str, None], Exception], bool] = None,
) -> None:
# finish_func(jobid) is the function that is called when a job is completed. # finish_func(jobid) is the function that is called when a job is completed.
GUIObject.__init__(self) GUIObject.__init__(self)
ThreadedJobPerformer.__init__(self) ThreadedJobPerformer.__init__(self)
@ -71,9 +76,9 @@ class ProgressWindow(GUIObject, ThreadedJobPerformer):
#: :class:`.TextField`. It contains the job textual update that the function might yield #: :class:`.TextField`. It contains the job textual update that the function might yield
#: during its course. #: during its course.
self.progressdesc_textfield = TextField() self.progressdesc_textfield = TextField()
self.jobid = None self.jobid: Union[str, None] = None
def cancel(self): def cancel(self) -> None:
"""Call for a user-initiated job cancellation.""" """Call for a user-initiated job cancellation."""
# The UI is sometimes a bit buggy and calls cancel() on self.view.close(). We just want to # The UI is sometimes a bit buggy and calls cancel() on self.view.close(). We just want to
# make sure that this doesn't lead us to think that the user acually cancelled the task, so # make sure that this doesn't lead us to think that the user acually cancelled the task, so
@ -81,7 +86,7 @@ class ProgressWindow(GUIObject, ThreadedJobPerformer):
if self._job_running: if self._job_running:
self.job_cancelled = True self.job_cancelled = True
def pulse(self): def pulse(self) -> None:
"""Update progress reports in the GUI. """Update progress reports in the GUI.
Call this regularly from the GUI main run loop. The values might change before Call this regularly from the GUI main run loop. The values might change before
@ -111,7 +116,7 @@ class ProgressWindow(GUIObject, ThreadedJobPerformer):
self.progressdesc_textfield.text = last_desc self.progressdesc_textfield.text = last_desc
self.view.set_progress(last_progress) self.view.set_progress(last_progress)
def run(self, jobid, title, target, args=()): def run(self, jobid: str, title: str, target: Callable, args: Tuple = ()):
"""Starts a threaded job. """Starts a threaded job.
The ``target`` function will be sent, as its first argument, a :class:`.Job` instance which The ``target`` function will be sent, as its first argument, a :class:`.Job` instance which

View File

@ -8,6 +8,7 @@
from collections.abc import MutableSequence from collections.abc import MutableSequence
from collections import namedtuple from collections import namedtuple
from typing import Any, List, Tuple, Union
from hscommon.gui.base import GUIObject from hscommon.gui.base import GUIObject
from hscommon.gui.selectable_list import Selectable from hscommon.gui.selectable_list import Selectable
@ -27,12 +28,16 @@ class Table(MutableSequence, Selectable):
Subclasses :class:`.Selectable`. Subclasses :class:`.Selectable`.
""" """
def __init__(self): # Should be List[Column], but have circular import...
Selectable.__init__(self) COLUMNS: List = []
self._rows = []
self._header = None
self._footer = None
def __init__(self) -> None:
Selectable.__init__(self)
self._rows: List["Row"] = []
self._header: Union["Row", None] = None
self._footer: Union["Row", None] = None
# TODO type hint for key
def __delitem__(self, key): def __delitem__(self, key):
self._rows.__delitem__(key) self._rows.__delitem__(key)
if self._header is not None and ((not self) or (self[0] is not self._header)): if self._header is not None and ((not self) or (self[0] is not self._header)):
@ -41,16 +46,18 @@ class Table(MutableSequence, Selectable):
self._footer = None self._footer = None
self._check_selection_range() self._check_selection_range()
def __getitem__(self, key): # TODO type hint for key
def __getitem__(self, key) -> Any:
return self._rows.__getitem__(key) return self._rows.__getitem__(key)
def __len__(self): def __len__(self) -> int:
return len(self._rows) return len(self._rows)
def __setitem__(self, key, value): # TODO type hint for key
def __setitem__(self, key, value: Any) -> None:
self._rows.__setitem__(key, value) self._rows.__setitem__(key, value)
def append(self, item): def append(self, item: "Row") -> None:
"""Appends ``item`` at the end of the table. """Appends ``item`` at the end of the table.
If there's a footer, the item is inserted before it. If there's a footer, the item is inserted before it.
@ -60,7 +67,7 @@ class Table(MutableSequence, Selectable):
else: else:
self._rows.append(item) self._rows.append(item)
def insert(self, index, item): def insert(self, index: int, item: "Row") -> None:
"""Inserts ``item`` at ``index`` in the table. """Inserts ``item`` at ``index`` in the table.
If there's a header, will make sure we don't insert before it, and if there's a footer, will If there's a header, will make sure we don't insert before it, and if there's a footer, will
@ -72,7 +79,7 @@ class Table(MutableSequence, Selectable):
index = len(self) - 1 index = len(self) - 1
self._rows.insert(index, item) self._rows.insert(index, item)
def remove(self, row): def remove(self, row: "Row") -> None:
"""Removes ``row`` from table. """Removes ``row`` from table.
If ``row`` is a header or footer, that header or footer will be set to ``None``. If ``row`` is a header or footer, that header or footer will be set to ``None``.
@ -84,7 +91,7 @@ class Table(MutableSequence, Selectable):
self._rows.remove(row) self._rows.remove(row)
self._check_selection_range() self._check_selection_range()
def sort_by(self, column_name, desc=False): def sort_by(self, column_name: str, desc: bool = False) -> None:
"""Sort table by ``column_name``. """Sort table by ``column_name``.
Sort key for each row is computed from :meth:`Row.sort_key_for_column`. Sort key for each row is computed from :meth:`Row.sort_key_for_column`.
@ -105,7 +112,7 @@ class Table(MutableSequence, Selectable):
# --- Properties # --- Properties
@property @property
def footer(self): def footer(self) -> Union["Row", None]:
"""If set, a row that always stay at the bottom of the table. """If set, a row that always stay at the bottom of the table.
:class:`Row`. *get/set*. :class:`Row`. *get/set*.
@ -128,7 +135,7 @@ class Table(MutableSequence, Selectable):
return self._footer return self._footer
@footer.setter @footer.setter
def footer(self, value): def footer(self, value: Union["Row", None]) -> None:
if self._footer is not None: if self._footer is not None:
self._rows.pop() self._rows.pop()
if value is not None: if value is not None:
@ -136,7 +143,7 @@ class Table(MutableSequence, Selectable):
self._footer = value self._footer = value
@property @property
def header(self): def header(self) -> Union["Row", None]:
"""If set, a row that always stay at the bottom of the table. """If set, a row that always stay at the bottom of the table.
See :attr:`footer` for details. See :attr:`footer` for details.
@ -144,7 +151,7 @@ class Table(MutableSequence, Selectable):
return self._header return self._header
@header.setter @header.setter
def header(self, value): def header(self, value: Union["Row", None]) -> None:
if self._header is not None: if self._header is not None:
self._rows.pop(0) self._rows.pop(0)
if value is not None: if value is not None:
@ -152,7 +159,7 @@ class Table(MutableSequence, Selectable):
self._header = value self._header = value
@property @property
def row_count(self): def row_count(self) -> int:
"""Number or rows in the table (without counting header and footer). """Number or rows in the table (without counting header and footer).
*int*. *read-only*. *int*. *read-only*.
@ -165,7 +172,7 @@ class Table(MutableSequence, Selectable):
return result return result
@property @property
def rows(self): def rows(self) -> List["Row"]:
"""List of rows in the table, excluding header and footer. """List of rows in the table, excluding header and footer.
List of :class:`Row`. *read-only*. List of :class:`Row`. *read-only*.
@ -179,7 +186,7 @@ class Table(MutableSequence, Selectable):
return self[start:end] return self[start:end]
@property @property
def selected_row(self): def selected_row(self) -> "Row":
"""Selected row according to :attr:`Selectable.selected_index`. """Selected row according to :attr:`Selectable.selected_index`.
:class:`Row`. *get/set*. :class:`Row`. *get/set*.
@ -190,14 +197,14 @@ class Table(MutableSequence, Selectable):
return self[self.selected_index] if self.selected_index is not None else None return self[self.selected_index] if self.selected_index is not None else None
@selected_row.setter @selected_row.setter
def selected_row(self, value): def selected_row(self, value: int) -> None:
try: try:
self.selected_index = self.index(value) self.selected_index = self.index(value)
except ValueError: except ValueError:
pass pass
@property @property
def selected_rows(self): def selected_rows(self) -> List["Row"]:
"""List of selected rows based on :attr:`.selected_indexes`. """List of selected rows based on :attr:`.selected_indexes`.
List of :class:`Row`. *read-only*. List of :class:`Row`. *read-only*.
@ -219,20 +226,20 @@ class GUITableView:
Whenever the user changes the selection, we expect the view to call :meth:`Table.select`. Whenever the user changes the selection, we expect the view to call :meth:`Table.select`.
""" """
def refresh(self): def refresh(self) -> None:
"""Refreshes the contents of the table widget. """Refreshes the contents of the table widget.
Ensures that the contents of the table widget is synced with the model. This includes Ensures that the contents of the table widget is synced with the model. This includes
selection. selection.
""" """
def start_editing(self): def start_editing(self) -> None:
"""Start editing the currently selected row. """Start editing the currently selected row.
Begin whatever inline editing support that the view supports. Begin whatever inline editing support that the view supports.
""" """
def stop_editing(self): def stop_editing(self) -> None:
"""Stop editing if there's an inline editing in effect. """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 There's no "aborting" implied in this call, so it's appropriate to send whatever the user
@ -260,33 +267,33 @@ class GUITable(Table, GUIObject):
:class:`GUITableView`. :class:`GUITableView`.
""" """
def __init__(self): def __init__(self) -> None:
GUIObject.__init__(self) GUIObject.__init__(self)
Table.__init__(self) Table.__init__(self)
#: The row being currently edited by the user. ``None`` if no edit is taking place. #: The row being currently edited by the user. ``None`` if no edit is taking place.
self.edited = None self.edited: Union["Row", None] = None
self._sort_descriptor = None self._sort_descriptor: Union[SortDescriptor, None] = None
# --- Virtual # --- Virtual
def _do_add(self): def _do_add(self) -> Tuple["Row", int]:
"""(Virtual) Creates a new row, adds it in the table. """(Virtual) Creates a new row, adds it in the table.
Returns ``(row, insert_index)``. Returns ``(row, insert_index)``.
""" """
raise NotImplementedError() raise NotImplementedError()
def _do_delete(self): def _do_delete(self) -> None:
"""(Virtual) Delete the selected rows.""" """(Virtual) Delete the selected rows."""
pass pass
def _fill(self): def _fill(self) -> None:
"""(Virtual/Required) 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. Called by :meth:`refresh`. Does nothing by default.
""" """
pass pass
def _is_edited_new(self): def _is_edited_new(self) -> bool:
"""(Virtual) Returns whether the currently edited row should be considered "new". """(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 This is used in :meth:`cancel_edits` to know whether the cancellation of the edit means a
@ -315,7 +322,7 @@ class GUITable(Table, GUIObject):
self.select([len(self) - 1]) self.select([len(self) - 1])
# --- Public # --- Public
def add(self): def add(self) -> None:
"""Add a new row in edit mode. """Add a new row in edit mode.
Requires :meth:`do_add` to be implemented. The newly added row will be selected and in edit Requires :meth:`do_add` to be implemented. The newly added row will be selected and in edit
@ -334,7 +341,7 @@ class GUITable(Table, GUIObject):
self.edited = row self.edited = row
self.view.start_editing() self.view.start_editing()
def can_edit_cell(self, column_name, row_index): def can_edit_cell(self, column_name: str, row_index: int) -> bool:
"""Returns whether the cell at ``row_index`` and ``column_name`` can be edited. """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`. A row is, by default, editable as soon as it has an attr with the same name as `column`.
@ -346,7 +353,7 @@ class GUITable(Table, GUIObject):
row = self[row_index] row = self[row_index]
return row.can_edit_cell(column_name) return row.can_edit_cell(column_name)
def cancel_edits(self): def cancel_edits(self) -> None:
"""Cancels the current edit operation. """Cancels the current edit operation.
If there's an :attr:`edited` row, it will be re-initialized (with :meth:`Row.load`). If there's an :attr:`edited` row, it will be re-initialized (with :meth:`Row.load`).
@ -364,7 +371,7 @@ class GUITable(Table, GUIObject):
self.edited = None self.edited = None
self.view.refresh() self.view.refresh()
def delete(self): def delete(self) -> None:
"""Delete the currently selected rows. """Delete the currently selected rows.
Requires :meth:`_do_delete` for this to have any effect on the model. Cancels editing if Requires :meth:`_do_delete` for this to have any effect on the model. Cancels editing if
@ -377,7 +384,7 @@ class GUITable(Table, GUIObject):
if self: if self:
self._do_delete() self._do_delete()
def refresh(self, refresh_view=True): def refresh(self, refresh_view: bool = True) -> None:
"""Empty the table and re-create its rows. """Empty the table and re-create its rows.
:meth:`_fill` is called after we emptied the table to create our rows. Previous sort order :meth:`_fill` is called after we emptied the table to create our rows. Previous sort order
@ -399,7 +406,7 @@ class GUITable(Table, GUIObject):
if refresh_view: if refresh_view:
self.view.refresh() self.view.refresh()
def save_edits(self): def save_edits(self) -> None:
"""Commit user edits to the model. """Commit user edits to the model.
This is done by calling :meth:`Row.save`. This is done by calling :meth:`Row.save`.
@ -410,7 +417,7 @@ class GUITable(Table, GUIObject):
self.edited = None self.edited = None
row.save() row.save()
def sort_by(self, column_name, desc=False): def sort_by(self, column_name: str, desc: bool = False) -> None:
"""Sort table by ``column_name``. """Sort table by ``column_name``.
Overrides :meth:`Table.sort_by`. After having performed sorting, calls Overrides :meth:`Table.sort_by`. After having performed sorting, calls
@ -450,18 +457,18 @@ class Row:
Of course, this is only default behavior. This can be overriden. Of course, this is only default behavior. This can be overriden.
""" """
def __init__(self, table): def __init__(self, table: GUITable) -> None:
super().__init__() super().__init__()
self.table = table self.table = table
def _edit(self): def _edit(self) -> None:
if self.table.edited is self: if self.table.edited is self:
return return
assert self.table.edited is None assert self.table.edited is None
self.table.edited = self self.table.edited = self
# --- Virtual # --- Virtual
def can_edit(self): def can_edit(self) -> bool:
"""(Virtual) Whether the whole row can be edited. """(Virtual) Whether the whole row can be edited.
By default, always returns ``True``. This is for the *whole* row. For individual cells, it's By default, always returns ``True``. This is for the *whole* row. For individual cells, it's
@ -469,7 +476,7 @@ class Row:
""" """
return True return True
def load(self): def load(self) -> None:
"""(Virtual/Required) Loads up values from the model to be presented in the table. """(Virtual/Required) Loads up values from the model to be presented in the table.
Usually, our model instances contain values that are not quite ready for display. If you Usually, our model instances contain values that are not quite ready for display. If you
@ -478,7 +485,7 @@ class Row:
""" """
raise NotImplementedError() raise NotImplementedError()
def save(self): def save(self) -> None:
"""(Virtual/Required) Saves user edits into your model. """(Virtual/Required) Saves user edits into your model.
If your table is editable, this is called when the user commits his changes. Usually, these If your table is editable, this is called when the user commits his changes. Usually, these
@ -487,7 +494,7 @@ class Row:
""" """
raise NotImplementedError() raise NotImplementedError()
def sort_key_for_column(self, column_name): def sort_key_for_column(self, column_name: str) -> Any:
"""(Virtual) Return the value that is to be used to sort by column ``column_name``. """(Virtual) Return the value that is to be used to sort by column ``column_name``.
By default, looks for an attribute with the same name as ``column_name``, but with an By default, looks for an attribute with the same name as ``column_name``, but with an
@ -500,7 +507,7 @@ class Row:
return getattr(self, column_name) return getattr(self, column_name)
# --- Public # --- Public
def can_edit_cell(self, column_name): def can_edit_cell(self, column_name: str) -> bool:
"""Returns whether cell for column ``column_name`` can be edited. """Returns whether cell for column ``column_name`` can be edited.
By the default, the check is done in many steps: By the default, the check is done in many steps:
@ -530,7 +537,7 @@ class Row:
return False return False
return bool(getattr(prop, "fset", None)) return bool(getattr(prop, "fset", None))
def get_cell_value(self, attrname): def get_cell_value(self, attrname: str) -> Any:
"""Get cell value for ``attrname``. """Get cell value for ``attrname``.
By default, does a simple ``getattr()``, but it is used to allow subclasses to have By default, does a simple ``getattr()``, but it is used to allow subclasses to have
@ -540,7 +547,7 @@ class Row:
attrname = "from_" attrname = "from_"
return getattr(self, attrname) return getattr(self, attrname)
def set_cell_value(self, attrname, value): def set_cell_value(self, attrname: str, value: Any) -> None:
"""Set cell value to ``value`` for ``attrname``. """Set cell value to ``value`` for ``attrname``.
By default, does a simple ``setattr()``, but it is used to allow subclasses to have By default, does a simple ``setattr()``, but it is used to allow subclasses to have

View File

@ -7,6 +7,9 @@
# http://www.gnu.org/licenses/gpl-3.0.html # http://www.gnu.org/licenses/gpl-3.0.html
from typing import Any, Callable, Generator, Iterator, List, Union
class JobCancelled(Exception): class JobCancelled(Exception):
"The user has cancelled the job" "The user has cancelled the job"
@ -36,7 +39,7 @@ class Job:
""" """
# ---Magic functions # ---Magic functions
def __init__(self, job_proportions, callback): def __init__(self, job_proportions: Union[List[int], int], callback: Callable) -> None:
"""Initialize the Job with 'jobcount' jobs. Start every job with """Initialize the Job with 'jobcount' jobs. Start every job with
start_job(). Every time the job progress is updated, 'callback' is called start_job(). Every time the job progress is updated, 'callback' is called
'callback' takes a 'progress' int param, and a optional 'desc' 'callback' takes a 'progress' int param, and a optional 'desc'
@ -55,12 +58,12 @@ class Job:
self._currmax = 1 self._currmax = 1
# ---Private # ---Private
def _subjob_callback(self, progress, desc=""): def _subjob_callback(self, progress: int, desc: str = "") -> bool:
"""This is the callback passed to children jobs.""" """This is the callback passed to children jobs."""
self.set_progress(progress, desc) self.set_progress(progress, desc)
return True # if JobCancelled has to be raised, it will be at the highest level return True # if JobCancelled has to be raised, it will be at the highest level
def _do_update(self, desc): def _do_update(self, desc: str) -> None:
"""Calls the callback function with a % progress as a parameter. """Calls the callback function with a % progress as a parameter.
The parameter is a int in the 0-100 range. The parameter is a int in the 0-100 range.
@ -78,13 +81,16 @@ class Job:
raise JobCancelled() raise JobCancelled()
# ---Public # ---Public
def add_progress(self, progress=1, desc=""): def add_progress(self, progress: int = 1, desc: str = "") -> None:
self.set_progress(self._progress + progress, desc) self.set_progress(self._progress + progress, desc)
def check_if_cancelled(self): def check_if_cancelled(self) -> None:
self._do_update("") self._do_update("")
def iter_with_progress(self, iterable, desc_format=None, every=1, count=None): # TODO type hint iterable
def iter_with_progress(
self, iterable, desc_format: Union[str, None] = None, every: int = 1, count: Union[int, None] = None
) -> Generator[Any, None, None]:
"""Iterate through ``iterable`` while automatically adding progress. """Iterate through ``iterable`` while automatically adding progress.
WARNING: We need our iterable's length. If ``iterable`` is not a sequence (that is, WARNING: We need our iterable's length. If ``iterable`` is not a sequence (that is,
@ -107,7 +113,7 @@ class Job:
desc = desc_format % (count, count) desc = desc_format % (count, count)
self.set_progress(100, desc) self.set_progress(100, desc)
def start_job(self, max_progress=100, desc=""): def start_job(self, max_progress: int = 100, desc: str = "") -> None:
"""Begin work on the next job. You must not call start_job more than """Begin work on the next job. You must not call start_job more than
'jobcount' (in __init__) times. 'jobcount' (in __init__) times.
'max' is the job units you are to perform. 'max' is the job units you are to perform.
@ -122,7 +128,7 @@ class Job:
self._currmax = max(1, max_progress) self._currmax = max(1, max_progress)
self._do_update(desc) self._do_update(desc)
def start_subjob(self, job_proportions, desc=""): def start_subjob(self, job_proportions: Union[List[int], int], desc: str = "") -> "Job":
"""Starts a sub job. Use this when you want to split a job into """Starts a sub job. Use this when you want to split a job into
multiple smaller jobs. Pretty handy when starting a process where you multiple smaller jobs. Pretty handy when starting a process where you
know how many subjobs you will have, but don't know the work unit count know how many subjobs you will have, but don't know the work unit count
@ -132,7 +138,7 @@ class Job:
self.start_job(100, desc) self.start_job(100, desc)
return Job(job_proportions, self._subjob_callback) return Job(job_proportions, self._subjob_callback)
def set_progress(self, progress, desc=""): def set_progress(self, progress: int, desc: str = "") -> None:
"""Sets the progress of the current job to 'progress', and call the """Sets the progress of the current job to 'progress', and call the
callback callback
""" """
@ -143,29 +149,29 @@ class Job:
class NullJob: class NullJob:
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs) -> None:
# Null job does nothing # Null job does nothing
pass pass
def add_progress(self, *args, **kwargs): def add_progress(self, *args, **kwargs) -> None:
# Null job does nothing # Null job does nothing
pass pass
def check_if_cancelled(self): def check_if_cancelled(self) -> None:
# Null job does nothing # Null job does nothing
pass pass
def iter_with_progress(self, sequence, *args, **kwargs): def iter_with_progress(self, sequence, *args, **kwargs) -> Iterator:
return iter(sequence) return iter(sequence)
def start_job(self, *args, **kwargs): def start_job(self, *args, **kwargs) -> None:
# Null job does nothing # Null job does nothing
pass pass
def start_subjob(self, *args, **kwargs): def start_subjob(self, *args, **kwargs) -> "NullJob":
return NullJob() return NullJob()
def set_progress(self, *args, **kwargs): def set_progress(self, *args, **kwargs) -> None:
# Null job does nothing # Null job does nothing
pass pass

View File

@ -8,6 +8,7 @@
from threading import Thread from threading import Thread
import sys import sys
from typing import Callable, Tuple, Union
from hscommon.jobprogress.job import Job, JobInProgressError, JobCancelled from hscommon.jobprogress.job import Job, JobInProgressError, JobCancelled
@ -28,15 +29,15 @@ class ThreadedJobPerformer:
last_error = None last_error = None
# --- Protected # --- Protected
def create_job(self): def create_job(self) -> Job:
if self._job_running: if self._job_running:
raise JobInProgressError() raise JobInProgressError()
self.last_progress = -1 self.last_progress: Union[int, None] = -1
self.last_desc = "" self.last_desc = ""
self.job_cancelled = False self.job_cancelled = False
return Job(1, self._update_progress) return Job(1, self._update_progress)
def _async_run(self, *args): def _async_run(self, *args) -> None:
target = args[0] target = args[0]
args = tuple(args[1:]) args = tuple(args[1:])
self._job_running = True self._job_running = True
@ -52,7 +53,7 @@ class ThreadedJobPerformer:
self._job_running = False self._job_running = False
self.last_progress = None self.last_progress = None
def reraise_if_error(self): def reraise_if_error(self) -> None:
"""Reraises the error that happened in the thread if any. """Reraises the error that happened in the thread if any.
Call this after the caller of run_threaded detected that self._job_running returned to False Call this after the caller of run_threaded detected that self._job_running returned to False
@ -60,13 +61,13 @@ class ThreadedJobPerformer:
if self.last_error is not None: if self.last_error is not None:
raise self.last_error.with_traceback(self.last_traceback) raise self.last_error.with_traceback(self.last_traceback)
def _update_progress(self, newprogress, newdesc=""): def _update_progress(self, newprogress: int, newdesc: str = "") -> bool:
self.last_progress = newprogress self.last_progress = newprogress
if newdesc: if newdesc:
self.last_desc = newdesc self.last_desc = newdesc
return not self.job_cancelled return not self.job_cancelled
def run_threaded(self, target, args=()): def run_threaded(self, target: Callable, args: Tuple = ()) -> None:
if self._job_running: if self._job_running:
raise JobInProgressError() raise JobInProgressError()
args = (target,) + args args = (target,) + args