Source code for ipysheet.easy

"""Easy context-based interface for generating a sheet and cells.

Comparable to matplotlib pylab interface, this interface keeps track of the current
sheet. Using the ``cell`` function, ``Cell`` widgets are added to the current sheet.
"""
__all__ = ['sheet', 'current', 'cell', 'calculation', 'row', 'column', 'cell_range', 'hold_cells', 'renderer']

import numbers
import six
from contextlib import contextmanager

import ipywidgets as widgets

from .sheet import Cell, Sheet, Renderer
from .utils import transpose as default_transpose
from .utils import adapt_value
from .docutils import doc_subst

_last_sheet = None
_sheets = {}  # maps from key to Sheet instance
_hold_cells = False  # when try (using hold_cells() it does not add cells directly)
_cells = ()  # cells that aren't added directly

_common_doc = {
    'args': """
        type (string): Type of cell, options are: text, numeric, checkbox, dropdown, numeric, date, widget.
            If type is None, the type is inferred from the type of the value being passed,
            numeric (float or int type), boolean (bool type), widget (any widget object), or else text.
            When choice is given the type will be assumed to be dropdown.
            The types refer (currently) to the handsontable types: https://handsontable.com/docs/6.2.2/demo-custom-renderers.html
        color (string): The text color in the cell
        background_color (string): The background color in the cell
        read_only (bool): Whether the cell is editable or not
        numeric_format (string): Numbers format
        date_format (string): Dates format
        time_format (string): Time format
        renderer (string): Renderer name to use for the cell
    """
}


[docs]def sheet(key=None, rows=5, columns=5, column_width=None, row_headers=True, column_headers=True, stretch_headers='all', cls=Sheet, **kwargs): """Creates a new ``Sheet`` instance or retrieves one registered with key, and sets this as the 'current'. If the key argument is given, and no sheet is created before with this key, it will be registered under this key. If this function is called again with the same key argument, that ``Sheet`` instance will be returned. Args: key (string): If not used before, register the sheet under this key. If used before, return the previous ``Sheet`` instance registered with this key. rows (int): The number of rows in the sheet columns (int): The number of columns in the sheet row_headers (bool, list): Either a boolean specifying if row headers should be displayed or not, or a list of strings containing the row headers column_headers (bool, list): Either a boolean specifying if column headers should be displayed or not, or a list of strings containing the column headers Returns: The new ``Sheet`` widget, or if key is given, the previously created sheet registered with this key. Example: >>> from ipysheet import sheet, current >>> >>> s1 = sheet('key1') >>> s2 = sheet('key2') >>> >>> assert s2 is current() >>> assert s1 is sheet('key1') >>> assert s1 is current() """ global _last_sheet if isinstance(key, Sheet): _last_sheet = key elif key is None or key not in _sheets: _last_sheet = cls(rows=rows, columns=columns, column_width=column_width, row_headers=row_headers, column_headers=column_headers, stretch_headers=stretch_headers, **kwargs) if key is not None: _sheets[key] = _last_sheet else: _last_sheet = _sheets[key] return _last_sheet
[docs]def current(): """ Returns: the current ``Sheet`` instance """ return _last_sheet
[docs]@doc_subst(_common_doc) def cell(row, column, value=0., type=None, color=None, background_color=None, font_style=None, font_weight=None, style=None, label_left=None, choice=None, read_only=False, numeric_format='0.000', date_format='YYYY/MM/DD', renderer=None, **kwargs): """Adds a new ``Cell`` widget to the current ``Sheet`` Args: row (int): Zero based row index where to put the cell in the sheet column (int): Zero based column index where to put the cell in the sheet value (int, float, string, bool, Widget): The value of the cell {args} Returns: The new ``Cell`` widget. Example: >>> from ipysheet import sheet, cell >>> >>> s1 = sheet() >>> cell(0, 0, 36.) # The Cell type will be 'numeric' >>> cell(1, 0, True) # The Cell type will be 'checkbox' >>> cell(0, 1, 'Hello World!') # The Cell type will be 'text' >>> c = cell(1, 1, True) >>> c.value = False # Dynamically changing the cell value at row=1, column=1 """ global _cells if type is None: if isinstance(value, bool): type = 'checkbox' elif isinstance(value, numbers.Number): type = 'numeric' elif isinstance(value, widgets.Widget): type = 'widget' else: type = 'text' if choice is not None: type = 'dropdown' style = style or {} if color is not None: style['color'] = color if background_color is not None: style['backgroundColor'] = background_color if font_style is not None: style['fontStyle'] = font_style if font_weight is not None: style['fontWeight'] = font_weight c = Cell(value=value, row_start=row, column_start=column, row_end=row, column_end=column, squeeze_row=True, squeeze_column=True, type=type, style=style, choice=choice, read_only=read_only, numeric_format=numeric_format, date_format=date_format, renderer=renderer, **kwargs) if _hold_cells: _cells += (c,) else: _last_sheet.cells = _last_sheet.cells+(c,) if label_left: if column-1 < 0: raise IndexError("cannot put label to the left of column 0") cell(row, column-1, value=label_left, font_weight='bold') return c
[docs]@doc_subst(_common_doc) def row(row, value, column_start=0, column_end=None, type=None, color=None, background_color=None, font_style=None, font_weight=None, style=None, choice=None, read_only=False, numeric_format='0.000', date_format='YYYY/MM/DD', renderer=None, **kwargs): """Create a ``Cell`` widget, representing multiple cells in a sheet, in a horizontal row Args: row (int): Zero based row index where to put the row in the sheet value (list): The list of cell values representing the row column_start (int): Which column the row will start, default 0. column_end (int): Which column the row will end, default is the last. {args} Returns: The new ``Cell`` widget. Example: >>> from ipysheet import sheet, row >>> >>> s1 = sheet() >>> row(0, [1, 2, 3, 34, 5]) # The Cell type will be 'numeric' >>> row(1, [True, False, True], column_start=2) # The Cell type will be 'checkbox' """ return cell_range(value, column_start=column_start, column_end=column_end, row_start=row, row_end=row, squeeze_row=True, squeeze_column=False, color=color, background_color=background_color, font_style=font_style, font_weight=font_weight, style=style, type=type, choice=choice, read_only=read_only, numeric_format=numeric_format, date_format=date_format, renderer=renderer, **kwargs)
[docs]@doc_subst(_common_doc) def column(column, value, row_start=0, row_end=None, type=None, color=None, background_color=None, font_style=None, font_weight=None, style=None, choice=None, read_only=False, numeric_format='0.000', date_format='YYYY/MM/DD', renderer=None, **kwargs): """Create a ``Cell`` widget, representing multiple cells in a sheet, in a vertical column Args: column (int): Zero based column index where to put the column in the sheet value (list): The list of cell values representing the column row_start (int): Which row the column will start, default 0. row_end (int): Which row the column will end, default is the last. {args} Returns: The new ``Cell`` widget. Example: >>> from ipysheet import sheet, column >>> >>> s1 = sheet() >>> column(0, [1, 2, 3, 34, 5]) # The Cell type will be 'numeric' >>> column(1, [True, False, True], row_start=2) # The Cell type will be 'checkbox' """ return cell_range(value, column_start=column, column_end=column, row_start=row_start, row_end=row_end, squeeze_row=False, squeeze_column=True, style=style, choice=choice, read_only=read_only, numeric_format=numeric_format, date_format=date_format, renderer=renderer, color=color, background_color=background_color, type=type, font_style=font_style, font_weight=font_weight, **kwargs)
[docs]@doc_subst(_common_doc) def cell_range(value, row_start=0, column_start=0, row_end=None, column_end=None, transpose=False, squeeze_row=False, squeeze_column=False, type=None, color=None, background_color=None, font_style=None, font_weight=None, style=None, choice=None, read_only=False, numeric_format='0.000', date_format='YYYY/MM/DD', renderer=None, **kwargs): """Create a ``Cell`` widget, representing multiple cells in a sheet Args: value (list): The list of cell values representing the range row_start (int): Which row the range will start, default 0. column_start (int): Which column the range will start, default 0. row_end (int): Which row the range will end, default is the last. column_end (int): Which column the range will end, default is the last. transpose (bool): Whether to interpret the value array as value[column_index][row_index] or not. squeeze_row (bool): Take out the row dimensions, meaning only value[column_index] is used. squeeze_column (bool): Take out the column dimensions, meaning only value[row_index] is used. {args} Returns: The new ``Cell`` widget. Example: >>> from ipysheet import sheet, cell_range >>> >>> s1 = sheet() >>> cell_range([[1, 2, 3, 34, 5], [6, 7, 8, 89, 10]]) """ global _cells value_original = value value = adapt_value(value) # instead of an if statements, we just use T to transpose or not when needed T = (lambda x: x) if not transpose else default_transpose # we work with the optionally transposed values for simplicity value = T(value) if squeeze_row: value = [value] if squeeze_column: value = [[k] for k in value] if row_end is None: row_end = row_start + len(value) - 1 row_length = row_end - row_start + 1 if row_length != len(value): raise ValueError("length or array doesn't match number of rows") if row_length == 0: raise ValueError("0 rows not supported") if column_end is None: column_end = column_start + len(value[0]) - 1 column_length = column_end - column_start + 1 if column_length == 0: raise ValueError("0 columns not supported") for row in value: if column_length != len(row): raise ValueError("not a regular matrix, columns lengths differ") if row_start + row_length > _last_sheet.rows: raise ValueError("array will go outside of sheet, too many rows") if column_start + column_length > _last_sheet.columns: raise ValueError("array will go outside of sheet, too many columns") # see if we an infer a type from the data, otherwise leave it None if type is None: type_check_map = [('checkbox', lambda x: isinstance(x, bool)), ('numeric', lambda x: isinstance(x, numbers.Number)), ('text', lambda x: isinstance(x, six.string_types)), ('widget', lambda x: isinstance(x, widgets.Widget)), ] for type_check, check in type_check_map: checks = True # ok until proven wrong for i in range(row_length): for j in range(column_length): if not check(value[i][j]): checks = False if checks: # we found a matching type type = type_check break style = style or {} if color is not None: style['color'] = color if background_color is not None: style['backgroundColor'] = background_color if font_style is not None: style['fontStyle'] = font_style if font_weight is not None: style['fontWeight'] = font_weight c = Cell(value=value_original, row_start=row_start, column_start=column_start, row_end=row_end, column_end=column_end, squeeze_row=squeeze_row, squeeze_column=squeeze_column, transpose=transpose, type=type, read_only=read_only, choice=choice, renderer=renderer, numeric_format=numeric_format, date_format=date_format, style=style, **kwargs) if _hold_cells: _cells += (c,) else: _last_sheet.cells = _last_sheet.cells+(c,) return c
[docs]def renderer(code, name): """Create a ``Renderer`` widget Args: code (string or code or function object): If a string object, it is assumed to be a JavaScript snippet, else it is assumed to be a function or code object and will be transpiled to javascript using flexxui/pscript. name (string): Name of the renderer Returns: The new ``Renderer`` widget. Example: >>> from ipysheet import sheet, renderer, cell >>> >>> s1 = sheet() >>> >>> def renderer_negative(instance, td, row, col, prop, value, cellProperties): >>> Handsontable.renderers.TextRenderer.apply(this, arguments); >>> if value < 0: >>> td.style.backgroundColor = 'orange' >>> else: >>> td.style.backgroundColor = '' >>> >>> renderer(code=renderer_negative, name='negative'); >>> cell(0, 0, 36, renderer='negative') # Will be white >>> cell(1, 0, -36, renderer='negative') # Will be orange """ if not isinstance(code, six.string_types): from pscript import py2js code_transpiled = py2js(code, new_name='the_renderer', indent=4) code = ''' function() { %s return the_renderer }() ''' % code_transpiled renderer = Renderer(code=code, name=name) return renderer
def _assign(object, value): if isinstance(object, widgets.Widget): object, trait = object, 'value' else: object, trait = object setattr(object, trait, value)
[docs]def calculation(inputs, output, initial_calculation=True): """A decorator that assigns to output cell a calculation depending on the inputs Args: inputs (list of widgets, or (widget, 'traitname') pairs): List of all widget, whose values (default 'value', otherwise specified by 'traitname') are input of the function that is decorated output (widget or (widget, 'traitname')): The output of the decorator function will be assigned to output.value or output.<traitname>. initial_calculation (bool): When True the calculation will be done directly for the first time. Example: >>> from ipywidgets import IntSlider >>> from ipysheet import cell, calculation >>> >>> a = cell(0, 0, value=1) >>> b = cell(1, 0, value=IntSlider(value=2)) >>> c = IntSlider(max=56) >>> d = cell(3, 0, value=1) >>> >>> @calculation(inputs=[a, (b, 'value'), (c, 'max')], output=d) >>> def add(a, b, c): >>> return a + b + c """ def decorator(f): def get_value(input): if isinstance(input, widgets.Widget): object, trait = input, 'value' else: object, trait = input # assume it's a tup;e if isinstance(object, Cell) and isinstance(object.value, widgets.Widget): object = object.value return getattr(object, trait) def calculate(*ignore_args): values = map(get_value, inputs) result = f(*values) _assign(output, result) for input in inputs: if isinstance(input, widgets.Widget): object, trait = input, 'value' else: object, trait = input # assume it's a tuple if isinstance(object, Cell) and isinstance(object.value, widgets.Widget): # when it is a cell which holds a widget, we actually want the widgets' value object.value.observe(calculate, trait) else: object.observe(calculate, trait) def handle_possible_widget_change(change, trait=trait): if isinstance(change['old'], widgets.Widget): change['old'].unobserve(calculate, trait) if isinstance(change['new'], widgets.Widget): change['new'].observe(calculate, trait) calculate() object.observe(handle_possible_widget_change, 'value') if initial_calculation: calculate() return decorator
[docs]@contextmanager def hold_cells(): """Hold adding any cell widgets until leaving this context. This may give a better performance when adding many cells. Example: >>> from ipysheet import sheet, cell, hold_cells >>> >>> sheet(rows=10,columns=10) >>> with hold_cells() >>> for i in range(10): >>> for j in range(10): >>> cell(i, j, value=i * 10 + j) >>> # at this line, the Cell widgets are added """ global _hold_cells global _cells if _hold_cells is True: yield else: try: _hold_cells = True yield finally: _hold_cells = False # print(_cells, _last_sheet.cells) _last_sheet.cells = tuple(_last_sheet.cells) + tuple(_cells) _cells = ()