Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
fail-fast: false
matrix:
# os: ["ubuntu-latest", "macos-latest", "windows-latest"]
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14']

defaults:
run:
Expand Down
8 changes: 8 additions & 0 deletions doc/source/changes.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
Change log
##########

Version 0.35.1
==============

In development.

.. include:: ./changes/version_0_35_1.rst.inc


Version 0.35
============

Expand Down
22 changes: 22 additions & 0 deletions doc/source/changes/version_0_35_1.rst.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
.. py:currentmodule:: larray_editor

New features
^^^^^^^^^^^^

* added explicit support for Python 3.14.


Miscellaneous improvements
^^^^^^^^^^^^^^^^^^^^^^^^^^

* improved something.


Fixes
^^^^^

* fixed displaying DataFrames with Pandas >= 3 (closes :editor_issue:`308`).

* fixed some objects not being updated immediately when updated. This includes
updates to Pandas DataFrames done via `df.loc[key] = value` and
`df.iloc[position] = value` (closes :editor_issue:`310`).
2 changes: 1 addition & 1 deletion larray_editor/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from larray_editor.api import * # noqa: F403

__version__ = '0.35'
__version__ = '0.35.1-dev'
20 changes: 17 additions & 3 deletions larray_editor/arrayadapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -1834,22 +1834,36 @@ def get_hnames(self):
def get_vnames(self):
return self.data.index.names

@staticmethod
def _ensure_numpy_array(data):
"""Convert data to a numpy array if it is not already one."""
pd = sys.modules['pandas']
if isinstance(data, pd.arrays.ArrowStringArray):
return data.to_numpy()
else:
assert isinstance(data, np.ndarray)
return data

def get_vlabels_values(self, start, stop):
pd = sys.modules['pandas']
index = self.sorted_data.index[start:stop]
if isinstance(index, pd.MultiIndex):
# It seems like Pandas always returns a 1D array of tuples for
# MultiIndex.values, even if the MultiIndex has an homoneneous
# string type. That's why we do not need _ensure_numpy_array here
# list(row) because we want a list of list and not a list of tuples
return [list(row) for row in index.values]
else:
return index.values[:, np.newaxis]
return self._ensure_numpy_array(index.values)[:, np.newaxis]

def get_hlabels_values(self, start, stop):
pd = sys.modules['pandas']
index = self.sorted_data.columns[start:stop]
if isinstance(index, pd.MultiIndex):
return [index.get_level_values(i).values
return [self._ensure_numpy_array(index.get_level_values(i).values)
for i in range(index.nlevels)]
else:
return [index.values]
return [self._ensure_numpy_array(index.values)]

def get_values(self, h_start, v_start, h_stop, v_stop):
# Sadly, as of Pandas 2.2.3, the previous version of this code:
Expand Down
10 changes: 5 additions & 5 deletions larray_editor/arraywidget.py
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@ def _close_adapter(adapter):

def on_clicked(self):
if not len(self._back_data):
logger.warn("Back button has no target to go to")
logger.warning("Back button has no target to go to")
return
target_data = self._back_data.pop()
data_adapter = self._back_data_adapters.pop()
Expand Down Expand Up @@ -1272,17 +1272,17 @@ def update_range(self):
max_value = total_cols - buffer_ncols + hidden_hscroll_max
logger.debug(f"update_range horizontal {total_cols=} {buffer_ncols=} {hidden_hscroll_max=} => {max_value=}")
if total_cols == 0 and max_value != 0:
logger.warn(f"empty data but {max_value=}. We let it pass for "
f"now (set it to 0).")
logger.warning(f"empty data but {max_value=}. We let it pass "
"for now (set it to 0).")
max_value = 0
else:
buffer_nrows = self.model.nrows
hidden_vscroll_max = view_data.verticalScrollBar().maximum()
max_value = total_rows - buffer_nrows + hidden_vscroll_max
logger.debug(f"update_range vertical {total_rows=} {buffer_nrows=} {hidden_vscroll_max=} => {max_value=}")
if total_rows == 0 and max_value != 0:
logger.warn(f"empty data but {max_value=}. We let it pass for "
f"now (set it to 0).")
logger.warning(f"empty data but {max_value=}. We let it pass "
"for now (set it to 0).")
max_value = 0
assert max_value >= 0, "max_value should not be negative"
value_before = self.value()
Expand Down
38 changes: 27 additions & 11 deletions larray_editor/editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,20 @@
REOPEN_LAST_FILE = object()

ASSIGNMENT_PATTERN = re.compile(r'[^\[\]]+[^=]=[^=].+')
SUBSET_UPDATE_PATTERN = re.compile(r'(\w+)'
r'(\.i|\.iflat|\.points|\.ipoints)?'
r'\[.+\]\s*'
r'([-+*/%&|^><]|//|\*\*|>>|<<)?'
r'=\s*[^=].*')
# This will match for:
# * variable = expr
# * variable[key] = expr
# * variable.attribute = expr
# * variable.attribute[key] = expr
# and their inplace ops counterpart
UPDATE_VARIABLE_PATTERN = re.compile(
r'(?P<variable>\w+)'
r'(?P<attribute>\.\w+)?'
r'(?P<subset>\[.+\])?'
r'\s*'
r'(?P<inplaceop>[-+*/%&|^><]|//|\*\*|>>|<<)?'
r'=\s*[^=].*'
) # = expr
HISTORY_VARS_PATTERN = re.compile(r'_i?\d+')

opened_secondary_windows = []
Expand Down Expand Up @@ -986,11 +995,18 @@ def ipython_cell_executed(self):
# It would be easier to use '_' instead but that refers to the last output, not the output of the
# last command. Which means that if the last command did not produce any output, _ is not modified.
cur_output = user_ns['_oh'].get(cur_input_num)
setitem_pattern_match = SUBSET_UPDATE_PATTERN.match(last_input_last_line)
# setitem
if setitem_pattern_match is not None:
varname = setitem_pattern_match.group(1)
# simple variable

# matches both setitem and setattr
update_variable_match = UPDATE_VARIABLE_PATTERN.match(last_input_last_line)
if update_variable_match is not None:
parts = update_variable_match.groupdict()
varname = parts['variable']
if all(parts[name] is None for name in ('attribute', 'subset', 'inplaceop')):
# simple variable assignment => could be a new variable
# => must update mapping and varlist
changed_var = self.update_mapping_and_varlist(clean_ns)
assert changed_var == varname
# simple variable name (only)
elif last_input_last_line in clean_ns:
varname = last_input_last_line
# any other statement
Expand Down Expand Up @@ -1018,7 +1034,7 @@ def ipython_cell_executed(self):
# For better or worse, _save_data() only saves "displayable data"
# so changes to variables we cannot display do not concern us,
# and this line should not be moved outside the if condition.
if setitem_pattern_match is not None:
if update_variable_match is not None:
self.unsaved_modifications = True

# TODO: this completely refreshes the array, including detecting
Expand Down
3 changes: 2 additions & 1 deletion larray_editor/tests/test_api_larray.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@
array_signed_int = array.array('l', [1, 2, 3, 4, 5])
array_signed_int_empty = array.array('l')
# should show as hello alpha and omega
array_unicode = array.array('w', 'hello \u03B1 and \u03C9')
unicode_typecode = 'w' if sys.version_info >= (3, 13) else 'u'
array_unicode = array.array(unicode_typecode, 'hello \u03B1 and \u03C9')

# list
list_empty = []
Expand Down
77 changes: 48 additions & 29 deletions larray_editor/tests/test_inplace_modification_pattern.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,51 @@
from larray_editor.editor import SUBSET_UPDATE_PATTERN
from larray_editor.editor import UPDATE_VARIABLE_PATTERN


def test_pattern():
assert SUBSET_UPDATE_PATTERN.match('arr1[1] = 2')
assert SUBSET_UPDATE_PATTERN.match('arr1[1]= 2')
assert SUBSET_UPDATE_PATTERN.match('arr1[1]=2')
assert SUBSET_UPDATE_PATTERN.match("arr1['a'] = arr2")
assert SUBSET_UPDATE_PATTERN.match("arr1[func(mapping['a'])] = arr2")
assert SUBSET_UPDATE_PATTERN.match("arr1.i[0, 0] = arr2")
assert SUBSET_UPDATE_PATTERN.match("arr1.iflat[0, 0] = arr2")
assert SUBSET_UPDATE_PATTERN.match("arr1.points[0, 0] = arr2")
assert SUBSET_UPDATE_PATTERN.match("arr1.ipoints[0, 0] = arr2")
assert SUBSET_UPDATE_PATTERN.match("arr1[0] += arr2")
assert SUBSET_UPDATE_PATTERN.match("arr1[0] -= arr2")
assert SUBSET_UPDATE_PATTERN.match("arr1[0] *= arr2")
assert SUBSET_UPDATE_PATTERN.match("arr1[0] /= arr2")
assert SUBSET_UPDATE_PATTERN.match("arr1[0] %= arr2")
assert SUBSET_UPDATE_PATTERN.match("arr1[0] //= arr2")
assert SUBSET_UPDATE_PATTERN.match("arr1[0] **= arr2")
assert SUBSET_UPDATE_PATTERN.match("arr1[0] &= arr2")
assert SUBSET_UPDATE_PATTERN.match("arr1[0] |= arr2")
assert SUBSET_UPDATE_PATTERN.match("arr1[0] ^= arr2")
assert SUBSET_UPDATE_PATTERN.match("arr1[0] >>= arr2")
assert SUBSET_UPDATE_PATTERN.match("arr1[0] <<= arr2")
assert SUBSET_UPDATE_PATTERN.match("arr1[0]") is None
assert SUBSET_UPDATE_PATTERN.match("arr1.method()") is None
assert SUBSET_UPDATE_PATTERN.match("arr1[0].method()") is None
assert SUBSET_UPDATE_PATTERN.match("arr1[0].method(arg=thing)") is None
assert SUBSET_UPDATE_PATTERN.match("arr1[0].method(arg==thing)") is None
# this test fails but I don't think it is possible to fix it with regex
# assert SUBSET_UPDATE_PATTERN.match("arr1[func('[]=0')].method()") is None
matching_patterns = [
'arr1[1] = 2',
'arr1[1]= 2',
'arr1[1]=2',
"arr1['a'] = arr2",
"arr1[func(mapping['a'])] = arr2",
"arr1.i[0, 0] = arr2",
"arr1.iflat[0, 0] = arr2",
"arr1.points[0, 0] = arr2",
"arr1.ipoints[0, 0] = arr2",
"arr1[0] += arr2",
"arr1[0] -= arr2",
"arr1[0] *= arr2",
"arr1[0] /= arr2",
"arr1[0] %= arr2",
"arr1[0] //= arr2",
"arr1[0] **= arr2",
"arr1[0] &= arr2",
"arr1[0] |= arr2",
"arr1[0] ^= arr2",
"arr1[0] >>= arr2",
"arr1[0] <<= arr2",
"arr1.data[1] = 2",
"arr1.data = np.array([1, 2, 3])"
]
for pattern in matching_patterns:
match = UPDATE_VARIABLE_PATTERN.match(pattern)
assert match is not None and match.group('variable') == 'arr1'

for pattern in [
"df.loc[1] = 2",
"df.iloc[1] = 2"
]:
match = UPDATE_VARIABLE_PATTERN.match(pattern)
assert match is not None and match.group('variable') == 'df'

# no match
for pattern in [
"arr1[0]",
"arr1.method()",
"arr1[0].method()",
"arr1[0].method(arg=thing)",
"arr1[0].method(arg==thing)",
# this test fails but I don't think it is possible to fix it with regex
# "arr1[func('[]=0')].method()"
]:
assert UPDATE_VARIABLE_PATTERN.match(pattern) is None
6 changes: 5 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ requires = [

[project]
name = "larray-editor"
version = "0.35"
version = "0.35.1-dev"
description = "Graphical User Interface for LArray library"
readme = { file = "README.rst", content-type = "text/x-rst" }

Expand Down Expand Up @@ -67,7 +67,11 @@ excel = ["xlwings"]
# (=PyTables) to load the example datasets from larray
hdf5 = ["tables"]

# project.gui-scripts create .exe files on Windows (like project.scripts would)
# but which call pythonw.exe internally instead of python.exe and thus do not
# open a console when launched
[project.gui-scripts]
# name_of_executable = "module:function"
larray-editor = "larray_editor.start:main"

[project.urls]
Expand Down
Loading