Skip to content

Commit 0962965

Browse files
Added dynamic properties to Set (#727)
* Minor refactor to support dynamicprops Set * Rewrite Set to use dynamic properties * Re-add `clear` to Set * fix missing argout setting * vastly simplify Set clear * Fix NwbFile searchFor method for Sets * Undo unsupported syntax * Fix untypedSetTest - change test for construction with function input. - fix whitespace - remove validation function that displays text * Update addRawData.m Revert to use legacy syntax. Support "h5 name" over remapped "matlab-valid name", as remapped names should only be used for dot-syntax * Update addVecInd.m Remove reference to removed property of types.untyped.Set * Fix bugs in updates Set, refactor - Remove internal prefix from property names - Make properties private - Make more methods private - Reintroduce setValidationFunction (as the ValidationFunction prop is private), but make method Hidden because is it is not meant to be used by users. * Update NwbFile.m Keep use legacy syntax for Set, as the name here will refer to the h5 name * Fix failing test, add option to warn or fail if entry for set is invalid * "Standardise" error ids in types.untyped.Set * Update types.untyped.Set rename function validate -> validateEntry change error ids and message update dependent tests * Update untypedSetTest.m Update error id * Refactor constructor of types.untyped.Set Create separate function for extracting names and values for Set from varargin Skip putting names and values in "sourceMap" * Fix incomplete merge * Update Set.m Renamed isOverride to propertyAlreadyExists * Update Set/set - Renamed name to names as this can be an array - Renames val to value as this can be an array - Add validator function for values input * Update Set.m - Renamed elem to currentValue in loop for adding values to set - Fixed bug where wrong variable name was used to if property already exists - Added support for char input in mustBeSamLength validator * Update Set.m Changed header for displayNonScalarObject to be more precise * Update Set.m - Moved private methods further down in file - Update docstrings for some methods - Renamed val to value - Fixed bug where variable reference was not updated during merge - Renamed name to names in `get` where names can be an array * Update Set.m - More specific try/catch block - add method should error if value is invalid type * Fix failing test * Support running test workflow manually (#725) * Update Set.m Fix failing test. * Support option for testing with all releases or latest release when running test workflow manually (#726) * Update run_tests.yml * Update run_tests.yml Reorder code * Update untyped.rst - Updated documentation describing untyped types - Updated documentation for Sets and Anons * Update untyped.rst Updated with suggestions from chatGPT * Create NameRegistry.m * Create DynamicPropertyManager.m * Refactor Set * Update Set.m Rename variables, comments and error messages for more consistent naming * Misc cleanup * More cleanup Change wording Add check for reserved property names * Move warning to separate (internal) function and reuse across classes * Simplify getNameMappingTable method and modify some comments * Update DynamicPropertyManager.m - Updated internal class docstring - Fixed bug in class constructor, assigning input to wrong property - Updated logic in remove method - Renamed validName to propertyName in some places - Added getOriginalNameForPropertyName * Update Set.m - Updated renamed methods from PropertyManager - Added getOriginalName method - Added private method for displaying a warning that is used in two places * Update HasUnnamedGroups.m - Added arguments blocks to user-facing methods - Remove method requires original name for a set entry, will warn if original name does not exist but a property identifier does exist - Rearranged order of methods - ++ * Fix failing tests * Fixed failing test and added new tests for HasUnnamedgroups mixin * Update displayAliasWarning.m Added more detailed function description --------- Co-authored-by: Lawrence <[email protected]>
1 parent 85e4bdd commit 0962965

File tree

14 files changed

+1233
-586
lines changed

14 files changed

+1233
-586
lines changed

+matnwb/+mixin/HasUnnamedGroups.m

Lines changed: 256 additions & 276 deletions
Large diffs are not rendered by default.
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
classdef DynamicPropertyManager < handle
2+
% DynamicPropertyManager - Manages dynamic properties for a target object
3+
4+
% - This class provides a consistent interface for creating and removing
5+
% dynamic properties on a target object.
6+
% - It additionally provides an internal name registry, to keep track of
7+
% original names of properties that might not be valid MATLAB
8+
% identifiers. When adding a property with a name which is not a valid
9+
% MATLAB identifier, a valid alias is registered and used as a name for
10+
% the dynamic property.
11+
%
12+
% Used by types.untyped.Set and matnwb.mixin.HasUnnamedGroups to
13+
% allow users to access neurodata types stored in object properties of
14+
% other neurodata types through dot-syntax.
15+
16+
properties (Access = private)
17+
TargetObject % The object to manage dynamic properties for
18+
DynamicPropertyMap % A containers.Map (name) -> (meta.DynamicProperty)
19+
NameRegistry % NameRegistry instance for name mapping
20+
end
21+
22+
properties (SetAccess = immutable)
23+
PropertyAddedFunction % Function handle called when a property is added
24+
PropertyRemovedFunction % Function handle called when a property is removed
25+
end
26+
27+
methods
28+
function obj = DynamicPropertyManager(targetObject, nameRegistry, propArgs)
29+
% Create a new DynamicPropertyManager for the target object
30+
arguments
31+
targetObject (1,1) dynamicprops
32+
nameRegistry (1,1) matnwb.utility.NameRegistry = matnwb.utility.NameRegistry
33+
propArgs.propertyAddedFunction function_handle = function_handle.empty
34+
propArgs.propertyRemovedFunction function_handle = function_handle.empty
35+
end
36+
37+
obj.TargetObject = targetObject;
38+
obj.DynamicPropertyMap = containers.Map('KeyType', 'char', 'ValueType', 'any');
39+
obj.NameRegistry = nameRegistry;
40+
obj.PropertyAddedFunction = propArgs.propertyAddedFunction;
41+
obj.PropertyRemovedFunction = propArgs.propertyRemovedFunction;
42+
end
43+
44+
function metaProperty = addProperty(obj, originalName, options)
45+
% Add a dynamic property to the target object
46+
arguments
47+
obj matnwb.utility.DynamicPropertyManager
48+
originalName (1,1) string
49+
options.GetMethod function_handle = function_handle.empty
50+
options.SetMethod function_handle = function_handle.empty
51+
options.Dependent (1,1) logical = false
52+
end
53+
54+
% Get or create valid name
55+
if obj.NameRegistry.existOriginalName(originalName)
56+
validName = obj.NameRegistry.getValidName(originalName);
57+
else
58+
validName = obj.NameRegistry.addMapping(originalName);
59+
end
60+
61+
% Check if property already exists
62+
assert(~isprop(obj.TargetObject, validName), ...
63+
'NWB:DynamicPropertyManager:PropertyExists', ...
64+
'Property "%s" already exists on target object', validName);
65+
66+
% Add the property
67+
metaProperty = obj.TargetObject.addprop(validName);
68+
69+
% Set get/set methods if provided
70+
if ~isempty(options.GetMethod)
71+
metaProperty.GetMethod = options.GetMethod;
72+
end
73+
74+
if ~isempty(options.SetMethod)
75+
metaProperty.SetMethod = options.SetMethod;
76+
end
77+
78+
metaProperty.Dependent = options.Dependent;
79+
80+
% Store the property metadata
81+
obj.DynamicPropertyMap(validName) = metaProperty;
82+
83+
% Call the callback if set
84+
if ~isempty(obj.PropertyAddedFunction)
85+
obj.PropertyAddedFunction(originalName);
86+
end
87+
88+
if ~nargout
89+
clear metaProperty
90+
end
91+
end
92+
93+
function removeProperty(obj, name)
94+
% Remove a dynamic property from the target object
95+
arguments
96+
obj matnwb.utility.DynamicPropertyManager
97+
name (1,1) string
98+
end
99+
100+
% Try to remove entry assuming original name is given, fall back
101+
% to removing entry using property name.
102+
if obj.existOriginalName(name)
103+
originalName = name;
104+
propertyName = obj.getPropertyNameForOriginalName(originalName);
105+
elseif obj.existPropertyName(name)
106+
propertyName = name;
107+
originalName = obj.getOriginalNameForPropertyName(propertyName);
108+
else
109+
error('NWB:DynamicPropertyManager:UnknownProperty', ...
110+
'No property with name "%s" exists', name);
111+
end
112+
113+
% Check if the property exists in our map
114+
assert(obj.DynamicPropertyMap.isKey(propertyName), ...
115+
'NWB:DynamicPropertyManager:PropertyNotManaged', ...
116+
'Property "%s" is not managed by this DynamicPropertyManager', propertyName);
117+
118+
% Get the property metadata
119+
propMeta = obj.DynamicPropertyMap(propertyName);
120+
121+
% Delete the property
122+
delete(propMeta);
123+
124+
% Remove from internal maps
125+
obj.DynamicPropertyMap.remove(propertyName);
126+
obj.NameRegistry.removeMapping(propertyName);
127+
128+
% Call the callback if set
129+
if ~isempty(obj.PropertyRemovedFunction)
130+
obj.PropertyRemovedFunction(originalName);
131+
end
132+
end
133+
134+
function originalName = getOriginalNameForPropertyName(obj, propertyName)
135+
originalName = obj.NameRegistry.getOriginalName(propertyName);
136+
end
137+
138+
function propertyName = getPropertyNameForOriginalName(obj, originalName)
139+
propertyName = obj.NameRegistry.getValidName(originalName);
140+
end
141+
142+
function count = getPropertyCount(obj)
143+
count = obj.DynamicPropertyMap.Count;
144+
end
145+
146+
function names = getAllPropertyNames(obj)
147+
% Get all property names
148+
names = obj.DynamicPropertyMap.keys();
149+
end
150+
151+
function names = getAllOriginalNames(obj)
152+
% Get all property names
153+
names = obj.NameRegistry.getAllOriginalNames();
154+
end
155+
156+
function tf = existPropertyName(obj, propertyName)
157+
arguments
158+
obj matnwb.utility.DynamicPropertyManager
159+
propertyName (1,1) string
160+
end
161+
tf = obj.NameRegistry.existValidName(propertyName);
162+
end
163+
164+
function tf = existOriginalName(obj, originalName)
165+
arguments
166+
obj matnwb.utility.DynamicPropertyManager
167+
originalName (1,1) string
168+
end
169+
tf = obj.NameRegistry.existOriginalName(originalName);
170+
end
171+
172+
function T = getPropertyMappingTable(obj)
173+
% Get a table showing property mappings
174+
T = obj.NameRegistry.getNameMappingTable();
175+
end
176+
end
177+
end

+matnwb/+utility/NameRegistry.m

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
classdef NameRegistry < handle
2+
% NameRegistry - Mapping between original NWB names and MATLAB-valid names
3+
%
4+
% Ensures unique, reversible (bi-directional) mapping from original names
5+
% used for data objects in NWB files to valid MATLAB identifiers.
6+
7+
properties (Access = private)
8+
% Map from valid MATLAB names to original names
9+
ValidToOriginalMap
10+
11+
% Map from original names to valid MATLAB names
12+
OriginalToValidMap
13+
end
14+
15+
methods
16+
function obj = NameRegistry()
17+
obj.ValidToOriginalMap = containers.Map('KeyType', 'char', 'ValueType', 'char');
18+
obj.OriginalToValidMap = containers.Map('KeyType', 'char', 'ValueType', 'char');
19+
end
20+
21+
function validName = addMapping(obj, originalName, validName)
22+
% Add a mapping between an original name and a MATLAB-valid name
23+
% If validName is not provided, it will be generated
24+
25+
arguments
26+
obj matnwb.utility.NameRegistry
27+
originalName (1,1) string
28+
validName (1,1) string = missing
29+
end
30+
31+
if ismissing(validName)
32+
validName = obj.createValidName(originalName);
33+
end
34+
35+
assert(~obj.existOriginalName(originalName), ...
36+
'NWB:NameRegistry:DuplicateOriginalName', ...
37+
'The original name "%s" is already mapped', originalName);
38+
39+
assert(~obj.existValidName(validName), ...
40+
'NWB:NameRegistry:DuplicateValidName', ...
41+
'The valid name "%s" is already mapped', validName);
42+
43+
% Add the mapping
44+
obj.ValidToOriginalMap(validName) = originalName;
45+
obj.OriginalToValidMap(originalName) = validName;
46+
end
47+
48+
function removeMapping(obj, nameToRemove)
49+
% Remove a mapping by either valid or original name
50+
51+
if obj.existValidName(nameToRemove)
52+
originalName = obj.getOriginalName(nameToRemove);
53+
obj.ValidToOriginalMap.remove(nameToRemove);
54+
obj.OriginalToValidMap.remove(originalName);
55+
elseif obj.existOriginalName(nameToRemove)
56+
validName = obj.getValidName(nameToRemove);
57+
obj.ValidToOriginalMap.remove(validName);
58+
obj.OriginalToValidMap.remove(nameToRemove);
59+
else
60+
error('NWB:NameRegistry:UnknownName', ...
61+
'No mapping exists for name "%s"', nameToRemove);
62+
end
63+
end
64+
65+
function validName = getValidName(obj, originalName)
66+
% Get the valid MATLAB name for an original name
67+
assert(obj.existOriginalName(originalName), ...
68+
'NWB:NameRegistry:UnknownOriginalName', ...
69+
'No mapping exists for original name "%s"', originalName);
70+
validName = obj.OriginalToValidMap(originalName);
71+
end
72+
73+
function originalName = getOriginalName(obj, validName)
74+
% Get the original name for a valid MATLAB name
75+
assert(obj.existValidName(validName), ...
76+
'NWB:NameRegistry:UnknownValidName', ...
77+
'No mapping exists for valid name "%s"', validName);
78+
originalName = obj.ValidToOriginalMap(validName);
79+
end
80+
81+
function tf = existValidName(obj, validName)
82+
% Check if a valid MATLAB name exists in the mapping
83+
tf = obj.ValidToOriginalMap.isKey(validName);
84+
end
85+
86+
function tf = existOriginalName(obj, originalName)
87+
% Check if an original name exists in the mapping
88+
tf = obj.OriginalToValidMap.isKey(originalName);
89+
end
90+
91+
function validNames = getAllValidNames(obj)
92+
% Return all valid MATLAB names as a 1xn cell array
93+
validNames = obj.ValidToOriginalMap.keys();
94+
end
95+
96+
function originalNames = getAllOriginalNames(obj)
97+
% Return all original names as a 1xn cell array
98+
originalNames = obj.OriginalToValidMap.keys();
99+
end
100+
101+
function T = getNameMappingTable(obj)
102+
% Return a table showing all name mappings
103+
validNames = obj.ValidToOriginalMap.keys();
104+
originalNames = obj.ValidToOriginalMap.values();
105+
106+
tableVariableNames = {'ValidIdentifier', 'OriginalName'};
107+
108+
T = table(string(validNames'), string(originalNames'), ...
109+
'VariableNames', tableVariableNames);
110+
end
111+
end
112+
113+
methods (Access = private)
114+
function validName = createValidName(obj, originalName)
115+
% Create a valid MATLAB name from an original name
116+
baseName = matlab.lang.makeValidName(originalName);
117+
118+
% Ensure uniqueness
119+
if obj.existValidName(baseName)
120+
counter = 1;
121+
while obj.existValidName(sprintf('%s_%d', baseName, counter))
122+
counter = counter + 1;
123+
end
124+
validName = sprintf('%s_%d', baseName, counter);
125+
else
126+
validName = baseName;
127+
end
128+
end
129+
end
130+
end

+tests/+unit/+schema/AnonTest.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
methods (Test)
99
function testAnonDataset(testCase)
1010
import matlab.unittest.fixtures.SuppressedWarningsFixture
11-
warningIdentifier = 'NWB:HasUnnamedGroupsMixin:NotImplemented';
11+
warningIdentifier = 'NWB:HasUnnamedGroups:NotImplemented';
1212
testCase.applyFixture(SuppressedWarningsFixture(warningIdentifier))
1313

1414
ag = types.anon.AnonGroup('ad', types.anon.AnonData('data', 0));

0 commit comments

Comments
 (0)