Skip to content
Draft
12 changes: 8 additions & 4 deletions +file/fillClass.m
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
%name is the name of the scheme
%namespace is the namespace context for this class

customBaseClasses = struct( ...
'DynamicTable', 'matnwb.neurodata.DynamicTableBase' ...
);

%% PROCESSING
class = processed(1);

Expand Down Expand Up @@ -96,6 +100,10 @@
superclassNames{end+1} = 'matnwb.mixin.HasUnnamedGroups';
end

if isfield(customBaseClasses, name)
superclassNames{end+1} = customBaseClasses.(name);
end

%% return classfile string
classDefinitionHeader = [...
'classdef ' name ' < ' strjoin(superclassNames, ' & ') newline... %header, dependencies
Expand Down Expand Up @@ -172,10 +180,6 @@
methodBody = strjoin({methodBody, '%% CUSTOM CONSTRAINTS', customConstraintStr}, newline);
end

if strcmp(name, 'DynamicTable')
methodBody = strjoin({methodBody, '%% TABLE METHODS', file.fillDynamicTableMethods()}, newline);
end

fullMethodBody = strjoin({'methods' ...
file.addSpaces(methodBody, 4) 'end'}, newline);
readPolicyMethodBlock = fillReadPolicy(class, classprops);
Expand Down
2 changes: 1 addition & 1 deletion +file/fillConstructor.m
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@

% Add custom validation for DynamicTable and its descendant classes
if file.isDynamicTableDescendant(name, namespace)
constructorElements{end+1} = ' types.util.dynamictable.checkConfig(obj);';
constructorElements{end+1} = ' obj.ensureDynamicTableConsistency();';
end

constructorElements{end+1} = 'end';
Expand Down
2 changes: 1 addition & 1 deletion +file/fillCustomConstraint.m
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
customConstraintStr = sprintf( [...
'function checkCustomConstraint(obj)\n', ...
' checkCustomConstraint@types.untyped.MetaClass(obj)\n', ...
' types.util.dynamictable.checkConfig(obj)\n', ...
' obj.ensureDynamicTableConsistency()\n', ...
'end'] );

otherwise
Expand Down
23 changes: 0 additions & 23 deletions +file/fillDynamicTableMethods.m

This file was deleted.

222 changes: 222 additions & 0 deletions +matnwb/+neurodata/DynamicTableBase.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
classdef (Abstract) DynamicTableBase < handle
% DynamicTableBase - Non-generated base class for DynamicTable behavior.
%
% This class owns handwritten DynamicTable behavior that the generated
% schema class cannot express: row and column mutation helpers, row
% retrieval, table conversion, table clearing, and DynamicTable consistency
% validation.

properties (Abstract)
id
colnames
end

methods
function addRow(obj, columnName, columnValue, options)
% addRow - Add a single row to the DynamicTable.
%
% Syntax:
% dynamicTable.addRow(columnName, columnValue, ..., columnNameN, columnValueN)
% append a single row to the DynamicTable.
%
% dynamicTable.addRow(__, Name, Value) add a row, providing an
% optional value for the 'id' using the optional 'id' name-value
% argument
%
% Input Arguments (Repeating):
% - columnName (string) -
% Name of a column in the table.
%
% - columnValue (any) -
% Corresponding value for the preceding columnName.
%
% Name-Value Arguments:
% - id (int) -
% A custom value for the id of the row being added.

arguments
obj (1,1) matnwb.neurodata.DynamicTableBase {matnwb.common.validation.mustBeDynamicTable}
end
arguments (Repeating)
columnName (1,1) string
columnValue
end
arguments
options.id
end

assert(~isempty(columnName), ...
'NWB:DynamicTable:AddRow:NoData', 'Not enough arguments')

obj.assertIsEditable('NWB:DynamicTable:AddRow:Uneditable')

assert(~isempty(obj.colnames), ...
'NWB:DynamicTable:AddRow:NoColumns',...
['The `colnames` property of the DynamicTable needs to be ', ...
'populated with a cell array of column names before being ', ...
'able to add row data.'])

obj.ensureDynamicTableConsistency()

columnValuePairs = [columnName; columnValue];
optionalArgs = namedargs2cell(options);

types.util.dynamictable.addVarargRow(obj, columnValuePairs{:}, optionalArgs{:});
end

function addColumn(obj, columnName, columnVector)
% addColumn - Add one or more columns to the DynamicTable.
%
% Given a dynamic table and a set of keyword arguments for one or
% more columns, add one or more columns to the dynamic table by
% providing name-value pairs where the name is a column name and
% the value is a column vector
%
% Syntax:
% dynamicTable.addColumn(columnName, columnVector)
% add a single column to the DynamicTable.
%
% dynamicTable.addColumn(columnName, columnVector, ..., columnNameN, columnVectorN)
% add many new columns to the DynamicTable
%
% Input Arguments (Repeating):
% - columnName (string) -
% Name of the new column in the table.
%
% - columnVector (VectorData | VectorIndex) -
% Corresponding VectorData or VectorIndex for the new column
%
% Note:
% The height of the columns to be appended must match the height of
% the existing columns

arguments
obj (1,1) {matnwb.common.validation.mustBeDynamicTable}
end

arguments (Repeating)
columnName (1,1) string
columnVector
end

assert(~isempty(columnName), ...
'NWB:DynamicTable:AddColumn:NoData', 'Not enough arguments')

if isempty(obj.id)
types.util.dynamictable.internal.initDynamicTableId(obj);
end

obj.assertIsEditable('NWB:DynamicTable:AddColumn:Uneditable')

columnVectorPairs = [columnName; columnVector];
types.util.dynamictable.addVarargColumn(obj, columnVectorPairs{:});
end

function row = getRow(obj, rowIndices, options)
% getRow - Return one or more DynamicTable rows.
%
% Syntax:
% dynamicTable.getRow(rowIndices) return one or more rows of the
% table given a scalar row index or a list of row indices.
%
% dynamicTable.getRow(rowIndices, Name, Value) get rows providing
% optional name-value pairs for customization (see Input Arguments).
%
% Input Arguments:
% - rowIndices (double) -
% A scalar index or a vector of row indices for rows to extract.
% Must be positive integers, respecting the row count of the table.
%
% - options (name-value pairs) -
% Optional name-value pairs. Available options:
%
% - columns (string) -
% A list of names of columns to retrieve. Allows for only
% grabbing certain columns instead of returning all columns.
%
% - useId (logical) -
% If true, rowIndices refer to the table's id column instead
% of the MATLAB-based row indices.
%
% Output Arguments:
% - row (table) -
% A table of specified rows, with columns ordered according to
% the DynamicTable's colnames property, or the values given for
% the "columns" option if provided.

arguments
obj (1,1) {matnwb.common.validation.mustBeDynamicTable}
rowIndices (1,:) double {mustBeInteger}
options.columns (1,:)
options.useId (1,1) logical
end

nvPairs = namedargs2cell(options);
row = types.util.dynamictable.getRow(obj, rowIndices, nvPairs{:});
end

function table = toTable(obj, keepRegionsIndexed)
% toTable - Convert the DynamicTable to a MATLAB table.
%
% Syntax:
% dynamicTable.toTable() converts the DynamicTable object to a
% MATLAB table. DynamicTableRegion columns are kept as index
% references by default.
%
% dynamicTable.toTable(keepRegionsIndexed) controls how
% DynamicTableRegion columns are represented (see Input Arguments).
%
% Input Arguments:
% - keepRegionsIndexed (logical) -
% When true (default), each DynamicTableRegion column is preserved
% as row indices into the referenced table. When false, each
% DynamicTableRegion column is expanded into a nested subtable of
% the referenced rows.

arguments
obj (1,1) {matnwb.common.validation.mustBeDynamicTable}
keepRegionsIndexed (1,1) logical = true
end

table = types.util.dynamictable.nwbToTable(obj, keepRegionsIndexed);
end

function clear(obj)
% clear - Remove all row and column data from the DynamicTable.
%
% Resets the table to an empty state: all VectorData columns,
% VectorIndex columns, and row ids are cleared. The colnames
% property is preserved.

types.util.dynamictable.clear(obj);
end
end

methods (Hidden)
function ensureDynamicTableConsistency(obj)
% ensureDynamicTableConsistency - Ensure DynamicTable column consistency.
%
% This method validates column registration, row-height consistency,
% compound column shape, VectorIndex chains, and id height. It may
% also initialize missing ids when the table height can be inferred
% from materialized columns.

types.util.dynamictable.checkConfig(obj);
end
end

methods (Access = private)
function assertIsEditable(obj, errorID)
arguments
obj (1,1) matnwb.neurodata.DynamicTableBase
errorID (1,1) string = "NWB:DynamicTable:Uneditable"
end

isEditable = ~isa(obj.id.data, 'types.untyped.DataStub');

assert(isEditable, errorID, ...
['Cannot write to on-file Dynamic Tables without enabling data pipes. '...
'If this was produced with pynwb, please enable chunking for this table.']);
end
end
end
7 changes: 0 additions & 7 deletions +tests/+sanity/GenerationTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,6 @@ function dynamicTableMethodsTest(testCase)
'stringdata', {'TRUE'},...
'id', id(i));
end
t = table(id(6:10), (6:10)', (7:11)', ...
rand(5,1), repmat({'TRUE'}, 5, 1), ...
'VariableNames', {'id', 'start_time', 'stop_time', 'randomvalues', 'stringdata'});
% verify error is thrown when addRow input is MATLAB table
testCase.verifyError(@() TimeIntervals.addRow(t), ...
"NWB:DynamicTable" ...
);

retrievalIndex = round(1 + 4 .* rand(10, 1));
indexedRow = TimeIntervals.getRow(retrievalIndex);
Expand Down
8 changes: 0 additions & 8 deletions +tests/+system/DynamicTableTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -117,14 +117,6 @@ function appendContainer(testCase, file)
'data', (20:-1:1) .' ...
) ...
);
% verify error is thrown when addRow input is MATLAB table
t = table( ...
(1:2:40)', (1:4:80)' , ...
'VariableNames', {'newcolumn2', 'newcolumn3'} ...
);
testCase.verifyError(@() file.intervals_trials.addColumn(t), ...
"NWB:DynamicTable" ...
);
end

function appendRaggedContainer(~, file)
Expand Down
16 changes: 6 additions & 10 deletions +tests/+unit/+io/testCreateParsedType.m
Original file line number Diff line number Diff line change
Expand Up @@ -37,20 +37,16 @@ function testCreateTypeWithInvalidInputs(testCase)
end

function testCreateTypeWithInvalidPropertyValue(testCase)
% Invalid property values warn and remain accessible during reads.
testPath = 'some/dataset/path';
testType = 'types.hdmf_common.VectorIndex';
kwargs = {'data', 'text is not numeric indices'};

try
io.createParsedType(testPath, testType, kwargs{:});
testCase.verifyFail('Expected io.createParsedType to throw an error.');
catch exception
testCase.verifyEqual(...
exception.identifier, ...
'NWB:createParsedType:TypeCreationFailed');
testCase.verifyNotEmpty(strfind(exception.message, testType));
testCase.verifyNotEmpty(strfind(exception.message, testPath));
end
type = testCase.verifyWarning( ...
@() io.createParsedType(testPath, testType, kwargs{:}), ...
'NWB:CheckDataType:InvalidConversion');

testCase.verifyClass(type, testType)
end

function testCreateDynamicTableWithDuplicateColnamesWarns(testCase)
Expand Down
32 changes: 32 additions & 0 deletions +tests/+unit/+types/+validators/CheckConstraintTest.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
classdef CheckConstraintTest < matlab.unittest.TestCase
% CheckConstraintTest - Unit tests for types.util.checkConstraint.

methods (Test)
function testValueMatchingNoConstrainedTypeErrorsInEditContext(testCase)
testCase.verifyError( ...
@() types.util.checkConstraint('group', 'item', struct(), ...
{'types.hdmf_common.VectorData'}, 5), ...
'NWB:CheckConstraint:InvalidType')
end

function testValueMatchingNoConstrainedTypeWarnsInReadContext(testCase)
[~, cleanup] = matnwb.common.validation.internal.context("read"); %#ok<ASGLU>

value = testCase.verifyWarning( ...
@() types.util.checkConstraint('group', 'item', struct(), ...
{'types.hdmf_common.VectorData'}, 5), ...
'NWB:CheckConstraint:InvalidType');
testCase.verifyEqual(value, 5)
end

function testReadContextProbesConstrainedTypesStrictly(testCase)
[~, cleanup] = matnwb.common.validation.internal.context("read"); %#ok<ASGLU>

value = testCase.verifyWarningFree( ...
@() types.util.checkConstraint('group', 'item', struct(), ...
{'types.hdmf_common.VectorData', 'double'}, 5));

testCase.verifyEqual(value, 5)
end
end
end
Loading
Loading