Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve error message if adding neurodata types of the wrong types as property values #638

Merged
merged 18 commits into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
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;

Check warning on line 57 in +matnwb/+utility/isNeurodataTypeClassName.m

View check run for this annotation

Codecov / codecov/patch

+matnwb/+utility/isNeurodataTypeClassName.m#L57

Added line #L57 was not covered by tests
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
Loading