Repository: NeurodataWithoutBorders/pynwb

Issue:
TwoPhotonSeries data warning
TwoPhotonSeries should not throw warning`UserWarning: missing required attribute 'data' for 'object_name' warnings.warn("missing required attribute '%s' for '%s'" % (spec.name, builder.name))` when `format='external'`

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/NeurodataWithoutBorders/pynwb/pull/596?src=pr&el=h1) Report
> Merging [#596](https://codecov.io/gh/NeurodataWithoutBorders/pynwb/pull/596?src=pr&el=desc) into [dev](https://codecov.io/gh/NeurodataWithoutBorders/pynwb/commit/96b7f081534f26ca1a8a3f3080a6faae914fe5f3?src=pr&el=desc) will **decrease** coverage by `0.02%`.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/NeurodataWithoutBorders/pynwb/pull/596/graphs/tree.svg?width=650&token=2HFN1QHI9R&height=150&src=pr)](https://codecov.io/gh/NeurodataWithoutBorders/pynwb/pull/596?src=pr&el=tree)

```diff
@@            Coverage Diff            @@
##              dev    #596      +/-   ##
=========================================
- Coverage   76.83%   76.8%   -0.03%     
=========================================
  Files          59      59              
  Lines        6354    6356       +2     
  Branches     1257    1258       +1     
=========================================
  Hits         4882    4882              
- Misses       1138    1139       +1     
- Partials      334     335       +1
```


| [Impacted Files](https://codecov.io/gh/NeurodataWithoutBorders/pynwb/pull/596?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/pynwb/image.py](https://codecov.io/gh/NeurodataWithoutBorders/pynwb/pull/596/diff?src=pr&el=tree#diff-c3JjL3B5bndiL2ltYWdlLnB5) | `100% <100%> (ø)` | :arrow_up: |
| [src/pynwb/form/utils.py](https://codecov.io/gh/NeurodataWithoutBorders/pynwb/pull/596/diff?src=pr&el=tree#diff-c3JjL3B5bndiL2Zvcm0vdXRpbHMucHk=) | `88.07% <0%> (-0.71%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/NeurodataWithoutBorders/pynwb/pull/596?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/NeurodataWithoutBorders/pynwb/pull/596?src=pr&el=footer). Last update [96b7f08...3b90f28](https://codecov.io/gh/NeurodataWithoutBorders/pynwb/pull/596?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

bendichter: These commits are a mess due to merging with the YAML reorder, but the diff is right: https://github.com/NeurodataWithoutBorders/pynwb/compare/dev...bendichter:two_photon_warning

Patch:
diff --git a/src/pynwb/data/nwb.image.yaml b/src/pynwb/data/nwb.image.yaml
index ba78d096..964b12bf 100644
--- a/src/pynwb/data/nwb.image.yaml
+++ b/src/pynwb/data/nwb.image.yaml
@@ -19,6 +19,7 @@ groups:
   - name: data
     dtype: int8
     doc: Either binary data containing image or empty.
+    quantity: '?'
     dims:
     - - x
       - y
diff --git a/src/pynwb/image.py b/src/pynwb/image.py
index f79248ad..1c1fcc0a 100644
--- a/src/pynwb/image.py
+++ b/src/pynwb/image.py
@@ -68,6 +68,8 @@ class ImageSeries(TimeSeries):
         bits_per_pixel, dimension, external_file, starting_frame, format = popargs(
             'bits_per_pixel', 'dimension', 'external_file', 'starting_frame', 'format', kwargs)
         call_docval_func(super(ImageSeries, self).__init__, kwargs)
+        if external_file is None and self.data is None:
+            raise ValueError('must supply either external_file or data to ' + self.name)
         self.bits_per_pixel = bits_per_pixel
         self.dimension = dimension
         self.external_file = external_file


Test patch:
diff --git a/tests/unit/pynwb_tests/test_ophys.py b/tests/unit/pynwb_tests/test_ophys.py
index 6c25c1c3..14081672 100644
--- a/tests/unit/pynwb_tests/test_ophys.py
+++ b/tests/unit/pynwb_tests/test_ophys.py
@@ -49,7 +49,7 @@ class TwoPhotonSeriesConstructor(unittest.TestCase):
         self.assertEqual(ip.unit, 'unit')
         self.assertEqual(ip.reference_frame, 'reference_frame')
 
-        tPS = TwoPhotonSeries('test_tPS', 'a hypothetical source', data=list(), unit='unit', field_of_view=list(),
+        tPS = TwoPhotonSeries('test_tPS', 'a hypothetical source', unit='unit', field_of_view=list(),
                               imaging_plane=ip, pmt_gain=1.0, scan_line_rate=2.0, external_file=['external_file'],
                               starting_frame=[1, 2, 3], format='tiff', timestamps=list())
         self.assertEqual(tPS.name, 'test_tPS')
@@ -64,6 +64,16 @@ class TwoPhotonSeriesConstructor(unittest.TestCase):
         self.assertEqual(tPS.format, 'tiff')
         self.assertEqual(tPS.dimension, [np.nan])
 
+    def test_args(self):
+        oc = OpticalChannel('test_name', 'test_source', 'description', 500.)
+        device = Device(name='device_name', source='device_source')
+        ip = ImagingPlane('test_imaging_plane', 'test source', oc, 'description', device, 600.,
+                          'imaging_rate', 'indicator', 'location', (50, 100, 3), 4.0, 'unit', 'reference_frame')
+        with self.assertRaises(ValueError):  # no data or external file
+            TwoPhotonSeries('test_tPS', 'a hypothetical source', unit='unit', field_of_view=list(),
+                            imaging_plane=ip, pmt_gain=1.0, scan_line_rate=2.0,
+                            starting_frame=[1, 2, 3], format='tiff', timestamps=list())
+
 
 class MotionCorrectionConstructor(unittest.TestCase):
     def test_init(self):