From 1e9259ec28172d0f1793fd10d3f4868df12549a1 Mon Sep 17 00:00:00 2001 From: ehennestad Date: Sat, 19 Sep 2026 13:13:27 +0200 Subject: [PATCH] fix(session): load and save data variables with function-based adapters A function-based file adapter, such as MatFile of the core module, is a folder with a fileadapter.json and read and write functions, not a class. VariableModel.updateVariableDataType got a variable's data type by creating the adapter class, so adding a variable with such an adapter failed, and Session.loadData called the folder's package name as a function. The DSM converter gives a .mat file pattern the first adapter that lists .mat, which is MatFile. The data type now comes from the adapter list, which reads it from fileadapter.json. Session wraps a function-based adapter in DynamicFileAdapter, as nansen.load does. DynamicFileAdapter.readData and writeData accept the variable name that FileAdapter.load and save pass on, so Session.saveData no longer fails with "Too many input arguments". Co-Authored-By: Claude Opus 5 --- .../+nansen/+config/+varmodel/VariableModel.m | 21 ++---- code/+nansen/+dataio/DynamicFileAdapter.m | 8 ++- code/+nansen/+metadata/+type/Session.m | 12 +++- .../+session/FunctionBasedFileAdapterTest.m | 65 +++++++++++++++++++ 4 files changed, 88 insertions(+), 18 deletions(-) create mode 100644 tests/+nansen/+unittest/+session/FunctionBasedFileAdapterTest.m diff --git a/code/+nansen/+config/+varmodel/VariableModel.m b/code/+nansen/+config/+varmodel/VariableModel.m index eabc1490b..23c858ab3 100644 --- a/code/+nansen/+config/+varmodel/VariableModel.m +++ b/code/+nansen/+config/+varmodel/VariableModel.m @@ -162,8 +162,6 @@ function updateDefaultValues(obj) dlModel = nansen.DataLocationModel(); end - fileAdapterList = nansen.dataio.listFileAdapters(); - for i = 1:numel(obj.Data) if isempty( obj.Data(i).FileAdapter ) || strcmp(obj.Data(i).FileAdapter, str) obj.Data(i).FileAdapter = 'Default'; @@ -179,15 +177,7 @@ function updateDefaultValues(obj) if ~isfield(obj.Data, 'DataType') % Infer from file adapter [obj.Data(:).DataType] = deal(''); for i = 1:numel(obj.Data) - if ~strcmp(obj.Data(i).FileAdapter, 'Default') - isMatch = strcmp({fileAdapterList.FileAdapterName}, obj.Data(i).FileAdapter); - if any(isMatch) - fileAdapterFcn = str2func(fileAdapterList(isMatch).FunctionName); - obj.Data(i).DataType = fileAdapterFcn().DataType; - else - % pass - end - end + obj.Data(i) = obj.updateVariableDataType(obj.Data(i)); end else % Normalize existing DataType to char. Todo: support string @@ -659,15 +649,16 @@ function disableNotifications(obj) methods (Static) % Todo: Should be moved to a data variable class function variableItem = updateVariableDataType(variableItem) + % The list holds the data type of every file adapter, also of a + % function-based one, which is not a class that can be created fileAdapterList = nansen.dataio.listFileAdapters(); if ~strcmp(variableItem.FileAdapter, 'Default') isMatch = strcmp({fileAdapterList.FileAdapterName}, variableItem.FileAdapter); if any(isMatch) - fileAdapterFcn = str2func(fileAdapterList(isMatch).FunctionName); - - variableItem.DataType = fileAdapterFcn().DataType; + variableItem.DataType = char(fileAdapterList(find(isMatch, 1)).DataType); else - % pass + % An adapter that is not in the current project keeps + % the data type the item has end end end diff --git a/code/+nansen/+dataio/DynamicFileAdapter.m b/code/+nansen/+dataio/DynamicFileAdapter.m index c02f3e09c..bdb486a8a 100644 --- a/code/+nansen/+dataio/DynamicFileAdapter.m +++ b/code/+nansen/+dataio/DynamicFileAdapter.m @@ -47,11 +47,15 @@ end methods (Access = protected) - function data = readData(obj) + function data = readData(obj, varargin) + % Session.loadData passes the variable name and name-value + % options to load; the read function takes the file only data = obj.ReadFunction(obj.Filename); end - function writeData(obj, data) + function writeData(obj, data, varargin) + % Session.saveData passes the variable name; the write + % function takes the file and the data only if ~isempty(obj.WriteFunction) obj.WriteFunction(obj.Filename, data) else diff --git a/code/+nansen/+metadata/+type/Session.m b/code/+nansen/+metadata/+type/Session.m index 4f104ba70..0e4b18328 100644 --- a/code/+nansen/+metadata/+type/Session.m +++ b/code/+nansen/+metadata/+type/Session.m @@ -717,7 +717,17 @@ function updateSessionFolder(obj, dataLocationName, folderPath) %#ok error('NANSEN:Session:MultipleFileAdapters', 'This is a bug. Please report') end - fileAdapterFcn = str2func(fileAdapterList(isMatch).FunctionName); + fileAdapterInfo = fileAdapterList(isMatch); + if isfield(fileAdapterInfo, 'IsDynamic') && isequal(fileAdapterInfo.IsDynamic, true) + % A function-based file adapter is a folder with a + % fileadapter.json and read and write functions, not a + % class, and DynamicFileAdapter wraps it, as nansen.load does + functionName = fileAdapterInfo.FunctionName; + fileAdapterFcn = @(filePath, varargin) ... + nansen.dataio.DynamicFileAdapter(functionName, filePath); + else + fileAdapterFcn = str2func(fileAdapterInfo.FunctionName); + end end function viewDataVariable(obj, varName) diff --git a/tests/+nansen/+unittest/+session/FunctionBasedFileAdapterTest.m b/tests/+nansen/+unittest/+session/FunctionBasedFileAdapterTest.m new file mode 100644 index 000000000..5293f8c0c --- /dev/null +++ b/tests/+nansen/+unittest/+session/FunctionBasedFileAdapterTest.m @@ -0,0 +1,65 @@ +classdef FunctionBasedFileAdapterTest < matlab.unittest.TestCase +%FunctionBasedFileAdapterTest - Data variables whose file adapter is function-based +% +% A function-based file adapter is a folder with a fileadapter.json and +% read and write functions, such as MatFile of the core module. The mock +% project gets the variable Values, with the adapter MatFile, and a .mat +% file for it in the first session folder. +% +% Run tests: +% runtests('nansen.unittest.session.FunctionBasedFileAdapterTest') + + properties + Project + Session + end + + methods (TestClassSetup) + function setupProject(testCase) + testCase.applyFixture(nansen.fixture.ProjectFixture) + testCase.Project = nansen.getCurrentProject(); + + % The new item copies the fields of the mock project's item, + % which has fields that a default item lacks + variableModel = testCase.Project.VariableModel; + item = variableModel.Data(1); + item.VariableName = 'Values'; + item.Uuid = nansen.util.getuuid(); + item.FilePathPattern = ''; + item.FileNameExpression = 'values'; + item.FileType = '.mat'; + item.FileAdapter = 'MatFile'; + item.DataType = ''; + variableModel.insertItem(item); + variableModel.save() + + testCase.Session = testCase.Project.getSessionObjects( ... + testCase.Project.MetaTableCatalog.getMasterMetaTable('session').entries.sessionID{1}); + Values = magic(3); + save(fullfile(testCase.Session.getSessionFolder('MockData'), 'values.mat'), 'Values') + end + end + + methods (Test) + function testInsertedVariableGetsDataTypeOfAdapter(testCase) + item = testCase.Project.VariableModel.getItem('Values'); + + testCase.verifyEqual(item.DataType, 'struct') + end + + function testLoadDataReadsWithFunctionBasedAdapter(testCase) + data = testCase.Session.loadData('Values'); + + testCase.verifyEqual(data.Values, magic(3)) + end + + function testSaveDataWritesWithFunctionBasedAdapter(testCase) + testCase.addTeardown(@() testCase.Session.saveData('Values', struct('Values', magic(3)))) + + testCase.Session.saveData('Values', struct('Values', magic(4))) + + data = testCase.Session.loadData('Values'); + testCase.verifyEqual(data.Values, magic(4)) + end + end +end