From 807ab00d228497b24612f72c797d3bba3faa3089 Mon Sep 17 00:00:00 2001 From: Ray Osborn Date: Sat, 9 May 2026 18:28:04 -0500 Subject: [PATCH 01/10] Add Claude settings to .gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index d7ad57e5..7c316946 100644 --- a/.gitignore +++ b/.gitignore @@ -64,5 +64,8 @@ src/nexusformat/_version.py # miscellaneous system files .directoryhash +# Claude settings +.claude + # uv uv.lock From 4232589cb5089b485b10cc72d12b3b9d74ec9253 Mon Sep 17 00:00:00 2001 From: Ray Osborn Date: Sat, 9 May 2026 18:28:51 -0500 Subject: [PATCH 02/10] Fix the natural sort function to handle path-like objects --- src/nexusformat/nexus/tree.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/nexusformat/nexus/tree.py b/src/nexusformat/nexus/tree.py index 7a969a0c..56cec2fc 100644 --- a/src/nexusformat/nexus/tree.py +++ b/src/nexusformat/nexus/tree.py @@ -352,7 +352,7 @@ def natural_sort(key): Parameters ---------- - key : str + key : str or Path String in the list to be sorted. Returns @@ -360,7 +360,7 @@ def natural_sort(key): list List of components splitting embedded numbers as integers. """ - return [int(t) if t.isdigit() else t for t in re.split(r'(\d+)', key)] + return [int(t) if t.isdigit() else t for t in re.split(r'(\d+)', str(key))] class NeXusError(Exception): From b25ee5cb848a9b4ce959658c9bd7799bafc6640d Mon Sep 17 00:00:00 2001 From: Ray Osborn Date: Sat, 9 May 2026 18:30:02 -0500 Subject: [PATCH 03/10] Set file mode to 'rw' after using the NXobject save function --- src/nexusformat/nexus/tree.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/nexusformat/nexus/tree.py b/src/nexusformat/nexus/tree.py index 56cec2fc..f7ce30c5 100644 --- a/src/nexusformat/nexus/tree.py +++ b/src/nexusformat/nexus/tree.py @@ -2345,10 +2345,7 @@ def save(self, filename=None, mode='w-', **kwargs): f.writefile(root) root = f._root root._file = f - if mode == 'w' or mode == 'w-': - root._mode = 'rw' - else: - root._mode = mode + root._mode = 'rw' self.set_changed() return root else: From 831d26b9675621f2aeb02781c4717ee655733388 Mon Sep 17 00:00:00 2001 From: Ray Osborn Date: Sat, 9 May 2026 18:30:41 -0500 Subject: [PATCH 04/10] Improve efficiency of the `plot_shape` function --- src/nexusformat/nexus/tree.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/nexusformat/nexus/tree.py b/src/nexusformat/nexus/tree.py index f7ce30c5..a377d152 100644 --- a/src/nexusformat/nexus/tree.py +++ b/src/nexusformat/nexus/tree.py @@ -4380,17 +4380,9 @@ def plot_shape(self): """ Shape of NXfield for plotting. - Size-1 axes are removed from the shape for multidimensional - data. + Size-1 axes are removed from the shape. """ - try: - _shape = list(self.shape) - if len(_shape) > 1: - while 1 in _shape: - _shape.remove(1) - return tuple(_shape) - except Exception: - return () + return tuple(s for s in self.shape if s > 1) @property def plot_rank(self): From 2ce8d8e758b320abfbfee38d622fd3f884e8e9d2 Mon Sep 17 00:00:00 2001 From: Ray Osborn Date: Sat, 9 May 2026 18:31:31 -0500 Subject: [PATCH 05/10] Fix a bug when copying virtual datasets --- src/nexusformat/nexus/tree.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/nexusformat/nexus/tree.py b/src/nexusformat/nexus/tree.py index a377d152..acf8c515 100644 --- a/src/nexusformat/nexus/tree.py +++ b/src/nexusformat/nexus/tree.py @@ -4603,7 +4603,10 @@ def __deepcopy__(self, memo={}): dpcpy._vidx = copy(obj._vidx) dpcpy._vpath = copy(obj._vpath) dpcpy._vfiles = copy(obj._vfiles) - shape = (len(obj._vfiles),) + slice_shape(obj._vidx, obj._vshape) + if obj._vidx: + shape = (len(obj._vfiles),) + slice_shape(obj._vidx, obj._vshape) + else: + shape = (len(obj._vfiles),) + obj._vshape dpcpy._create_virtual_data(shape=shape, idx=obj._vidx) dpcpy._h5opts = copy(obj._h5opts) dpcpy._changed = True From ba7f3f24c0f1ad90d151cc96967564b1aa5e1e31 Mon Sep 17 00:00:00 2001 From: Ray Osborn Date: Sat, 9 May 2026 18:32:46 -0500 Subject: [PATCH 06/10] Change the file mode to 'rw' in NXroot context managers. The main reason for using the context manager is when writin to the NXroot tre. --- src/nexusformat/nexus/tree.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/nexusformat/nexus/tree.py b/src/nexusformat/nexus/tree.py index acf8c515..2a5c8eaa 100644 --- a/src/nexusformat/nexus/tree.py +++ b/src/nexusformat/nexus/tree.py @@ -6356,12 +6356,15 @@ def __enter__(self): Current NXroot instance. """ if self.nxfile: + self._current_mode = self._mode + self._mode = self._file.mode = 'rw' self.nxfile.__enter__() return self def __exit__(self, *args): """Close the NeXus file.""" if self.nxfile: + self._mode = self._file.mode = self._current_mode self.nxfile.__exit__() def serialize(self): From b83ce2812e449a76bb32ebdd21412ea91c9ae2a6 Mon Sep 17 00:00:00 2001 From: Ray Osborn Date: Sat, 9 May 2026 18:33:37 -0500 Subject: [PATCH 07/10] Ensure that auxiliary signals can include NXlinks. --- src/nexusformat/nexus/tree.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/nexusformat/nexus/tree.py b/src/nexusformat/nexus/tree.py index 2a5c8eaa..24851866 100644 --- a/src/nexusformat/nexus/tree.py +++ b/src/nexusformat/nexus/tree.py @@ -7907,7 +7907,8 @@ def nxauxiliary_signals(self, signals): signals = list(signals) if all(isinstance(signal, str) for signal in signals): self.attrs['auxiliary_signals'] = signals - elif all(isinstance(signal, NXfield) for signal in signals): + elif all(isinstance(signal, NXfield) or + isinstance(signal, NXlink)for signal in signals): self.attrs['auxiliary_signals'] = [signal.nxname for signal in signals] else: From 858ee495faf187f31e336f9b7ae1febd257a46fe Mon Sep 17 00:00:00 2001 From: Ray Osborn Date: Sat, 9 May 2026 18:34:45 -0500 Subject: [PATCH 08/10] Add a `set_date` function to the NXprocess class to facilitate updates --- src/nexusformat/nexus/tree.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/nexusformat/nexus/tree.py b/src/nexusformat/nexus/tree.py index 24851866..7e8b1b76 100644 --- a/src/nexusformat/nexus/tree.py +++ b/src/nexusformat/nexus/tree.py @@ -7998,6 +7998,12 @@ def __init__(self, *args, **kwargs): from datetime import datetime as dt self.date = dt.isoformat(dt.today()) + def set_date(self, date=None): + """Set the date to a specific value.""" + from datetime import datetime as dt + if date is None: + date = dt.today() + self.date = dt.isoformat(date) class NXnote(NXgroup): From fd34d3f65498db84cb16ee76012c6ab0342588ec Mon Sep 17 00:00:00 2001 From: Ray Osborn Date: Sat, 9 May 2026 18:35:56 -0500 Subject: [PATCH 09/10] Add auxiliary signals when creating virtual data groups in `nxconsolidate`. --- src/nexusformat/nexus/tree.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/nexusformat/nexus/tree.py b/src/nexusformat/nexus/tree.py index 7e8b1b76..0dca1252 100644 --- a/src/nexusformat/nexus/tree.py +++ b/src/nexusformat/nexus/tree.py @@ -8592,7 +8592,7 @@ def consolidate(files, data_path, scan_path=None, idx=None): Data slice to be used in the virtual field, by default None """ - if isinstance(files[0], str): + if isinstance(files[0], str) or isinstance(files[0], Path): files = [nxload(f) for f in files] if isinstance(data_path, NXdata): data_path = data_path.nxpath @@ -8604,6 +8604,8 @@ def consolidate(files, data_path, scan_path=None, idx=None): else: scan_files = [f for f in files if data_path in f and f[data_path].nxsignal.exists()] + if len(scan_files) == 0: + raise NeXusError(f'{data_path} not found in files') scan_file = scan_files[0] if scan_path: scan_values = [f[scan_path] for f in scan_files] @@ -8618,15 +8620,18 @@ def consolidate(files, data_path, scan_path=None, idx=None): else: scan_axis = NXfield(range(len(scan_files)), name='file_index', long_name='File Index') - signal = scan_file[data_path].nxsignal axes = scan_file[data_path].nxaxes - if idx is not None: - axes = [axis[s] for axis, s in zip(axes, idx)] - sources = [f[signal.nxpath].nxfilename for f in scan_files] - scan_field = NXvirtualfield(signal, sources, shape=signal.shape, idx=idx, - name=signal.nxname) - scan_data = NXdata(scan_field, [scan_axis] + axes, - name=scan_file[data_path].nxname) + for signal in [s for s in scan_file[data_path].nxsignals if s.exists()]: + if idx is not None: + axes = [axis[s] for axis, s in zip(axes, idx)] + sources = [f[signal.nxpath].nxfilename for f in scan_files] + scan_field = NXvirtualfield(signal, sources, shape=signal.shape, + idx=idx, name=signal.nxname) + if signal.nxname == scan_file[data_path].nxsignal.nxname: + scan_data = NXdata(scan_field, [scan_axis] + axes, + name=scan_file[data_path].nxname) + else: + scan_data[signal.nxname] = scan_field scan_data.title = data_path return scan_data From b660ceef31c1b00793a51b8d18ed61839889abca Mon Sep 17 00:00:00 2001 From: Ray Osborn Date: Sun, 10 May 2026 15:34:02 -0500 Subject: [PATCH 10/10] Correct the regular expression for valid NeXus names --- src/nexusformat/nexus/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nexusformat/nexus/utils.py b/src/nexusformat/nexus/utils.py index 42467c87..b24efe03 100644 --- a/src/nexusformat/nexus/utils.py +++ b/src/nexusformat/nexus/utils.py @@ -22,7 +22,7 @@ from dateutil.parser import parse -name_pattern = re.compile(r'^[a-zA-Z0-9_]([a-zA-Z0-9_.]*[a-zA-Z0-9_])?$') +name_pattern = re.compile(r'^[a-zA-Z_][a-zA-Z0-9_]*$') def get_logger():