Repository: mne-tools/mne-bids

Issue:
We erroneously assume BTi data if raw data file cannot be found
#### Describe the bug
MNE-BIDS is not being able to locate some files and then believes it might be BTi data (which comes in a directory).

For example, I get the following error:
![image](https://user-images.githubusercontent.com/38733898/120164079-77c02d00-c1fa-11eb-909e-6200e17ad44f.png)

I tried to debug a little and I got:
![image](https://user-images.githubusercontent.com/38733898/120163944-519a8d00-c1fa-11eb-9e4f-7019a45bf942.png)

It's because at line 569 we assign `config = None`



Discussion:
up
Thanks for reporting both, PR with a fix would be welcome
In my case, the error pointed to a config=None error, but this was actually due to a broken symbolic link. Apologies for the spam.
I see, no worries. @crsegerie could you please check whether that was the problem in your case as well?
I see this quite often, @sappelhoff, e.g. when there's a typo somewhere in the `BIDSPath`. We really need to fix this…
MWE:

```python
# %%
import tempfile
from pathlib import Path
import mne_bids


with tempfile.TemporaryDirectory() as temp_dir_name:
    temp_dir = Path(temp_dir_name)
    bp = mne_bids.BIDSPath(
        root=temp_dir, subject='foo', task='bar', datatype='eeg', suffix='eeg'
    )
    bp.fpath.parent.mkdir(parents=True, exist_ok=True)
    mne_bids.read_raw_bids(bids_path=bp)
```
```python
TypeError                                 Traceback (most recent call last)
Untitled-1 in <module>
     11     )
     12     bp.fpath.parent.mkdir(parents=True, exist_ok=True)
---> 13     mne_bids.read_raw_bids(bids_path=bp)
     14 

<decorator-gen-552> in read_raw_bids(bids_path, extra_params, verbose)

~/Development/mne-bids/mne_bids/read.py in read_raw_bids(bids_path, extra_params, verbose)
    679     if bids_fname.endswith('.fif') and 'allow_maxshield' not in extra_params:
    680         extra_params['allow_maxshield'] = True
--> 681     raw = _read_raw(bids_fpath, electrode=None, hsp=None, hpi=None,
    682                     config=config, **extra_params)
    683 

~/Development/mne-bids/mne_bids/read.py in _read_raw(raw_fpath, electrode, hsp, hpi, allow_maxshield, config, **kwargs)
     45     # BTi systems
     46     elif ext == '.pdf':
---> 47         raw = io.read_raw_bti(raw_fpath, config_fname=config,
     48                               head_shape_fname=hsp,
     49                               preload=False, **kwargs)

<decorator-gen-210> in read_raw_bti(pdf_fname, config_fname, head_shape_fname, rotation_x, translation, convert, rename_channels, sort_by_ch_name, ecg_ch, eog_ch, preload, verbose)

~/Development/mne-python/mne/io/bti/bti.py in read_raw_bti(pdf_fname, config_fname, head_shape_fname, rotation_x, translation, convert, rename_channels, sort_by_ch_name, ecg_ch, eog_ch, preload, verbose)
   1285     mne.io.Raw : Documentation of attribute and methods.
   1286     """
-> 1287     return RawBTi(pdf_fname, config_fname=config_fname,
   1288                   head_shape_fname=head_shape_fname,
   1289                   rotation_x=rotation_x, translation=translation,

<decorator-gen-209> in __init__(self, pdf_fname, config_fname, head_shape_fname, rotation_x, translation, convert, rename_channels, sort_by_ch_name, ecg_ch, eog_ch, preload, verbose)

~/Development/mne-python/mne/io/bti/bti.py in __init__(self, pdf_fname, config_fname, head_shape_fname, rotation_x, translation, convert, rename_channels, sort_by_ch_name, ecg_ch, eog_ch, preload, verbose)
    948                  ecg_ch='E31', eog_ch=('E63', 'E64'),
    949                  preload=False, verbose=None):  # noqa: D102
--> 950         info, bti_info = _get_bti_info(
    951             pdf_fname=pdf_fname, config_fname=config_fname,
    952             head_shape_fname=head_shape_fname, rotation_x=rotation_x,

~/Development/mne-python/mne/io/bti/bti.py in _get_bti_info(pdf_fname, config_fname, head_shape_fname, rotation_x, translation, convert, ecg_ch, eog_ch, rename_channels, sort_by_ch_name)
   1048 
   1049     if not isinstance(config_fname, BytesIO):
-> 1050         if not op.isabs(config_fname):
   1051             config_tries = [op.abspath(config_fname),
   1052                             op.abspath(op.join(op.dirname(pdf_fname),

~/miniforge3/envs/mne/lib/python3.9/posixpath.py in isabs(s)
     60 def isabs(s):
     61     """Test whether a path is absolute"""
---> 62     s = os.fspath(s)
     63     sep = _get_sep(s)
     64     return s.startswith(sep)

TypeError: expected str, bytes or os.PathLike object, not NoneType
```

If an `extension` is provided, the problem does not occur, and we get the expected `FileNotFoundError`:

```python
# %%
import tempfile
from pathlib import Path
import mne_bids


with tempfile.TemporaryDirectory() as temp_dir_name:
    temp_dir = Path(temp_dir_name)
    bp = mne_bids.BIDSPath(
        root=temp_dir, subject='foo', task='bar', datatype='eeg', suffix='eeg',
        extension='.fif'
    )
    bp.fpath.parent.mkdir(parents=True, exist_ok=True)
    mne_bids.read_raw_bids(bids_path=bp)
```
```python
FileNotFoundError                         Traceback (most recent call last)
Untitled-1 in <module>
     12     )
     13     bp.fpath.parent.mkdir(parents=True, exist_ok=True)
---> 14     mne_bids.read_raw_bids(bids_path=bp)
     15 

<decorator-gen-552> in read_raw_bids(bids_path, extra_params, verbose)

~/Development/mne-bids/mne_bids/read.py in read_raw_bids(bids_path, extra_params, verbose)
    679     if bids_fname.endswith('.fif') and 'allow_maxshield' not in extra_params:
    680         extra_params['allow_maxshield'] = True
--> 681     raw = _read_raw(bids_fpath, electrode=None, hsp=None, hpi=None,
    682                     config=config, **extra_params)
    683 

~/Development/mne-bids/mne_bids/read.py in _read_raw(raw_fpath, electrode, hsp, hpi, allow_maxshield, config, **kwargs)
     50 
     51     elif ext == '.fif':
---> 52         raw = reader[ext](raw_fpath, allow_maxshield, **kwargs)
     53 
     54     elif ext in ['.ds', '.vhdr', '.set', '.edf', '.bdf', '.EDF']:

~/Development/mne-python/mne/io/fiff/raw.py in read_raw_fif(fname, allow_maxshield, preload, on_split_missing, verbose)
    471     .. versionadded:: 0.9.0
    472     """
--> 473     return Raw(fname=fname, allow_maxshield=allow_maxshield,
    474                preload=preload, verbose=verbose,
    475                on_split_missing=on_split_missing)

<decorator-gen-226> in __init__(self, fname, allow_maxshield, preload, on_split_missing, verbose)

~/Development/mne-python/mne/io/fiff/raw.py in __init__(self, fname, allow_maxshield, preload, on_split_missing, verbose)
     79         while next_fname is not None:
     80             raw, next_fname, buffer_size_sec = \
---> 81                 self._read_raw_file(next_fname, allow_maxshield,
     82                                     preload, do_check_ext)
     83             do_check_ext = False

<decorator-gen-227> in _read_raw_file(self, fname, allow_maxshield, preload, do_check_ext, verbose)

~/Development/mne-python/mne/io/fiff/raw.py in _read_raw_file(self, fname, allow_maxshield, preload, do_check_ext, verbose)
    144                 check_fname(fname, 'raw', endings)
    145             # filename
--> 146             fname = _check_fname(fname, 'read', True, 'fname')
    147             ext = os.path.splitext(fname)[1].lower()
    148             whole_file = preload if '.gz' in ext else False

~/Development/mne-python/mne/utils/check.py in _check_fname(fname, overwrite, must_exist, name, need_dir)
    179                     f'{name} does not have read permissions: {fname}')
    180     elif must_exist:
--> 181         raise FileNotFoundError(f'{name} does not exist: {fname}')
    182 
    183     return fname

FileNotFoundError: fname does not exist: /var/folders/k_/b0x8c_5s6c96c497xzvwp6m80000gn/T/tmpe_dxn_t9/sub-foo/eeg/sub-foo_task-bar_eeg.fif
```

Resolving patch:
diff --git a/mne_bids/commands/mne_bids_raw_to_bids.py b/mne_bids/commands/mne_bids_raw_to_bids.py
--- a/mne_bids/commands/mne_bids_raw_to_bids.py
+++ b/mne_bids/commands/mne_bids_raw_to_bids.py
@@ -75,7 +75,7 @@ def run():
         allow_maxshield = True
 
     raw = _read_raw(opt.raw_fname, hpi=opt.hpi, electrode=opt.electrode,
-                    hsp=opt.hsp, config=opt.config,
+                    hsp=opt.hsp, config_path=opt.config,
                     allow_maxshield=allow_maxshield)
     if opt.line_freq is not None:
         line_freq = None if opt.line_freq == "None" else opt.line_freq
diff --git a/mne_bids/path.py b/mne_bids/path.py
--- a/mne_bids/path.py
+++ b/mne_bids/path.py
@@ -599,14 +599,6 @@ def fpath(self):
 
                 # found no matching paths
                 if not matching_paths:
-                    msg = (f'Could not locate a data file of a supported '
-                           f'format. This is likely a problem with your '
-                           f'BIDS dataset. Please run the BIDS validator '
-                           f'on your data. (root={self.root}, '
-                           f'basename={self.basename}). '
-                           f'{matching_paths}')
-                    warn(msg)
-
                     bids_fpath = op.join(data_path, self.basename)
                 # if paths still cannot be resolved, then there is an error
                 elif len(matching_paths) > 1:
diff --git a/mne_bids/read.py b/mne_bids/read.py
--- a/mne_bids/read.py
+++ b/mne_bids/read.py
@@ -8,7 +8,6 @@
 # License: BSD-3-Clause
 import os.path as op
 from pathlib import Path
-import glob
 import json
 import re
 import warnings
@@ -32,33 +31,32 @@
                            _infer_datatype)
 
 
-def _read_raw(raw_fpath, electrode=None, hsp=None, hpi=None,
-              allow_maxshield=False, config=None, **kwargs):
+def _read_raw(raw_path, electrode=None, hsp=None, hpi=None,
+              allow_maxshield=False, config_path=None, **kwargs):
     """Read a raw file into MNE, making inferences based on extension."""
-    _, ext = _parse_ext(raw_fpath)
+    _, ext = _parse_ext(raw_path)
 
     # KIT systems
     if ext in ['.con', '.sqd']:
-        raw = io.read_raw_kit(raw_fpath, elp=electrode, hsp=hsp,
+        raw = io.read_raw_kit(raw_path, elp=electrode, hsp=hsp,
                               mrk=hpi, preload=False, **kwargs)
 
     # BTi systems
     elif ext == '.pdf':
-        raw = io.read_raw_bti(raw_fpath, config_fname=config,
-                              head_shape_fname=hsp,
-                              preload=False, **kwargs)
+        raw = io.read_raw_bti(
+            pdf_fname=str(raw_path),  # FIXME MNE should accept Path!
+            config_fname=str(config_path),  # FIXME MNE should accept Path!
+            head_shape_fname=hsp,
+            preload=False,
+            **kwargs
+        )
 
     elif ext == '.fif':
-        raw = reader[ext](raw_fpath, allow_maxshield, **kwargs)
+        raw = reader[ext](raw_path, allow_maxshield, **kwargs)
 
     elif ext in ['.ds', '.vhdr', '.set', '.edf', '.bdf', '.EDF']:
-        raw_fpath = Path(raw_fpath)
-        # handle EDF extension upper/lower casing
-        if ext == '.edf' and not raw_fpath.exists():
-            raw_fpath = raw_fpath.with_suffix('.EDF')
-        elif ext == '.EDF' and not raw_fpath.exists():
-            raw_fpath = raw_fpath.with_suffix('.edf')
-        raw = reader[ext](raw_fpath, **kwargs)
+        raw_path = Path(raw_path)
+        raw = reader[ext](raw_path, **kwargs)
 
     # MEF and NWB are allowed, but not yet implemented
     elif ext in ['.mef', '.nwb']:
@@ -652,23 +650,36 @@ def read_raw_bids(bids_path, extra_params=None, verbose=None):
     if suffix is None:
         bids_path.update(suffix=datatype)
 
-    data_dir = bids_path.directory
-    bids_fname = bids_path.fpath.name
-
-    if op.splitext(bids_fname)[1] == '.pdf':
-        bids_raw_folder = op.join(data_dir, f'{bids_path.basename}')
-        bids_fpath = glob.glob(op.join(bids_raw_folder, 'c,rf*'))[0]
-        config = op.join(bids_raw_folder, 'config')
+    if bids_path.fpath.suffix == '.pdf':
+        bids_raw_folder = bids_path.directory / f'{bids_path.basename}'
+        raw_path = list(bids_raw_folder.glob('c,rf*'))[0]
+        config_path = bids_raw_folder / 'config'
     else:
-        bids_fpath = op.join(data_dir, bids_fname)
+        raw_path = bids_path.fpath
         # Resolve for FIFF files
-        if (bids_fpath.endswith('.fif') and bids_path.split is None and
-                op.islink(bids_fpath)):
-            target_path = op.realpath(bids_fpath)
+        if (
+            raw_path.suffix == '.fif' and
+            bids_path.split is None and
+            raw_path.is_symlink()
+        ):
+            target_path = raw_path.resolve()
             logger.info(f'Resolving symbolic link: '
-                        f'{bids_fpath} -> {target_path}')
-            bids_fpath = target_path
-        config = None
+                        f'{raw_path} -> {target_path}')
+            raw_path = target_path
+        config_path = None
+
+    # Special-handle EDF filenames: we accept upper- and lower-case extensions
+    if raw_path.suffix.lower() == '.edf':
+        for extension in ('.edf', '.EDF'):
+            candidate_path = raw_path.with_suffix(extension)
+            if candidate_path.exists():
+                raw_path = candidate_path
+                break
+
+    if not raw_path.exists():
+        raise FileNotFoundError(f'File does not exist: {raw_path}')
+    if config_path is not None and not config_path.exists():
+        raise FileNotFoundError(f'config directory not found: {config_path}')
 
     if extra_params is None:
         extra_params = dict()
@@ -676,10 +687,10 @@ def read_raw_bids(bids_path, extra_params=None, verbose=None):
         del extra_params['exclude']
         logger.info('"exclude" parameter is not supported by read_raw_bids')
 
-    if bids_fname.endswith('.fif') and 'allow_maxshield' not in extra_params:
+    if raw_path.suffix == '.fif' and 'allow_maxshield' not in extra_params:
         extra_params['allow_maxshield'] = True
-    raw = _read_raw(bids_fpath, electrode=None, hsp=None, hpi=None,
-                    config=config, **extra_params)
+    raw = _read_raw(raw_path, electrode=None, hsp=None, hpi=None,
+                    config_path=config_path, **extra_params)
 
     # Try to find an associated events.tsv to get information about the
     # events in the recorded data
@@ -739,14 +750,16 @@ def read_raw_bids(bids_path, extra_params=None, verbose=None):
         raw = _handle_scans_reading(scans_fname, raw, bids_path)
 
     # read in associated subject info from participants.tsv
-    participants_tsv_fpath = op.join(bids_root, 'participants.tsv')
+    participants_tsv_path = bids_root / 'participants.tsv'
     subject = f"sub-{bids_path.subject}"
-    if op.exists(participants_tsv_fpath):
-        raw = _handle_participants_reading(participants_tsv_fpath, raw,
-                                           subject)
+    if op.exists(participants_tsv_path):
+        raw = _handle_participants_reading(
+            participants_fname=participants_tsv_path,
+            raw=raw,
+            subject=subject
+        )
     else:
-        warn("Participants file not found for {}... Not reading "
-             "in any particpants.tsv data.".format(bids_fname))
+        warn(f"participants.tsv file not found for {raw_path}")
 
     assert raw.annotations.orig_time == raw.info['meas_date']
     return raw


Test patch:
diff --git a/mne_bids/tests/test_path.py b/mne_bids/tests/test_path.py
--- a/mne_bids/tests/test_path.py
+++ b/mne_bids/tests/test_path.py
@@ -410,16 +410,6 @@ def test_bids_path_inference(return_bids_test_dir):
     with pytest.raises(RuntimeError, match='Found more than one'):
         bids_path.fpath
 
-    # can't locate a file, but the basename should work
-    bids_path = BIDSPath(
-        subject=subject_id, session=session_id, acquisition=acq,
-        task=task, run='10', root=bids_root)
-    with pytest.warns(RuntimeWarning, match='Could not locate'):
-        fpath = bids_path.fpath
-        assert str(fpath) == op.join(bids_root, f'sub-{subject_id}',
-                                     f'ses-{session_id}',
-                                     bids_path.basename)
-
     # shouldn't error out when there is no uncertainty
     channels_fname = BIDSPath(subject=subject_id, session=session_id,
                               run=run, acquisition=acq, task=task,
diff --git a/mne_bids/tests/test_read.py b/mne_bids/tests/test_read.py
--- a/mne_bids/tests/test_read.py
+++ b/mne_bids/tests/test_read.py
@@ -154,7 +154,7 @@ def test_read_participants_data(tmp_path):
     raw = _read_raw_fif(raw_fname, verbose=False)
     write_raw_bids(raw, bids_path, overwrite=True, verbose=False)
     os.remove(participants_tsv_fpath)
-    with pytest.warns(RuntimeWarning, match='Participants file not found'):
+    with pytest.warns(RuntimeWarning, match='participants.tsv file not found'):
         raw = read_raw_bids(bids_path=bids_path)
         assert raw.info['subject_info'] is None
 
@@ -1069,3 +1069,20 @@ def test_read_edf_extension_casing(tmp_path):
 
     # obtain the sensor positions and assert ch_coords are same
     read_raw_bids(bids_path, verbose=True)
+
+
+def test_file_not_found(tmp_path):
+    """Check behavior if the requested file cannot be found."""
+    # First a path with a filename extension.
+    bp = BIDSPath(
+        root=tmp_path, subject='foo', task='bar', datatype='eeg', suffix='eeg',
+        extension='.fif'
+    )
+    bp.fpath.parent.mkdir(parents=True)
+    with pytest.raises(FileNotFoundError, match='File does not exist'):
+        read_raw_bids(bids_path=bp)
+
+    # Now without an extension
+    bp.extension = None
+    with pytest.raises(FileNotFoundError, match='File does not exist'):
+        read_raw_bids(bids_path=bp)