Skip to content

Commit

Permalink
Improve error message if adding neurodata types of the wrong types as…
Browse files Browse the repository at this point in the history
… property values (#638)

* Add utility methods for checking if an object / classname represents a neurodata type

* Fix bug and improve error for invalid neurodata types in types.util.checkDType

* Update isNeurodataTypeClassName.m

Fix missing arg

* Try running tests with pynwb pre-release (dev)

* Add new error id to whitelist

* Update run_tests.yml

* Update requirements to use dev from pynwb and nwbinspector

* Change error id in types.util.checkDType

* Fix errorID in linkTest

* Add test for new utility function

* Fix +matnwb/+utility/isNeurodataTypeClassName.m

Make check more robust
  • Loading branch information
ehennestad authored Dec 12, 2024
1 parent 349b175 commit bb9acbf
Show file tree
Hide file tree
Showing 7 changed files with 97 additions and 6 deletions.
16 changes: 16 additions & 0 deletions +matnwb/+utility/isNeurodataType.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
function tf = isNeurodataType(value)
% isNeurodataType - Check if a value / object is a neurodata type.
%
% tf = matnwb.utility.isNeurodataType(value) returns true if the value
% is an object of a class representing a neurodata type of the NWB Format.
% If the input is a string representing the class name of a neurodata
% type, the function will also return true.

tf = false;
if isa(value, 'char') || isa(value, 'string')
tf = matnwb.utility.isNeurodataTypeClassName(value);
elseif isa(value, 'types.untyped.MetaClass')
className = class(value);
tf = matnwb.utility.isNeurodataTypeClassName(className);
end
end
58 changes: 58 additions & 0 deletions +matnwb/+utility/isNeurodataTypeClassName.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
function tf = isNeurodataTypeClassName(typeName)
% isNeurodataTypeClassName - Check if a name is the class name of a neurodata type.
%
% tf = matnwb.utility.isNeurodataTypeClassName(value) returns true if a
% string is the class name of a class representing a neurodata type of
% the NWB Format

arguments
typeName (1,1) string
end

tf = false;
if startsWith(typeName, 'types.') && ~startsWith(typeName, 'types.untyped')
mc = meta.class.fromName(typeName);
if ~isempty(mc)
tf = hasSuperClass(mc, 'types.untyped.MetaClass');
end
end
end

function tf = hasSuperClass(mc, superClassName)
% hasSuperClass - Recursively check if a meta.class object has a specific superclass.
%
% tf = hasSuperClass(mc, superClassName) returns true if the meta.class object
% mc has a superclass with the name superClassName, either directly or
% indirectly (through its own superclasses).
%
% Arguments:
% mc - A meta.class object.
% superClassName - The name of the superclass to check for (string).
%
% Returns:
% tf - Logical value indicating if the class has the specified superclass.

arguments
mc meta.class
superClassName (1,1) string
end

% Check if the current class has the desired superclass directly.
for i = 1:numel(mc.SuperclassList)
if mc.SuperclassList(i).Name == superClassName
tf = true;
return;
end
end

% If not, check recursively through each superclass.
for i = 1:numel(mc.SuperclassList)
if hasSuperClass(mc.SuperclassList(i), superClassName)
tf = true;
return;
end
end

% If no match found, return false.
tf = false;
end
7 changes: 7 additions & 0 deletions +tests/+unit/FunctionTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,12 @@ function testWriteCompoundScalar(testCase)
io.writeCompound(fid, '/map_data', data)
H5F.close(fid);
end
function testIsNeurodatatype(testCase)
timeSeries = types.core.TimeSeries();
testCase.verifyTrue(matnwb.utility.isNeurodataType(timeSeries))

dataPipe = types.untyped.DataPipe('data', rand(10,10));
testCase.verifyFalse(matnwb.utility.isNeurodataType(dataPipe))
end
end
end
4 changes: 2 additions & 2 deletions +tests/+unit/linkTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,13 @@ function testDirectTypeAssignmentToSoftLinkProperty(testCase)
end

function testWrongTypeInSoftLinkAssignment(testCase)

% Adding an OpticalChannel as device for ElectrodeGroup should fail.
function createElectrodeGroupWithWrongDeviceType()
not_a_device = types.core.OpticalChannel('description', 'test_channel');
electrodeGroup = types.core.ElectrodeGroup(...
'description', 'test_group', ...
'device', not_a_device); %#ok<NASGU>
end
testCase.verifyError(@createElectrodeGroupWithWrongDeviceType, ...
'NWB:TypeCorrection:InvalidConversion')
'NWB:CheckDType:InvalidNeurodataType')
end
2 changes: 1 addition & 1 deletion +tests/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
hdf5plugin
git+https://github.com/NeurodataWithoutBorders/nwbinspector.git@dev
git+https://github.com/NeurodataWithoutBorders/pynwb.git@dev
git+https://github.com/NeurodataWithoutBorders/pynwb.git@dev
1 change: 1 addition & 0 deletions +types/+util/checkConstraint.m
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ function checkConstraint(pname, name, namedprops, constrained, val)
expectedErrorTypes = {...
'NWB:CheckDType:InvalidType', ...
'NWB:CheckDType:InvalidShape', ...
'NWB:CheckDType:InvalidNeurodataType', ...
'NWB:TypeCorrection:InvalidConversion'};
if ~any(strcmp(ME.identifier, expectedErrorTypes))
rethrow(ME);
Expand Down
15 changes: 12 additions & 3 deletions +types/+util/checkDtype.m
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,12 @@
'Number of elements for each struct field must match to be valid.'], ...
num2str(fieldSizes));
end


parentName = name;
for iField = 1:length(expectedFields)
% validate subfield types.
name = expectedFields{iField};
subName = [name '.' name];
subName = [parentName '.' name];
subType = typeDescriptor.(name);

if (isstruct(value) && isscalar(value)) || istable(value)
Expand Down Expand Up @@ -123,7 +124,15 @@
value = unwrapValue(value);
end

correctedValue = types.util.correctType(value, typeDescriptor);
if matnwb.utility.isNeurodataType(typeDescriptor)
errorId = 'NWB:CheckDType:InvalidNeurodataType';
errorMessage = sprintf(['Expected value for `%s` to be of ', ...
'type `%s`. Instead it was `%s`'], name, typeDescriptor, class(value));
assert(isa(value, typeDescriptor), errorId, errorMessage)
correctedValue = value;
else
correctedValue = types.util.correctType(value, typeDescriptor);
end
% this specific conversion is fine as HDF5 doesn't have a representative
% datetime type. Thus we suppress the warning for this case.
isDatetimeConversion = isa(correctedValue, 'datetime')...
Expand Down

0 comments on commit bb9acbf

Please sign in to comment.