8000 WIP FEAT: heavily refactored array model, adapter code and widget code · larray-project/larray-editor@cb55ba7 · GitHub
[go: up one dir, main page]

Skip to content

Commit cb55ba7

Browse files
committed
WIP FEAT: heavily refactored array model, adapter code and widget code
* fixed #161: use indices for filters (to avoid problems with duplicate labels) * fixed single column plot in viewer when ticks are not strings * added support for plot with more than 1 dimension in columns (by making plot axes labels customizable by the adapter) * allow registering an adapter via an explicit function call instead of via a decorator * adapters can be registered using a string (so that we do not load the module just to register the adapter) looked-up adapters are cached for faster retrieval for subsequent instances of that type * adapters are used to display instances of their subclasses. When there is both an adapter for a parent class and a subclass, we use the adapter for the subclass by sorting registered types by the length of their mro. Note that string types always come last (so we cannot have two string types inheriting from each other) * added support for per-column color value * WIP: adapters can return simple sequences instead of ndarrays (buggy for list of sequences) * split get_adapter in two steps: get_adapter_creator which gets a callable and calling that callable to actually get an adapter * made writing adapters easier and to avoid having to load all data in memory remaining issues: * to make it work nicely with partially visible cells I used a shortcut and tweaked some limits by 1 but it gives odd behavior when all cells are visible (visible area >= array) fixing this properly is possible but not trivial
1 parent 773192b commit cb55ba7

File tree

8 files changed

+1986
-1244
lines changed

8 files changed

+1986
-1244
lines changed

larray_editor/arrayadapter.py

Lines changed: 575 additions & 417 deletions
Large diffs are not rendered by default.

larray_editor/arraymodel.py

Lines changed: 576 additions & 385 deletions
Large diffs are not rendered by default.

larray_editor/arraywidget.py

Lines changed: 663 additions & 208 deletions
Large diffs are not rendered by default.

larray_editor/commands.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ class ArrayValueChange:
1717
1818
Parameters
1919
----------
20+
# FIXME: key is a tuple of indices
2021
key: list/tuple of str
2122
Key associated with the value
2223
old_value: scalar
@@ -42,7 +43,7 @@ class EditArrayCommand(QUndoCommand):
4243
Instance of MappingEditor
4344
target : object
4445
target array to edit. Can be given under any form.
45-
changes: (list of) instance(s) of ArrayValueChange
46+
changes: list of ArrayValueChange
4647
List of changes
4748
"""
4849

@@ -61,6 +62,7 @@ def __init__(self, editor, target, changes):
6162
def undo(self):
6263
for change in self.changes:
6364
self.apply_change(change.key, change.old_value)
65+
# TODO: a full reset is wasteful
6466
self.editor.arraywidget.model_data.reset()
6567

6668
def redo(self):
@@ -95,7 +97,8 @@ def get_description(self, target, changes):
9597
return f"Pasting {len(changes)} Cells in {target}"
9698

9799
def apply_change(self, key, new_value):
98-
self.editor.kernel.shell.run_cell(f"{self.target}[{key}] = {new_value}")
100+
# FIXME: we should pass via the adapter to have something generic
101+
self.editor.kernel.shell.run_cell(f"{self.target}.i[{key}] = {new_value}")
99102

100103

101104
class EditCurrentArrayCommand(EditArrayCommand):
@@ -108,7 +111,7 @@ class EditCurrentArrayCommand(EditArrayCommand):
108111
Instance of ArrayEditor
109112
target : Array
110113
array to edit
111-
changes : (list of) instance(s) of ArrayValueChange
114+
changes : (list of) ArrayValueChange
112115
List of changes
113116
"""
114117
def get_description(self, target, changes):
@@ -118,4 +121,5 @@ def get_description(self, target, changes):
118121
return f"Pasting {len(changes)} Cells"
119122

120123
def apply_change(self, key, new_value):
121-
self.target[key] = new_value
124+
# FIXME: we should pass via the adapter to have something generic
125+
self.target.i[key] = new_value

larray_editor/editor.py

Lines changed: 51 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,9 @@
77
from pathlib import Path
88
from typing import Union
99

10-
1110
# Python3.8 switched from a Selector to a Proactor based event loop for asyncio but they do not offer the same
1211
# features, which breaks Tornado and all projects depending on it, including Jupyter consoles
13-
# refs: https://github.com/larray-project/larray-editor/issues/208
12+
# ref: https://github.com/larray-project/larray-editor/issues/208
1413
if sys.platform.startswith("win") and sys.version_info >= (3, 8):
1514
import asyncio
1615

@@ -26,15 +25,10 @@
2625
import matplotlib
2726
import matplotlib.axes
2827
import numpy as np
28+
import pandas as pd
2929

3030
import larray as la
3131

32-
from larray_editor.traceback_tools import StackSummary
33-
from larray_editor.utils import (_, create_action, show_figure, ima, commonpath, dependencies,
34-
get_versions, get_documentation_url, urls, RecentlyUsedList)
35-
from larray_editor.arraywidget import ArrayEditorWidget
36-
from larray_editor.commands import EditSessionArrayCommand, EditCurrentArrayCommand
37-
3832
from qtpy.QtCore import Qt, QUrl, QSettings
3933
from qtpy.QtGui import QDesktopServices, QKeySequence
4034
from qtpy.QtWidgets import (QMainWindow, QWidget, QListWidget, QListWidgetItem, QSplitter, QFileDialog, QPushButton,
@@ -48,6 +42,13 @@
4842
# unsure qtpy has been fixed yet (see https://github.com/spyder-ide/qtpy/pull/366 for the fix for QUndoCommand)
4943
from qtpy.QtGui import QUndoStack
5044

45+
from larray_editor.traceback_tools import StackSummary
46+
from larray_editor.utils import (_, create_action, show_figure, ima, commonpath, dependencies,
47+
get_versions, get_documentation_url, urls, RecentlyUsedList)
48+
from larray_editor.arraywidget import ArrayEditorWidget
49+
from larray_editor.arrayadapter import get_adapter_creator
50+
from larray_editor.commands import EditSessionArrayCommand, EditCurrentArrayCommand
51+
5152
try:
5253
from qtconsole.rich_jupyter_widget import RichJupyterWidget
5354
from qtconsole.inprocess import QtInProcessKernelManager
@@ -80,7 +81,7 @@
8081
# XXX: add all scalars except strings (from numpy or plain Python)?
8182
# (long) strings are not handled correctly so should NOT be in this list
8283
# tuple, list
83-
DISPLAY_IN_GRID = (la.Array, np.ndarray)
84+
DISPLAY_IN_GRID = (la.Array, np.ndarray, pd.DataFrame)
8485

8586

8687
class AbstractEditor(QMainWindow):
@@ -273,30 +274,45 @@ def about(self):
273274
message += "</ul>"
274275
QMessageBox.about(self, _("About LArray Editor"), message.format(**kwargs))
275276

276-
def _update_title(self, title, array, name):
277+
def _update_title(self, title, value, name):
277278
if title is None:
278279
title = []
279280

280-
if array is not None:
281-
dtype = array.dtype.name
282-
# current file (if not None)
283-
if isinstance(array, la.Array):
284-
# array info
285-
shape = [f'{display_name} ({len(axis)})'
286-
for display_name, axis in zip(array.axes.display_names, array.axes)]
281+
if value is not None:
282+
# TODO: the type-specific information added to the title should be computed by a method on the adapter
283+
# (self.arraywidget.data_adapter)
284+
if hasattr(value, 'dtype'):
285+
try:
286+
dtype_str = f' [{value.dtype.name}]'
287+
except:
288+
dtype_str = ''
287289
else:
288-
# if it's not an Array, it must be a Numpy ndarray
289-
assert isinstance(array, np.ndarray)
290-
shape = [str(length) for length in array.shape]
291-
# name + shape + dtype
292-
array_info = ' x '.join(shape) + f' [{dtype}]'
293-
if name:
294-
title += [name + ': ' + array_info]
290+
dtype_str = ''
291+
292+
if hasattr(value, 'shape'):
293+
if isinstance(value, la.Array):
294+
shape = [f'{display_name} ({len(axis)})'
295+
for display_name, axis in zip(value.axes.display_names, value.axes)]
296+
else:
297+
try:
298+
shape = [str(length) for length in value.shape]
299+
except:
300+
shape = []
301+
shape_str = ' x '.join(shape)
295302
else:
296-
title += [array_info]
303+
shape_str = ''
304+
305+
# name + shape + dtype
306+
value_info = shape_str + dtype_str
307+
if name and value_info:
308+
title.append(name + ': ' + value_info)
309+
elif name:
310+
title.append(name)
311+
elif value_info:
312+
title.append(value_info)
297313

298314
# extra info
299-
title += [self._title]
315+
title.append(self._title)
300316
# set title
301317
self.setWindowTitle(' - '.join(title))
302318

@@ -379,7 +395,10 @@ def _setup_and_check(self, widget, data, title, readonly, stack_pos=None, add_la
379395
self.data = la.Session()
380396
self.arraywidget = ArrayEditorWidget(self, readonly=readonly)
381397
self.arraywidget.dataChanged.connect(self.push_changes)
382-
self.arraywidget.model_data.dataChanged.connect(self.update_title)
398+
# FIXME: this is currently broken as it fires for each scroll
399+
# we either need to fix model_data.dataChanged (but that might be needed for display)
400+
# or find another way to add a star to the window title *only* when the user actually changed something
401+
# self.arraywidget.model_data.dataChanged.connect(self.update_title)
383402

384403
if qtconsole_available:
385404
# silence a warning on Python 3.11 (see issue #263)
@@ -682,7 +701,8 @@ def view_expr(self, array, expr):
682701
self.set_current_array(array, expr)
683702

684703
def _display_in_grid(self, k, v):
685-
return not k.startswith('__') and isinstance(v, DISPLAY_IN_GRID)
704+
# return not k.startswith('__') and isinstance(v, DISPLAY_IN_GRID)
705+
return not k.startswith('__') and get_adapter_creator(v) is not None
686706

687707
def ipython_cell_executed(self):
688708
user_ns = self.kernel.shell.user_ns
@@ -779,6 +799,8 @@ def update_title(self):
779799
def set_current_array(self, array, name):
780800
# we should NOT check that "array is not self.current_array" because this method is also called to
781801
# refresh the widget value because of an inplace setitem
802+
803+
# FIXME: we should never store the current_array but current_adapter instead
782804
self.current_array = array
783805
self.arraywidget.set_data(array)
784806
self.current_array_name = name
@@ -1047,6 +1069,7 @@ def save_script(self):
10471069
if overwrite and os.path.isfile(filepath):
10481070
ret = QMessageBox.warning(self, "Warning",
10491071
f"File `{filepath}` exists. Are you sure to overwrite it?",
1072+
# TODO: Discard is useless I think
10501073
QMessageBox.Save | QMessageBox.Cancel)
10511074
if ret == QMessageBox.Save:
10521075
self._save_script(filepath, lines, overwrite)

larray_editor/tests/test_adapter.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import pytest
2-
32
import larray as la
43

54

larray_editor/tests/test_api_larray.py

Lines changed: 41 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -3,81 +3,16 @@
33
import logging
44
# from pathlib import Path
55

6-
import numpy as np
6+
import qtpy
77
import larray as la
88

99
from larray_editor.api import edit
1010
# from larray_editor.api import view, edit, debug, compare
1111
from larray_editor.utils import logger
1212

13-
import qtpy
14-
1513
print(f"Using {qtpy.API_NAME} as Qt API")
16-
1714
logger.setLevel(logging.DEBUG)
1815

19-
lipro = la.Axis('lipro=P01..P15')
20-
age = la.Axis('age=0..115')
21-
sex = la.Axis('sex=M,F')
22-
23-
vla = 'A11,A12,A13,A23,A24,A31,A32,A33,A34,A35,A36,A37,A38,A41,A42,A43,A44,A45,A46,A71,A72,A73'
24-
wal = 'A25,A51,A52,A53,A54,A55,A56,A57,A61,A62,A63,A64,A65,A81,A82,A83,A84,A85,A91,A92,A93'
25-
bru = 'A21'
26-
# list of strings
27-
belgium = la.union(vla, wal, bru)
28-
29-
geo = la.Axis(belgium, 'geo')
30-
31-
# arr1 = la.ndtest((sex, lipro))
32-
# edit(arr1)
33-
34-
# data2 = np.arange(116 * 44 * 2 * 15).reshape(116, 44, 2, 15) \
35-
# .astype(float)
36-
# data2 = np.random.random(116 * 44 * 2 * 15).reshape(116, 44, 2, 15) \
37-
# .astype(float)
38-
# data2 = (np.random.randint(10, size=(116, 44, 2, 15)) - 5) / 17
39-
# data2 = np.random.randint(10, size=(116, 44, 2, 15)) / 100 + 1567
40-
# data2 = np.random.normal(51000000, 10000000, size=(116, 44, 2, 15))
41-
arr2 = la.random.normal(axes=(age, geo, sex, lipro))
42-
# arr2 = la.ndrange([100, 100, 100, 100, 5])
43-
# arr2 = arr2['F', 'A11', 1]
44-
45-
# view(arr2[0, 'A11', 'F', 'P01'])
46-
# view(arr1)
47-
# view(arr2[0, 'A11'])
48-
# edit(arr1)
49-
# print(arr2[0, 'A11', :, 'P01'])
50-
# edit(arr2.astype(int), minvalue=-99, maxvalue=55.123456)
51-
# edit(arr2.astype(int), minvalue=-99)
52-
# arr2.i[0, 0, 0, 0] = np.inf
53-
# arr2.i[0, 0, 1, 1] = -np.inf
54-
# arr2 = [0.0000111, 0.0000222]
55-
# arr2 = [0.00001, 0.00002]
56-
# edit(arr2, minvalue=-99, maxvalue=25.123456)
57-
# print(arr2[0, 'A11', :, 'P01'])
58-
59-
# arr2 = la.random.normal(0, 10, axes="d0=0..4999;d1=0..19")
60-
# edit(arr2)
61-
62-
# view(['a', 'bb', 5599])
63-
# view(np.arange(12).reshape(2, 3, 2))
64-
# view([])
65-
66-
data3 = np.random.normal(0, 1, size=(2, 15))
67-
arr3 = la.ndtest((30, sex))
68-
# data4 = np.random.normal(0, 1, size=(2, 15))
69-
# arr4 = la.Array(data4, axes=(sex, lipro))
70-
71-
# arr4 = arr3.copy()
72-
# arr4['F'] /= 2
73-
arr4 = arr3.min(sex)
74-
arr5 = arr3.max(sex)
75-
arr6 = arr3.mean(sex)
76-
77-
# test isssue #35
78-
arr7 = la.from_lists([['a', 1, 2, 3],
79-
[ '', 1664780726569649730, -9196963249083393206, -7664327348053294350]])
80-
8116

8217
def make_circle(width=20, radius=9):
8318
x, y = la.Axis(width, 'x'), la.Axis(width, 'y')
@@ -109,29 +44,49 @@ def test_matplotlib_show_interaction():
10944
edit()
11045

11146

112-
demo = make_demo(9, 2.5, 1.5)
113-
sphere = make_sphere(9, 4)
114-
extreme_array = la.Array([-la.inf, -1, 0, la.nan, 1, la.inf])
115-
array_scalar = la.Array(0)
116-
arr_all_nan = la.Array([la.nan, la.nan])
47+
lipro = la.Axis('lipro=P01..P15')
48+
age = la.Axis('age=0..29')
49+
sex = la.Axis('sex=M,F')
50+
geo = la.Axis(['A11', 'A25', 'A51', 'A21'], 'geo')
51+
52+
la_arr2 = la.random.normal(axes=(age, geo, sex, lipro))
53+
la_arr3 = la.ndtest((30, sex))
54+
la_arr4 = la_arr3.min(sex)
55+
la_arr5 = la_arr3.max(sex)
56+
la_arr6 = la_arr3.mean(sex)
57+
58+
# test isssue #35
59+
la_arr7 = la.from_lists([['a', 1, 2, 3],
60+
[ '', 1664780726569649730, -9196963249083393206, -7664327348053294350]])
61+
62+
la_demo = make_demo(9, 2.5, 1.5)
63+
la_sphere = make_sphere(9, 4)
64+
la_extreme_array = la.Array([-la.inf, -1, 0, la.nan, 1, la.inf])
65+
la_scalar = la.Array(0)
66+
la_all_nan = la.Array([la.nan, la.nan])
67+
# FIXME: this test should be updated for buffer
11768
# this is crafted so that the entire 500 points sample is all nan but
11869
# other values need coloring
119-
arr_full_buffer_nan_should_not_be_all_white = la.ndtest(1000, dtype=float)
120-
arr_full_buffer_nan_should_not_be_all_white['a0'::2] = la.nan
121-
arr_empty = la.Array([])
122-
arr_empty_2d = la.Array([[], []])
123-
arr_obj = la.ndtest((2, 3)).astype(object)
124-
arr_str = la.ndtest((2, 3)).astype(str)
125-
big = la.ndtest((1000, 1000, 500))
126-
big1d = la.ndtest(1000000)
70+
la_full_buffer_nan_should_not_be_all_white = la.ndtest(1000, dtype=float)
71+
la_full_buffer_nan_should_not_be_all_white['a0'::2] = la.nan
72+
la_empty = la.Array([])
73+
la_empty_2d = la.Array([[], []])
74+
la_obj_numeric = la.ndtest((2, 3)).astype(object)
75+
la_boolean = (la_arr3 % 3) == 0
76+
la_obj_mixed = la.ndtest((2, 3)).astype(object)
77+
la_obj_mixed['a0', 'b1'] = 'hello'
78+
la_str = la.ndtest((2, 3)).astype(str)
79+
la_big = la.ndtest((1000, 1000, 500))
80+
la_big1d = la.ndtest(1000000)
12781
# force big1d.axes[0]._mapping to be created so that we do not measure that delay in the editor
128-
big1d[{}]
82+
_ = la_big1d[{}]
83+
del _
12984

130-
# test autoresizing
131-
long_labels = la.zeros('a=a_long_label,another_long_label; b=this_is_a_label,this_is_another_one')
132-
long_axes_names = la.zeros('first_axis=a0,a1; second_axis=b0,b1')
85+
# test auto-resizing
86+
la_long_labels = la.zeros('a=a_long_label,another_long_label; b=this_is_a_label,this_is_another_one')
87+
la_long_axes_names = la.zeros('first_axis=a0,a1; second_axis=b0,b1')
13388

134-
# compare(arr3, arr4, arr5, arr6)
89+
# compare(la_arr3, la_arr4, la_arr5, la_arr6)
13590

13691
# view(stack((arr3, arr4), la.Axis('arrays=arr3,arr4')))
13792
# ses = la.Session(arr2=arr2, arr3=arr3, arr4=arr4, arr5=arr5, arr6=arr6, arr7=arr7, long_labels=long_labels,
@@ -170,11 +125,8 @@ def test_matplotlib_show_interaction():
170125

171126
# s = la.local_arrays()
172127
# view(s)
173-
# print('HDF')
174128
# s.save('x.h5')
175-
# print('\nEXCEL')
176129
# s.save('x.xlsx')
177-
# print('\nCSV')
178130
# s.save('x_csv')
179131
# print('\n open HDF')
180132
# edit('x.h5')
@@ -218,4 +170,6 @@ def test_run_editor_on_exception(local_arr):
218170

219171
# test_run_editor_on_exception(arr2)
220172

173+
# debug()
174+
221175
test_matplotlib_show_interaction()

0 commit comments

Comments
 (0)
0