Repository: mne-tools/mne-bids

Issue:
Add `anonymize` parameter to copyfile_edf function
Currently, all EEG files are converted to BrainVision format if `anonymize` is set in `write_raw_bids`, because we have anonymization utilities only fir FIF (for MEG) and BrainVision (EEG and iEEG).

We can add anonymization functionality for the EDF data format quite easily, and then we won't have to "force-convert" the data to BrainVision anymore.

The function `anonymize_edf` in the pretty popular `pyedflib`, could be used to extend our own `copyfile_edf` with an `anonymize` parameter.

- https://github.com/holgern/pyedflib
- https://pyedflib.readthedocs.io/en/latest/ref/highlevel.html#pyedflib.highlevel.anonymize_edf

Usage from pyedflib would look like this:

```Python
from pyedflib import highlevel

# anonymize edf
highlevel.anonymize_edf('edf_file.edf', new_file='anonymized.edf'
                             to_remove=['patientname', 'birthdate'],
                             new_values=['anonymized', ''])

# check if the two files have the same content
highlevel.compare_edf('edf_file.edf', 'anonymized.edf')
```

alternatives:

- this command line tool? : https://www.physionet.org/content/edf-anonymize/1.0.0/


Discussion:
I would argue that there is nothing wrong in converting to Brainvision since for MEG we convert to fif. Standardization is the price you pay for anonymization ;-)
mhhh okay, fair point. What do you say about #473 though? I think I can achieve #473 in one small private function and 3 lines in `write_raw_bids`.

I certainly would sleep more easily if MNE-BIDS does not read my BV files and rewrites them using pybv (although I am a maintainer of pybv :rofl: )
> I certainly would sleep more easily if MNE-BIDS does not read my BV files
and rewrites them using pybv (although I am a maintainer of pybv 🤣 )

I see it as a good challenge to keep pybv the reference python
implementation to read/write BV files in Python :)

I agree with @agramfort :)
@sappelhoff Just FYI, I've written a [pure-python EDF writer](https://github.com/LBRF/DEMI_EEG_Pipeline/tree/main/utils) (based on a heavily-modified pyEDF) which would support this type of functionality, for my own analysis pipeline (passing EEG data from Python to R). Was wondering if it might be useful to a project like this!

Actually, I don't think you'd even need an external library at all to anonymize participant data: the patient ID data in the EDF+ format always starts and ends at the same location in the header (bytes 9 to 88), so all you'd need to do would be read that in and parse it (MNE already has the code for this in the io module), update the relevant parts of the string, and then overwrite those bytes in the EDF header using the new string. Here are the bits I wrote for creating and validating the participant ID string:

```python
def verify_ascii(x):
    err = "'{0}' contains unsupported characters (only printable ASCII allowed)"
    vals = [ord(c) for c in x]
    if min(vals) == 32:
        raise ValueError("Header fields cannot contain spaces")
    elif min(vals) < 32 or max(vals) > 126:
        raise ValueError(err.format(x))

def date_to_str(dt, fmt="%d-%b-%Y"):
    try:
        datestr = dt.strftime(fmt)
    except AttributeError:
        raise ValueError("All dates must be passed as Python datetime objects")
    return datestr.upper()

def create_patient_id(pid="X", sex="X", birthdate="X", name="X"):
    # First, check if inputs are valid
    verify_ascii(pid)
    if sex.upper()[0] not in ["M", "F", "X"]:
        raise ValueError("EDF+ only supports 'M', 'F', and 'X' as valid sex values")
    sex = sex.upper()[0]
    if birthdate != "X":
        birthdate = date_to_str(birthdate)
    verify_ascii(name)
    # Once validated, create and return string
    outstr = " ".join([pid, sex, birthdate, name])
    if len(outstr) > 80:
        raise ValueError("Combined ID string cannot exceed 80 characters")
    return outstr
```
> the patient ID data in the EDF+ format always starts and ends at the same location in the header (bytes 9 to 88), so all you'd need to do would be read that in and parse it (MNE already has the code for this in the io module), update the relevant parts of the string, and then overwrite those bytes in the EDF header using the new string. 

Interesting, is that true for both edf, and edf+? 

I still think it'd be cool to allow for edf anonymizing without forcing a conversion to brain vision.

If it can be accomplished with a small and robust function, that'd be cool for mne-bids imho
> Interesting, is that true for both edf, and edf+?

[According to the official specs](https://www.edfplus.info/specs/edf.html) the header region for patient info is identical for both EDF and EDF+ (bytes 9 to 88), the only difference is that  EDF+ explicitly defines the format that the participant info data must take ({id}-{sex}-{birthdate}-{name}) whereas regular EDF seems to let users define it however they want.

I think the easiest solution is to try and parse all patient data fields as EDF+, and if there's a parsing error of any kind, just write out "X-X-X-X" as the string since that's the correct way of specifying "no patient info" according to the EDF+ spec.

Based on what I've done already I can probably put together a PR for this pretty quickly!
What do @agramfort @jasmainak @hoechenberger say about it?


Sounds good to me!
sounds good to me too

>

+1 for me but @sappelhoff don't give up on `pybv` ;-)

Resolving patch:
diff --git a/mne_bids/copyfiles.py b/mne_bids/copyfiles.py
--- a/mne_bids/copyfiles.py
+++ b/mne_bids/copyfiles.py
@@ -19,8 +19,10 @@
 import re
 
 import shutil as sh
+from datetime import datetime
 
-from mne.io import read_raw_brainvision, anonymize_info
+from mne.io import (read_raw_brainvision, read_raw_edf, read_raw_bdf,
+                    anonymize_info)
 from scipy.io import loadmat, savemat
 
 from mne_bids.path import BIDSPath, _parse_ext, _mkdir_p
@@ -137,11 +139,19 @@ def copyfile_ctf(src, dest):
 
     Parameters
     ----------
-    src : str
+    src : str | pathlib.Path
         Path to the source raw .ds folder.
-    dest : str
+    dest : str | pathlib.Path
         Path to the destination of the new bids folder.
 
+    See Also
+    --------
+    copyfile_brainvision
+    copyfile_bti
+    copyfile_edf
+    copyfile_eeglab
+    copyfile_kit
+
     """
     _copytree(src, dest)
     # list of file types to rename
@@ -162,9 +172,9 @@ def copyfile_kit(src, dest, subject_id, session_id,
 
     Parameters
     ----------
-    src : str
+    src : str | pathlib.Path
         Path to the source raw .con or .sqd folder.
-    dest : str
+    dest : str | pathlib.Path
         Path to the destination of the new bids folder.
     subject_id : str | None
         The subject ID. Corresponds to "sub".
@@ -177,6 +187,14 @@ def copyfile_kit(src, dest, subject_id, session_id,
     _init_kwargs : dict
         Extract information of marker and headpoints
 
+    See Also
+    --------
+    copyfile_brainvision
+    copyfile_bti
+    copyfile_ctf
+    copyfile_edf
+    copyfile_eeglab
+
     """
     # create parent directories in case it does not exist yet
     _mkdir_p(op.dirname(dest))
@@ -253,9 +271,9 @@ def copyfile_brainvision(vhdr_src, vhdr_dest, anonymize=None, verbose=False):
 
     Parameters
     ----------
-    vhdr_src : str
-        The src path of the .vhdr file to be copied.
-    vhdr_dest : str
+    vhdr_src : str | pathlib.Path
+        The source path of the .vhdr file to be copied.
+    vhdr_dest : str | pathlib.Path
         The destination path of the .vhdr file.
     anonymize : dict | None
         If None (default), no anonymization is performed.
@@ -281,14 +299,19 @@ def copyfile_brainvision(vhdr_src, vhdr_dest, anonymize=None, verbose=False):
     See Also
     --------
     mne.io.anonymize_info
+    copyfile_bti
+    copyfile_ctf
+    copyfile_edf
+    copyfile_eeglab
+    copyfile_kit
 
     """
     # Get extenstion of the brainvision file
     fname_src, ext_src = _parse_ext(vhdr_src)
     fname_dest, ext_dest = _parse_ext(vhdr_dest)
     if ext_src != ext_dest:
-        raise ValueError(f'Need to move data with same extension'
-                         f' but got "{ext_src}", "{ext_dest}"')
+        raise ValueError(f'Need to move data with same extension, '
+                         f' but got "{ext_src}" and "{ext_dest}"')
 
     eeg_file_path, vmrk_file_path = _get_brainvision_paths(vhdr_src)
 
@@ -339,6 +362,140 @@ def copyfile_brainvision(vhdr_src, vhdr_dest, anonymize=None, verbose=False):
             print('Anonymized all dates in VHDR and VMRK.')
 
 
+def copyfile_edf(src, dest, anonymize=None):
+    """Copy an EDF, EDF+, or BDF file to a new location, optionally anonymize.
+
+    .. warning:: EDF/EDF+/BDF files contain two fields for recording dates:
+                 A generic "startdate" field that supports only 2-digit years,
+                 and a "Startdate" field as part of the "local recording
+                 identification", which supports 4-digit years.
+                 If you want to anonymize your file, MNE-BIDS will set the
+                 "startdate" field to 85 (i.e., 1985), the earliest possible
+                 date for that field. However, the "Startdate" field in the
+                 file's "local recording identification" and the date in the
+                 session's corresponding ``scans.tsv`` will be set correctly
+                 according to the argument provided to the ``anonymize``
+                 parameter. Note that it is possible that not all EDF/EDF+/BDF
+                 reading software parses the accurate recording date, and
+                 that for some reading software, the wrong year (1985) may
+                 be parsed.
+
+    Parameters
+    ----------
+    src : str | pathlib.Path
+        The source path of the .edf or .bdf file to be copied.
+    dest : str | pathlib.Path
+        The destination path of the .edf or .bdf file.
+    anonymize : dict | None
+        If None (default), no anonymization is performed.
+        If dict, data will be anonymized depending on the keys provided with
+        the dict: `daysback` is a required key, `keep_his` is an optional key.
+
+        `daysback` : int
+            Number of days by which to move back the recording date in time.
+            In studies with multiple subjects the relative recording date
+            differences between subjects can be kept by using the same number
+            of `daysback` for all subject anonymizations. `daysback` should be
+            great enough to shift the date prior to 1925 to conform with BIDS
+            anonymization rules. Due to limitations of the EDF/BDF format, the
+            year of the anonymized date will always be set to 1985 in the
+            'startdate' field of the file. The correctly-shifted year will be
+            written to the 'local recording identification' region of the
+            file header, which may not be parsed by all EDF/EDF+/BDF reader
+            softwares.
+
+        `keep_his` : bool
+            By default (False), all subject information next to the recording
+            date will be overwritten as well. If True, keep subject information
+            apart from the recording date. Participant names and birthdates
+            will always be anonymized if present, regardless of this setting.
+
+    See Also
+    --------
+    mne.io.anonymize_info
+    copyfile_brainvision
+    copyfile_bti
+    copyfile_ctf
+    copyfile_eeglab
+    copyfile_kit
+
+    """
+    # Ensure source & destination extensions are the same
+    fname_src, ext_src = _parse_ext(src)
+    fname_dest, ext_dest = _parse_ext(dest)
+    if ext_src != ext_dest:
+        raise ValueError(f'Need to move data with same extension, '
+                         f' but got "{ext_src}" and "{ext_dest}"')
+
+    # Copy data prior to any anonymization
+    sh.copyfile(src, dest)
+
+    # Anonymize EDF/BDF data, if requested
+    if anonymize is not None:
+        if ext_src == '.bdf':
+            raw = read_raw_bdf(dest, preload=False, verbose=0)
+        elif ext_src == '.edf':
+            raw = read_raw_edf(dest, preload=False, verbose=0)
+        else:
+            raise ValueError('Unsupported file type ({0})'.format(ext_src))
+
+        # Get subject info, recording info, and recording date
+        with open(dest, 'rb') as f:
+            f.seek(8)  # id_info field starts 8 bytes in
+            id_info = f.read(80).decode('ascii').rstrip()
+            rec_info = f.read(80).decode('ascii').rstrip()
+
+        # Parse metadata from file
+        if len(id_info) == 0 or len(id_info.split(' ')) != 4:
+            id_info = "X X X X"
+        if len(rec_info) == 0 or len(rec_info.split(' ')) != 5:
+            rec_info = "Startdate X X X X"
+        pid, sex, birthdate, name = id_info.split(' ')
+        startdate, admin_code, tech, equip = rec_info.split(' ')[1:5]
+
+        # Try to anonymize recording date using 4-digit year from
+        # "Startdate" field from "local recording identification"
+        # section, because the generic "startdate" field only
+        # supports 2-digit years.
+        #
+        # XXX: recording dates are specified twice in EDF/EDF+/BDF,
+        # MNE-Python currently parses the 2-digit year field. If that is
+        # changed to defaulting to the 4-digit field, this try-except
+        # clause can be removed. See:
+        # https://github.com/mne-tools/mne-python/issues/8544
+        try:
+            true_year = datetime.strptime(startdate, '%d-%b-%Y').year
+            newdate = raw.info['meas_date'].replace(year=true_year)
+            raw.info['meas_date'] = newdate
+        except ValueError as e:
+            # We could not parse the "Startdate" field from "local recording
+            # identification" section (e.g., because it was "X").
+            # So fall back to using the standard "startdate" field that only
+            # supports 2-digit years.
+            # This is what MNE-Python currently parses and puts into
+            # raw.info["meas_date"]
+            if "does not match format '%d-%b-%Y'" not in str(e):
+                raise e
+        daysback, keep_his = _check_anonymize(anonymize, raw, '.edf')
+        info = anonymize_info(raw.info, daysback=daysback, keep_his=keep_his)
+        startdate = datetime.strftime(info['meas_date'], '%d-%b-%Y').upper()
+        meas_date = datetime.strftime(info['meas_date'], '%d.%m.85')
+
+        # Anonymize ID info and write to file
+        if keep_his:
+            # Always remove participant birthdate and name to be safe
+            id_info = [pid, sex, "X", "X"]
+            rec_info = ["Startdate", startdate, admin_code, tech, equip]
+        else:
+            id_info = ["0", "X", "X", "X"]
+            rec_info = ["Startdate", startdate, "X", "mne-bids_anonymize", "X"]
+        with open(dest, 'r+b') as f:
+            f.seek(8)  # id_info field starts 8 bytes in
+            f.write(bytes(" ".join(id_info).ljust(80), 'ascii'))
+            f.write(bytes(" ".join(rec_info).ljust(80), 'ascii'))
+            f.write(bytes(meas_date, 'ascii'))
+
+
 def copyfile_eeglab(src, dest):
     """Copy a EEGLAB files to a new location and adjust pointer to '.fdt' file.
 
@@ -349,11 +506,19 @@ def copyfile_eeglab(src, dest):
 
     Parameters
     ----------
-    src : str
+    src : str | pathlib.Path
         Path to the source raw .set file.
-    dest : str
+    dest : str | pathlib.Path
         Path to the destination of the new .set file.
 
+    See Also
+    --------
+    copyfile_brainvision
+    copyfile_bti
+    copyfile_ctf
+    copyfile_edf
+    copyfile_kit
+
     """
     # Get extenstion of the EEGLAB file
     fname_src, ext_src = _parse_ext(src)
@@ -400,9 +565,17 @@ def copyfile_bti(raw, dest):
     ----------
     raw : instance of Raw
         An MNE-Python raw object of BTi data.
-    dest : str
+    dest : str | pathlib.Path
         Destination to copy the BTi data to.
 
+    See Also
+    --------
+    copyfile_brainvision
+    copyfile_ctf
+    copyfile_edf
+    copyfile_eeglab
+    copyfile_kit
+
     """
     pdf_fname = 'c,rfDC'
     if raw.info['highpass'] is not None:
diff --git a/mne_bids/write.py b/mne_bids/write.py
--- a/mne_bids/write.py
+++ b/mne_bids/write.py
@@ -42,7 +42,8 @@
 from mne_bids import read_raw_bids
 from mne_bids.path import BIDSPath, _parse_ext, _mkdir_p, _path_to_str
 from mne_bids.copyfiles import (copyfile_brainvision, copyfile_eeglab,
-                                copyfile_ctf, copyfile_bti, copyfile_kit)
+                                copyfile_ctf, copyfile_bti, copyfile_kit,
+                                copyfile_edf)
 from mne_bids.tsv_handler import (_from_tsv, _drop, _contains_row,
                                   _combine_rows)
 from mne_bids.read import (_get_bads_from_tsv_data, _find_matching_sidecar,
@@ -1112,12 +1113,13 @@ def write_raw_bids(raw, bids_path, events_data=None,
                 warn('Converting to FIF for anonymization')
             convert = True
             bids_path.update(extension='.fif')
-        elif bids_path.datatype in ['eeg', 'ieeg'] and ext != '.vhdr':
-            if verbose:
-                warn('Converting data files to BrainVision format '
-                     'for anonymization')
-            convert = True
-            bids_path.update(extension='.vhdr')
+        elif bids_path.datatype in ['eeg', 'ieeg']:
+            if ext not in ['.vhdr', '.edf', '.bdf']:
+                if verbose:
+                    warn('Converting data files to BrainVision format '
+                         'for anonymization')
+                convert = True
+                bids_path.update(extension='.vhdr')
 
     # Read in Raw object and extract metadata from Raw object if needed
     orient = ORIENTATION.get(ext, 'n/a')
@@ -1210,6 +1212,16 @@ def write_raw_bids(raw, bids_path, events_data=None,
     # BrainVision is multifile, copy over all of them and fix pointers
     elif ext == '.vhdr':
         copyfile_brainvision(raw_fname, bids_path, anonymize=anonymize)
+    elif ext in ['.edf', '.bdf']:
+        if anonymize is not None:
+            warn("EDF/EDF+/BDF files contain two fields for recording dates."
+                 "Due to file format limitations, one of these fields only "
+                 "supports 2-digit years. The date for that field will be "
+                 "set to 85 (i.e., 1985), the earliest possible date. "
+                 "EDF/EDF+/BDF reading software should parse the second "
+                 "field for recording dates, which contains the accurately "
+                 "anonymized date as calculated with `daysback`.")
+        copyfile_edf(raw_fname, bids_path, anonymize=anonymize)
     # EEGLAB .set might be accompanied by a .fdt - find out and copy it too
     elif ext == '.set':
         copyfile_eeglab(raw_fname, bids_path)


Test patch:
diff --git a/mne_bids/tests/test_copyfiles.py b/mne_bids/tests/test_copyfiles.py
--- a/mne_bids/tests/test_copyfiles.py
+++ b/mne_bids/tests/test_copyfiles.py
@@ -27,6 +27,7 @@
 from mne_bids.copyfiles import (_get_brainvision_encoding,
                                 _get_brainvision_paths,
                                 copyfile_brainvision,
+                                copyfile_edf,
                                 copyfile_eeglab,
                                 copyfile_kit)
 
@@ -112,6 +113,74 @@ def test_copyfile_brainvision():
     assert new_date == (prev_date - datetime.timedelta(days=32459))
 
 
+def test_copyfile_edf():
+    """Test the anonymization of EDF/BDF files"""
+    bids_root = _TempDir()
+    data_path = op.join(base_path, 'edf', 'tests', 'data')
+
+    # Test regular copying
+    for ext in ['.edf', '.bdf']:
+        raw_fname = op.join(data_path, 'test' + ext)
+        new_name = op.join(bids_root, 'test_copy' + ext)
+        copyfile_edf(raw_fname, new_name)
+
+    # IO error testing
+    with pytest.raises(ValueError, match='Need to move data with same'):
+        raw_fname = op.join(data_path, 'test.edf')
+        new_name = op.join(bids_root, 'test_copy.bdf')
+        copyfile_edf(raw_fname, new_name)
+
+    # Add some subject info to an EDF to test anonymization
+    testfile = op.join(bids_root, 'test_copy.edf')
+    raw_date = mne.io.read_raw_edf(testfile).info['meas_date']
+    date = datetime.datetime.strftime(raw_date, "%d-%b-%Y").upper()
+    test_id_info = '023 F 02-AUG-1951 Jane'
+    test_rec_info = 'Startdate {0} ID-123 John BioSemi_ActiveTwo'.format(date)
+    with open(testfile, 'r+b') as f:
+        f.seek(8)
+        f.write(bytes(test_id_info.ljust(80), 'ascii'))
+        f.write(bytes(test_rec_info.ljust(80), 'ascii'))
+
+    # Test date anonymization
+    def _edf_get_real_date(fpath):
+        with open(fpath, 'rb') as f:
+            f.seek(88)
+            rec_info = f.read(80).decode('ascii').rstrip()
+        startdate = rec_info.split(' ')[1]
+        return datetime.datetime.strptime(startdate, "%d-%b-%Y")
+
+    bids_root2 = _TempDir()
+    infile = op.join(bids_root, 'test_copy.edf')
+    outfile = op.join(bids_root2, 'test_copy_anon.edf')
+    anonymize = {'daysback': 33459, 'keep_his': False}
+    copyfile_edf(infile, outfile, anonymize)
+    prev_date = _edf_get_real_date(infile)
+    new_date = _edf_get_real_date(outfile)
+    assert new_date == (prev_date - datetime.timedelta(days=33459))
+
+    # Test full ID info anonymization
+    anon_startdate = datetime.datetime.strftime(new_date, "%d-%b-%Y").upper()
+    with open(outfile, 'rb') as f:
+        f.seek(8)
+        id_info = f.read(80).decode('ascii').rstrip()
+        rec_info = f.read(80).decode('ascii').rstrip()
+    rec_info_tmp = "Startdate {0} X mne-bids_anonymize X"
+    assert id_info == "0 X X X"
+    assert rec_info == rec_info_tmp.format(anon_startdate)
+
+    # Test partial ID info anonymization
+    outfile2 = op.join(bids_root2, 'test_copy_anon_partial.edf')
+    anonymize = {'daysback': 33459, 'keep_his': True}
+    copyfile_edf(infile, outfile2, anonymize)
+    with open(outfile2, 'rb') as f:
+        f.seek(8)
+        id_info = f.read(80).decode('ascii').rstrip()
+        rec_info = f.read(80).decode('ascii').rstrip()
+    rec = 'Startdate {0} ID-123 John BioSemi_ActiveTwo'.format(anon_startdate)
+    assert id_info == "023 F X X"
+    assert rec_info == rec
+
+
 def test_copyfile_eeglab():
     """Test the copying of EEGlab set and fdt files."""
     bids_root = _TempDir()
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
@@ -1070,20 +1070,15 @@ def test_eegieeg(dir_name, fname, reader, _bids_validate):
     assert len(list(data.values())[0]) == 2
 
     # check that scans list is properly converted to brainvision
-    if check_version('pybv', '0.4'):
+    if check_version('pybv', '0.4') or dir_name == 'EDF':
         daysback_min, daysback_max = _get_anonymization_daysback(raw)
         daysback = (daysback_min + daysback_max) // 2
 
-        # XXX: Need to remove "Status" channel until pybv supports
-        # channels that are non-Volt
-        if dir_name == 'EDF':
-            raw.drop_channels("Status")
-
         kwargs = dict(raw=raw, bids_path=bids_path,
                       anonymize=dict(daysback=daysback), overwrite=True)
         if dir_name == 'EDF':
-            with pytest.warns(RuntimeWarning,
-                              match='Encountered data in "int" format'):
+            match = r"^EDF\/EDF\+\/BDF files contain two fields .*"
+            with pytest.warns(RuntimeWarning, match=match):
                 write_raw_bids(**kwargs)
         elif dir_name == 'Persyst':
             with pytest.warns(RuntimeWarning,
@@ -1095,8 +1090,9 @@ def test_eegieeg(dir_name, fname, reader, _bids_validate):
                 write_raw_bids(**kwargs)
 
         data = _from_tsv(scans_tsv)
-        bids_fname = bids_path.copy().update(suffix='eeg',
-                                             extension='.vhdr')
+        bids_fname = bids_path.copy()
+        if dir_name != 'EDF':
+            bids_fname = bids_fname.update(suffix='eeg', extension='.vhdr')
         assert any([bids_fname.basename in fname
                     for fname in data['filename']])
 
@@ -1175,14 +1171,9 @@ def test_eegieeg(dir_name, fname, reader, _bids_validate):
     assert coordsystem_json['iEEGCoordinateSystem'] == 'Other'
 
     # test anonymize and convert
-    if check_version('pybv', '0.4'):
+    if check_version('pybv', '0.4') or dir_name == 'EDF':
         raw = reader(raw_fname)
 
-        # XXX: Need to remove "Status" channel until pybv supports
-        # channels that are non-Volt
-        if dir_name == 'EDF':
-            raw.drop_channels("Status")
-
         kwargs = dict(raw=raw, bids_path=bids_path, overwrite=True)
         if dir_name == 'NihonKohden':
             with pytest.warns(RuntimeWarning,
@@ -1190,9 +1181,9 @@ def test_eegieeg(dir_name, fname, reader, _bids_validate):
                 write_raw_bids(**kwargs)
                 output_path = _test_anonymize(raw, bids_path)
         elif dir_name == 'EDF':
-            write_raw_bids(**kwargs)  # Just copies.
-            with pytest.warns(RuntimeWarning,
-                              match='Encountered data in "int" format'):
+            match = r"^EDF\/EDF\+\/BDF files contain two fields .*"
+            with pytest.warns(RuntimeWarning, match=match):
+                write_raw_bids(**kwargs)  # Just copies.
                 output_path = _test_anonymize(raw, bids_path)
         else:
             with pytest.warns(RuntimeWarning,
@@ -1254,18 +1245,11 @@ def test_bdf(_bids_validate):
         write_raw_bids(raw, bids_path)
 
     # test anonymize and convert
-    if check_version('pybv', '0.4'):
-        raw = _read_raw_bdf(raw_fname)
-        # XXX: Need to remove "Status" channel until pybv supports
-        # channels that are non-Volt
-        with pytest.raises(RuntimeError, match='''in Volts: "{'Status'}"'''):
-            output_path = _test_anonymize(raw, bids_path)
-
-        raw.pick_types(eeg=True, exclude=[])
-        with pytest.warns(RuntimeWarning,
-                          match='Encountered data in "int" format'):
-            output_path = _test_anonymize(raw, bids_path)
-        _bids_validate(output_path)
+    raw = _read_raw_bdf(raw_fname)
+    match = r"^EDF\/EDF\+\/BDF files contain two fields .*"
+    with pytest.warns(RuntimeWarning, match=match):
+        output_path = _test_anonymize(raw, bids_path)
+    _bids_validate(output_path)
 
 
 @pytest.mark.filterwarnings(warning_str['meas_date_set_to_none'])