Skip to content

Commit d04c9ab

Browse files
rlystephprince
andauthored
Make ImagingPlane.description optional (#2051)
Co-authored-by: Steph Prince <[email protected]>
1 parent 618df86 commit d04c9ab

File tree

3 files changed

+86
-6
lines changed

3 files changed

+86
-6
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@
3434
- Fixed missing `__nwbfields__` and `_fieldsname` for `NWBData` and its subclasses. @rly [#2082](https://github.com/NeurodataWithoutBorders/pynwb/pull/2082)
3535
- Fixed caching of the type map when using HDMF 4.1.0. @rly [#2087](https://github.com/NeurodataWithoutBorders/pynwb/pull/2087)
3636
- Removed use of complex numbers in scratch tutorial because of incompatibilities with HDMF 4.1.0. @stephprince [#2090](https://github.com/NeurodataWithoutBorders/pynwb/pull/2090/)
37-
37+
- Made `ImagingPlane.description` optional to conform with the NWB Schema. @rly [#2051](https://github.com/NeurodataWithoutBorders/pynwb/pull/2051)
38+
3839
### Documentation and tutorial enhancements
3940
- Added NWB AI assistant to the home page of the documentation. @magland [#2076](https://github.com/NeurodataWithoutBorders/pynwb/pull/2076)
4041

src/pynwb/ophys.py

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,13 @@ class ImagingPlane(NWBContainer):
6868
@docval(*get_docval(NWBContainer.__init__, 'name'), # required
6969
{'name': 'optical_channel', 'type': (list, OpticalChannel), # required
7070
'doc': 'One of possibly many groups storing channel-specific data.'},
71-
{'name': 'description', 'type': str, 'doc': 'Description of this ImagingPlane.'}, # required
72-
{'name': 'device', 'type': Device, 'doc': 'the device that was used to record'}, # required
73-
{'name': 'excitation_lambda', 'type': float, 'doc': 'Excitation wavelength in nm.'}, # required
74-
{'name': 'indicator', 'type': str, 'doc': 'Calcium indicator'}, # required
75-
{'name': 'location', 'type': str, 'doc': 'Location of image plane.'}, # required
71+
{'name': 'description', 'type': str, 'doc': 'Description of this ImagingPlane.', 'default': None},
72+
{'name': 'device', 'type': Device, 'doc': 'the device that was used to record',
73+
'default': None}, # required
74+
{'name': 'excitation_lambda', 'type': float, 'doc': 'Excitation wavelength in nm.',
75+
'default': None}, # required
76+
{'name': 'indicator', 'type': str, 'doc': 'Calcium indicator', 'default': None}, # required
77+
{'name': 'location', 'type': str, 'doc': 'Location of image plane.', 'default': None}, # required
7678
{'name': 'imaging_rate', 'type': float,
7779
'doc': 'Rate images are acquired, in Hz. If the corresponding TimeSeries is present, the rate should be '
7880
'stored there instead.', 'default': None},
@@ -128,6 +130,21 @@ def __init__(self, **kwargs):
128130

129131
if not isinstance(args_to_set['optical_channel'], list):
130132
args_to_set['optical_channel'] = [args_to_set['optical_channel']]
133+
134+
# Note: device, excitation_lambda, indicator, and location are required arguments.
135+
# Description was made to be optional in PyNWB 3.1.0, however to avoid breaking API changes the order of
136+
# the arguments needs to be maintained even though the optional arguments came before the required ones.
137+
# So in docval these required arguments are displayed as optional when really they are required.
138+
# This section can be removed when positional arguments are no longer allowed.
139+
if args_to_set['device'] is None:
140+
raise ValueError("The 'device' argument is required for ImagingPlane.")
141+
if args_to_set['excitation_lambda'] is None:
142+
raise ValueError("The 'excitation_lambda' argument is required for ImagingPlane.")
143+
if args_to_set['indicator'] is None:
144+
raise ValueError("The 'indicator' argument is required for ImagingPlane.")
145+
if args_to_set['location'] is None:
146+
raise ValueError("The 'location' argument is required for ImagingPlane.")
147+
131148
if args_to_set['manifold'] is not None:
132149
error_msg = "The 'manifold' argument is deprecated in favor of 'origin_coords' and 'grid_spacing'."
133150
self._error_on_new_pass_on_construct(error_msg=error_msg)

tests/unit/test_ophys.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ def test_init(self):
118118
grid_spacing_unit='gs_unit'
119119
)
120120
self.assertEqual(ip.optical_channel[0], oc)
121+
self.assertEqual(ip.description, 'description')
121122
self.assertEqual(ip.device, device)
122123
self.assertEqual(ip.excitation_lambda, 600.)
123124
self.assertEqual(ip.imaging_rate, 300.)
@@ -196,6 +197,67 @@ def test_unit_deprecated(self):
196197
obj = ImagingPlane.__new__(ImagingPlane, in_construct_mode=True)
197198
obj.__init__(**kwargs)
198199

200+
def test_init_description_optional(self):
201+
"""Check creation of ImagingPlane with only required dependencies.
202+
203+
This is to check how creation of an ImagingPlane changes when the "description" argument moves from a
204+
required arg in the middle of the required args section to an optional arg, in alignment with the schema.
205+
"""
206+
oc, device = self.set_up_dependencies()
207+
208+
# description is now optional
209+
ip = ImagingPlane(
210+
name='test_imaging_plane',
211+
optical_channel=oc,
212+
device=device,
213+
excitation_lambda=600.,
214+
indicator='indicator',
215+
location='location',
216+
)
217+
self.assertIsNone(ip.description)
218+
219+
# description is still required to be provided when using positional arguments
220+
with warnings.catch_warnings(record=True): # catch positional argument deprecation warning
221+
with self.assertRaises(TypeError):
222+
ImagingPlane(
223+
'test_imaging_plane',
224+
oc,
225+
device,
226+
600.,
227+
'indicator',
228+
'location',
229+
)
230+
231+
def test_init_missing_required_args(self):
232+
"""Check that ImagingPlane raises an error if required args are missing."""
233+
oc, device = self.set_up_dependencies()
234+
235+
with self.assertRaisesWith(ValueError, "The 'device' argument is required for ImagingPlane."):
236+
ImagingPlane(
237+
name='test_imaging_plane',
238+
optical_channel=oc,
239+
)
240+
with self.assertRaisesWith(ValueError, "The 'excitation_lambda' argument is required for ImagingPlane."):
241+
ImagingPlane(
242+
name='test_imaging_plane',
243+
optical_channel=oc,
244+
device=device,
245+
)
246+
with self.assertRaisesWith(ValueError, "The 'indicator' argument is required for ImagingPlane."):
247+
ImagingPlane(
248+
name='test_imaging_plane',
249+
optical_channel=oc,
250+
device=device,
251+
excitation_lambda=600.,
252+
)
253+
with self.assertRaisesWith(ValueError, "The 'location' argument is required for ImagingPlane."):
254+
ImagingPlane(
255+
name='test_imaging_plane',
256+
optical_channel=oc,
257+
device=device,
258+
excitation_lambda=600.,
259+
indicator='indicator',
260+
)
199261

200262
class OnePhotonSeriesConstructor(TestCase):
201263

0 commit comments

Comments
 (0)