From c5483eb3e55173249981c9d9f3eda034b88638da Mon Sep 17 00:00:00 2001 From: ehennestad Date: Mon, 25 Nov 2024 13:48:47 +0100 Subject: [PATCH 01/12] Add utility methods for checking if an object / classname represents a neurodata type --- +matnwb/+utility/isNeurodataType.m | 16 ++++++++++++++++ +matnwb/+utility/isNeurodataTypeClassName.m | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 +matnwb/+utility/isNeurodataType.m create mode 100644 +matnwb/+utility/isNeurodataTypeClassName.m diff --git a/+matnwb/+utility/isNeurodataType.m b/+matnwb/+utility/isNeurodataType.m new file mode 100644 index 00000000..8f2af154 --- /dev/null +++ b/+matnwb/+utility/isNeurodataType.m @@ -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 diff --git a/+matnwb/+utility/isNeurodataTypeClassName.m b/+matnwb/+utility/isNeurodataTypeClassName.m new file mode 100644 index 00000000..529909ce --- /dev/null +++ b/+matnwb/+utility/isNeurodataTypeClassName.m @@ -0,0 +1,16 @@ +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('types.untyped') + tf = true; + end +end From 03cacbfff5f5fc11c63fc7a2e98af49fa6bb568e Mon Sep 17 00:00:00 2001 From: ehennestad Date: Mon, 25 Nov 2024 13:51:21 +0100 Subject: [PATCH 02/12] Fix bug and improve error for invalid neurodata types in types.util.checkDType --- +types/+util/checkDtype.m | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/+types/+util/checkDtype.m b/+types/+util/checkDtype.m index 8182e9aa..49c6033c 100644 --- a/+types/+util/checkDtype.m +++ b/+types/+util/checkDtype.m @@ -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) @@ -123,7 +124,15 @@ value = unwrapValue(value); end -correctedValue = types.util.correctType(value, typeDescriptor); +if matnwb.utility.isNeurodataType(typeDescriptor) + errorId = 'NWB:CheckDataType:InvalidPropertyValue'; + errorMessage = sprintf(['Expected property 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')... From fb6f887d8256dab5d764280e31d15915e863dea6 Mon Sep 17 00:00:00 2001 From: ehennestad Date: Wed, 27 Nov 2024 10:44:07 +0100 Subject: [PATCH 03/12] Update isNeurodataTypeClassName.m Fix missing arg --- +matnwb/+utility/isNeurodataTypeClassName.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/+matnwb/+utility/isNeurodataTypeClassName.m b/+matnwb/+utility/isNeurodataTypeClassName.m index 529909ce..bc53bdea 100644 --- a/+matnwb/+utility/isNeurodataTypeClassName.m +++ b/+matnwb/+utility/isNeurodataTypeClassName.m @@ -10,7 +10,7 @@ end tf = false; - if startsWith(typeName, 'types.') && ~startsWith('types.untyped') + if startsWith(typeName, 'types.') && ~startsWith(typeName, 'types.untyped') tf = true; end end From 074a9a532c60079306ffa22148bf5b5811d2d59e Mon Sep 17 00:00:00 2001 From: ehennestad Date: Wed, 27 Nov 2024 11:20:45 +0100 Subject: [PATCH 04/12] Try running tests with pynwb pre-release (dev) --- +tests/requirements.txt | 1 - .github/workflows/run_tests.yml | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/+tests/requirements.txt b/+tests/requirements.txt index cc9afd91..c6e14fed 100644 --- a/+tests/requirements.txt +++ b/+tests/requirements.txt @@ -1,3 +1,2 @@ -pynwb hdf5plugin nwbinspector diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index ad7aee46..070ad223 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -28,6 +28,7 @@ jobs: run: | python -m pip install -U pip pip install -r +tests/requirements.txt + pip install -U pynwb --find-links https://github.com/NeurodataWithoutBorders/pynwb/releases/tag/latest --no-index echo "HDF5_PLUGIN_PATH=$(python -c "import hdf5plugin; print(hdf5plugin.PLUGINS_PATH)")" >> "$GITHUB_ENV" echo $( python -m pip show nwbinspector | grep ^Location: | awk '{print $2}' ) - name: Install MATLAB From a8a0db8b36636d8674693a52d7003c725cc1163a Mon Sep 17 00:00:00 2001 From: ehennestad Date: Wed, 27 Nov 2024 11:40:00 +0100 Subject: [PATCH 05/12] Add new error id to whitelist --- +types/+util/checkConstraint.m | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/+types/+util/checkConstraint.m b/+types/+util/checkConstraint.m index 92b58324..2a9c341f 100644 --- a/+types/+util/checkConstraint.m +++ b/+types/+util/checkConstraint.m @@ -18,7 +18,8 @@ function checkConstraint(pname, name, namedprops, constrained, val) expectedErrorTypes = {... 'NWB:CheckDType:InvalidType', ... 'NWB:CheckDType:InvalidShape', ... - 'NWB:TypeCorrection:InvalidConversion'}; + 'NWB:TypeCorrection:InvalidConversion', ... + 'NWB:CheckDataType:InvalidPropertyValue'}; if ~any(strcmp(ME.identifier, expectedErrorTypes)) rethrow(ME); end From 99faf4de9a4ae83fd25cbc4bf1fda406104f0a55 Mon Sep 17 00:00:00 2001 From: ehennestad Date: Wed, 27 Nov 2024 20:16:47 +0100 Subject: [PATCH 06/12] Update run_tests.yml --- .github/workflows/run_tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index 070ad223..db1f7606 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -29,6 +29,7 @@ jobs: python -m pip install -U pip pip install -r +tests/requirements.txt pip install -U pynwb --find-links https://github.com/NeurodataWithoutBorders/pynwb/releases/tag/latest --no-index + python -m pip list echo "HDF5_PLUGIN_PATH=$(python -c "import hdf5plugin; print(hdf5plugin.PLUGINS_PATH)")" >> "$GITHUB_ENV" echo $( python -m pip show nwbinspector | grep ^Location: | awk '{print $2}' ) - name: Install MATLAB From 044badc8f38e0f32f6b13d06a413c6099ad5d9f8 Mon Sep 17 00:00:00 2001 From: ehennestad Date: Wed, 27 Nov 2024 20:17:17 +0100 Subject: [PATCH 07/12] ... --- tools/matnwb_setup.m | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/matnwb_setup.m b/tools/matnwb_setup.m index e6eef6cc..e8c33065 100644 --- a/tools/matnwb_setup.m +++ b/tools/matnwb_setup.m @@ -9,3 +9,4 @@ matnwb_installGitHooks() matnwb_installm2html(fileparts(currentFolder)) + From 9ebaa33927f0a68450e979ab0c3c01ba9748e4af Mon Sep 17 00:00:00 2001 From: ehennestad Date: Wed, 27 Nov 2024 20:41:44 +0100 Subject: [PATCH 08/12] Update requirements to use dev from pynwb and nwbinspector --- +tests/requirements.txt | 3 ++- .github/workflows/run_tests.yml | 1 - tools/matnwb_setup.m | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/+tests/requirements.txt b/+tests/requirements.txt index c6e14fed..f419ca56 100644 --- a/+tests/requirements.txt +++ b/+tests/requirements.txt @@ -1,2 +1,3 @@ hdf5plugin -nwbinspector +git+https://github.com/NeurodataWithoutBorders/nwbinspector@dev +git+https://github.com/NeurodataWithoutBorders/pynwb.git@dev \ No newline at end of file diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index db1f7606..89877a0f 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -28,7 +28,6 @@ jobs: run: | python -m pip install -U pip pip install -r +tests/requirements.txt - pip install -U pynwb --find-links https://github.com/NeurodataWithoutBorders/pynwb/releases/tag/latest --no-index python -m pip list echo "HDF5_PLUGIN_PATH=$(python -c "import hdf5plugin; print(hdf5plugin.PLUGINS_PATH)")" >> "$GITHUB_ENV" echo $( python -m pip show nwbinspector | grep ^Location: | awk '{print $2}' ) diff --git a/tools/matnwb_setup.m b/tools/matnwb_setup.m index e8c33065..e6eef6cc 100644 --- a/tools/matnwb_setup.m +++ b/tools/matnwb_setup.m @@ -9,4 +9,3 @@ matnwb_installGitHooks() matnwb_installm2html(fileparts(currentFolder)) - From e8392edc8dcab6785fd28e072ab20be634920825 Mon Sep 17 00:00:00 2001 From: ehennestad Date: Thu, 28 Nov 2024 15:04:36 +0100 Subject: [PATCH 09/12] Change error id in types.util.checkDType --- +types/+util/checkConstraint.m | 4 ++-- +types/+util/checkDtype.m | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/+types/+util/checkConstraint.m b/+types/+util/checkConstraint.m index 2a9c341f..bad61857 100644 --- a/+types/+util/checkConstraint.m +++ b/+types/+util/checkConstraint.m @@ -18,8 +18,8 @@ function checkConstraint(pname, name, namedprops, constrained, val) expectedErrorTypes = {... 'NWB:CheckDType:InvalidType', ... 'NWB:CheckDType:InvalidShape', ... - 'NWB:TypeCorrection:InvalidConversion', ... - 'NWB:CheckDataType:InvalidPropertyValue'}; + 'NWB:CheckDType:InvalidNeurodataType', ... + 'NWB:TypeCorrection:InvalidConversion'}; if ~any(strcmp(ME.identifier, expectedErrorTypes)) rethrow(ME); end diff --git a/+types/+util/checkDtype.m b/+types/+util/checkDtype.m index 49c6033c..e02a1b09 100644 --- a/+types/+util/checkDtype.m +++ b/+types/+util/checkDtype.m @@ -125,8 +125,8 @@ end if matnwb.utility.isNeurodataType(typeDescriptor) - errorId = 'NWB:CheckDataType:InvalidPropertyValue'; - errorMessage = sprintf(['Expected property value for `%s` to be of ', ... + 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; From fe1c25a8410132ec87aea27e300ae422d9d5c0f1 Mon Sep 17 00:00:00 2001 From: ehennestad Date: Thu, 12 Dec 2024 09:43:21 +0100 Subject: [PATCH 10/12] Fix errorID in linkTest --- +tests/+unit/linkTest.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/+tests/+unit/linkTest.m b/+tests/+unit/linkTest.m index 05f462f3..986f9665 100644 --- a/+tests/+unit/linkTest.m +++ b/+tests/+unit/linkTest.m @@ -100,7 +100,7 @@ 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(... @@ -108,5 +108,5 @@ function createElectrodeGroupWithWrongDeviceType() 'device', not_a_device); %#ok end testCase.verifyError(@createElectrodeGroupWithWrongDeviceType, ... - 'NWB:TypeCorrection:InvalidConversion') + 'NWB:CheckDType:InvalidNeurodataType') end From b76c07c1e9f70b044dea658c75008c98261c7f37 Mon Sep 17 00:00:00 2001 From: ehennestad Date: Thu, 12 Dec 2024 09:48:43 +0100 Subject: [PATCH 11/12] Add test for new utility function --- +tests/+unit/FunctionTests.m | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/+tests/+unit/FunctionTests.m b/+tests/+unit/FunctionTests.m index 4b16b445..2e739c5c 100644 --- a/+tests/+unit/FunctionTests.m +++ b/+tests/+unit/FunctionTests.m @@ -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 \ No newline at end of file From 153fa2dddd097be74a84fbfbdba6c6a32e49bbe6 Mon Sep 17 00:00:00 2001 From: ehennestad Date: Thu, 12 Dec 2024 11:04:25 +0100 Subject: [PATCH 12/12] Fix +matnwb/+utility/isNeurodataTypeClassName.m Make check more robust --- +matnwb/+utility/isNeurodataTypeClassName.m | 46 ++++++++++++++++++++- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/+matnwb/+utility/isNeurodataTypeClassName.m b/+matnwb/+utility/isNeurodataTypeClassName.m index bc53bdea..69f61cd8 100644 --- a/+matnwb/+utility/isNeurodataTypeClassName.m +++ b/+matnwb/+utility/isNeurodataTypeClassName.m @@ -8,9 +8,51 @@ arguments typeName (1,1) string end - + tf = false; if startsWith(typeName, 'types.') && ~startsWith(typeName, 'types.untyped') - tf = true; + 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 \ No newline at end of file