Repository: mne-tools/mne-bids

Issue:
write_raw_bids() should raise if no task is provided
@dnacombo and I created an entire BIDS dataset and we unfortunately forgot to pass a `task` entity to the `BIDSPath` passed to `write_raw_bids()`. Now we'll have to rename everything … shouldn't have happened in the first place – `task` is required by BIDS!


Discussion:
I guess we could/should add a set of conditional clauses like:

```
if suffix in [eeg, meg, ieeg, events]:
    if BIDSPath.task is None:
        raise
```

Not all BIDS files that one could use with `BIDSPath` require the task entity (e.g., electrodes.tsv, dataset_description, ...)

EDIT: see https://bids-specification.readthedocs.io/en/stable/99-appendices/04-entity-table.html
I was just thinking about specifically adding a test to write_raw_bids, not to BIDSPath. This will avoid collateral damage, no?
doing it in BIDSPath is probably stricter and better in terms of other functions "inheriting" this check ... but yeah, it could be inflexible and do more harm. I am fine with adding the check to write_raw :+1: 

Resolving patch:
diff --git a/mne_bids/write.py b/mne_bids/write.py
--- a/mne_bids/write.py
+++ b/mne_bids/write.py
@@ -1080,10 +1080,10 @@ def write_raw_bids(raw, bids_path, events_data=None, event_id=None,
         to ``True``. See warning for the ``allow_preload`` parameter.
     bids_path : mne_bids.BIDSPath
         The file to write. The `mne_bids.BIDSPath` instance passed here
-        **must** have the ``.root`` attribute set. If the ``.datatype``
-        attribute is not set, it will be inferred from the recording data type
-        found in ``raw``. In case of multiple data types, the ``.datatype``
-        attribute must be set.
+        **must** have the ``subject``, ``task``, and ``root`` attributes set.
+        If the ``datatype`` attribute is not set, it will be inferred from the
+        recording data type found in ``raw``. In case of multiple data types,
+        the ``.datatype`` attribute must be set.
         Example::
 
             bids_path = BIDSPath(subject='01', session='01', task='testing',
@@ -1295,11 +1295,23 @@ def write_raw_bids(raw, bids_path, events_data=None, event_id=None,
     if symlink and anonymize is not None:
         raise ValueError('Cannot create symlinks when anonymizing data.')
 
-    # Check if the root is available
     if bids_path.root is None:
-        raise ValueError('The root of the "bids_path" must be set. '
-                         'Please use `bids_path.update(root="<root>")` '
-                         'to set the root of the BIDS folder to read.')
+        raise ValueError(
+            'The root of the "bids_path" must be set. Please call '
+            '"bids_path.root = <root>" to set the root of the BIDS dataset.'
+        )
+
+    if bids_path.subject is None:
+        raise ValueError(
+            'The subject of the "bids_path" must be set. Please call '
+            '"bids_path.subject = <subject>"'
+        )
+
+    if bids_path.task is None:
+        raise ValueError(
+            'The task of the "bids_path" must be set. Please call '
+            '"bids_path.task = <task>"'
+        )
 
     if events_data is not None and event_id is None:
         raise RuntimeError('You passed events_data, but no event_id '


Test patch:
diff --git a/mne_bids/commands/tests/test_cli.py b/mne_bids/commands/tests/test_cli.py
--- a/mne_bids/commands/tests/test_cli.py
+++ b/mne_bids/commands/tests/test_cli.py
@@ -261,7 +261,7 @@ def test_count_events(tmp_path):
     event_id = {'auditory/left': 1, 'auditory/right': 2, 'visual/left': 3,
                 'visual/right': 4, 'face': 5, 'button': 32}
 
-    bids_path = BIDSPath(subject='01', root=output_path)
+    bids_path = BIDSPath(subject='01', root=output_path, task='foo')
     write_raw_bids(raw, bids_path, events, event_id, overwrite=True,
                    verbose=False)
 
diff --git a/mne_bids/tests/test_write.py b/mne_bids/tests/test_write.py
--- a/mne_bids/tests/test_write.py
+++ b/mne_bids/tests/test_write.py
@@ -245,11 +245,24 @@ def test_write_correct_inputs():
                                            'BIDSPath object'):
         write_raw_bids(raw, bids_path_str)
 
-    bids_path = _bids_path.copy().update(root=None)
+    bids_path = _bids_path.copy()
+    assert bids_path.root is None
     with pytest.raises(
             ValueError,
             match='The root of the "bids_path" must be set'):
-        write_raw_bids(raw, bids_path)
+        write_raw_bids(raw=raw, bids_path=bids_path)
+
+    bids_path = _bids_path.copy().update(root='/foo', subject=None)
+    with pytest.raises(
+            ValueError,
+            match='The subject of the "bids_path" must be set'):
+        write_raw_bids(raw=raw, bids_path=bids_path)
+
+    bids_path = _bids_path.copy().update(root='/foo', task=None)
+    with pytest.raises(
+            ValueError,
+            match='The task of the "bids_path" must be set'):
+        write_raw_bids(raw=raw, bids_path=bids_path)
 
 
 def test_make_dataset_description(tmp_path, monkeypatch):
@@ -791,7 +804,7 @@ def test_fif_ias(tmp_path):
 
     raw.set_channel_types({raw.ch_names[0]: 'ias'})
 
-    data_path = BIDSPath(subject='sample', root=tmp_path)
+    data_path = BIDSPath(subject='sample', task='task', root=tmp_path)
 
     write_raw_bids(raw, data_path)
     raw = read_raw_bids(data_path)
@@ -807,7 +820,7 @@ def test_fif_exci(tmp_path):
     raw = _read_raw_fif(raw_fname)
 
     raw.set_channel_types({raw.ch_names[0]: 'exci'})
-    data_path = BIDSPath(subject='sample', root=tmp_path)
+    data_path = BIDSPath(subject='sample', task='task', root=tmp_path)
 
     write_raw_bids(raw, data_path)
     raw = read_raw_bids(data_path)
@@ -2597,7 +2610,7 @@ def test_anonymize(subject, dir_name, fname, reader, tmp_path):
         bids_path.update(task='noise', session=raw_date,
                          suffix='meg', datatype='meg')
     else:
-        bids_path.update(suffix='eeg', datatype='eeg')
+        bids_path.update(task='task', suffix='eeg', datatype='eeg')
     daysback_min, daysback_max = get_anonymization_daysback(raw)
     anonymize = dict(daysback=daysback_min + 1)
     bids_path = \
@@ -2841,7 +2854,8 @@ def test_write_fif_triux(tmp_path):
     tri_fname = op.join(triux_path, 'triux_bmlhus_erm_raw.fif')
     raw = mne.io.read_raw_fif(tri_fname)
     bids_path = BIDSPath(
-        subject="01", session="01", run="01", datatype="meg", root=tmp_path
+        subject="01", task="task", session="01", run="01", datatype="meg",
+        root=tmp_path
     )
     write_raw_bids(raw, bids_path=bids_path, overwrite=True)