Skip to content

Commit

Permalink
Merge branch 'main' into fix-incompatible-bids-verisons
Browse files Browse the repository at this point in the history
  • Loading branch information
sappelhoff committed Jul 24, 2023
2 parents dbb9a5d + c3eb2cf commit acc77bb
Show file tree
Hide file tree
Showing 8 changed files with 262 additions and 4 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/unit_tests.yml
Expand Up @@ -139,7 +139,7 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: 16
node-version: 18

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
Expand Down
4 changes: 4 additions & 0 deletions CITATION.cff
Expand Up @@ -167,6 +167,10 @@ authors:
family-names: McDonald
affiliation: 'Behavior and NeuroData Core, Brown University, Providence, RI, USA'
orcid: 'https://orcid.org/0009-0004-5099-7109'
- given-names: Pierre
family-names: Guetschel
orcid: 'https://orcid.org/0000-0002-8933-7640'
affiliation: 'Donders Institute for Brain, Cognition and Behaviour, Radboud University, Nijmegen, Netherlands'
- given-names: Alexandre
family-names: Gramfort
affiliation: 'Université Paris-Saclay, Inria, CEA, Palaiseau, France'
Expand Down
16 changes: 16 additions & 0 deletions CONTRIBUTING.md
Expand Up @@ -177,6 +177,22 @@ The latter command will result in a faster build but produce no plots in the exa
More information on our documentation setup can be found in our
[mne-bids WIKI](https://github.com/mne-tools/mne-bids/wiki).

## Instructions for first-time contributors

When you are making your first contribution to `mne-bids`, we kindly request you to:

1. Create an account at [circleci](https://circleci.com/), because this will simplify running the automated test suite
of `mne-bids`.
Note: you can simply use your GitHub account to log in.
1. Add yourself to the [list of authors](https://github.com/mne-tools/mne-bids/blob/main/doc/authors.rst).
1. Add yourself to the [CITATION.cff](https://github.com/mne-tools/mne-bids/blob/main/CITATION.cff) file.
Note: please add yourself in the
["authors" section](https://github.com/mne-tools/mne-bids/blob/fff6e90984ea0aa1e2914bb55e4197f7ec2800bf/CITATION.cff#L7C3-L7C3)
of that file, towards the end of the list of authors, but **before** `Alexandre Gramfort` and `Mainak Jas`.
1. Update the [changelog](https://github.com/mne-tools/mne-bids/blob/main/doc/whats_new.rst) with your contribution.
Note: please follow the existing format and add your name as a list item under the heading saying:
`The following authors contributed for the first time. Thank you so much!`.

## Making a release

Usually only core developers make a release after consensus has been reached.
Expand Down
3 changes: 2 additions & 1 deletion doc/authors.rst
Expand Up @@ -42,4 +42,5 @@
.. _Moritz Gerster: http://moritz-gerster.com
.. _Laetitia Fesselier: https://github.com/laemtl
.. _Jonathan Vanhoecke: https://github.com/JonathanVHoecke
.. _Ford McDonald: https://github.com/fordmcdonald
.. _Ford McDonald: https://github.com/fordmcdonald
.. _Pierre Guetschel: https://github.com/PierreGtch
5 changes: 4 additions & 1 deletion doc/whats_new.rst
Expand Up @@ -26,13 +26,15 @@ The following authors contributed for the first time. Thank you so much! 🤩

* `Laetitia Fesselier`_
* `Jonathan Vanhoecke`_
* `Ford McDonald`_
* `Pierre Guetschel`_

The following authors had contributed before. Thank you for sticking around! 🤘

* `Richard Höchenberger`_
* `Eric Larson`_
* `Stefan Appelhoff`_
* `Ford McDonald`_
* `Adam Li`_

Detailed list of changes
~~~~~~~~~~~~~~~~~~~~~~~~
Expand All @@ -41,6 +43,7 @@ Detailed list of changes
^^^^^^^^^^^^^^^

- :class:`~mne_bids.BIDSPath` now supports the new ``"sessions"`` suffix, by `Jonathan Vanhoecke`_ and `Richard Höchenberger`_ (:gh:`1137`)
- The :func:`~mne_bids.BIDSPath.rm` method will safely delete all the files compatible with that path and update the ``scans.tsv`` and ``participants.tsv`` files accordingly, by `Pierre Guetschel`_ and `Adam Li`_ (:gh:`1149`)

🧐 API and behavior changes
^^^^^^^^^^^^^^^^^^^^^^^^^^^
Expand Down
167 changes: 167 additions & 0 deletions mne_bids/path.py
Expand Up @@ -37,6 +37,7 @@
_ensure_tuple,
warn,
)
from mne_bids.tsv_handler import _from_tsv, _drop, _to_tsv


def _find_empty_room_candidates(bids_path):
Expand Down Expand Up @@ -626,6 +627,172 @@ def mkdir(self, exist_ok=True):
self.directory.mkdir(parents=True, exist_ok=exist_ok)
return self

@verbose
def rm(self, *, safe_remove=True, verbose=None):
"""Safely delete a set of files from a BIDS dataset.
Deleting a scan that conforms to the bids-validator will
remove the respective row in ``*_scans.tsv``, the
corresponding sidecar files, and the data file itself.
Deleting all files of a subject will update the
``*_participants.tsv`` file.
Parameters
----------
safe_remove : bool
If ``False``, directly delete and update the files.
Otherwise, displays the list of operations planned
and asks for user confirmation before
executing them (default).
%(verbose)s
Returns
-------
self : BIDSPath
The BIDSPath object.
Examples
--------
Remove one specific run:
>>> bids_path = BIDSPath(subject='01', session='01', run="01", # doctest: +SKIP
... root='/bids_dataset').rm() # doctest: +SKIP
Please, confirm you want to execute the following operations:
Delete:
/bids_dataset/sub-01/ses-01/meg/sub-01_ses-01_run-01_channels.tsv
/bids_dataset/sub-01/ses-01/meg/sub-01_ses-01_run-01_events.json
/bids_dataset/sub-01/ses-01/meg/sub-01_ses-01_run-01_events.tsv
/bids_dataset/sub-01/ses-01/meg/sub-01_ses-01_run-01_meg.fif
/bids_dataset/sub-01/ses-01/meg/sub-01_ses-01_run-01_meg.json
Update:
/bids_dataset/sub-01/ses-01/sub-01_ses-01_scans.tsv
I confirm [y/N]>? y
Remove all the files of a specific subject:
>>> bids_path = BIDSPath(subject='01', root='/bids_dataset', # doctest: +SKIP
... check=False).rm() # doctest: +SKIP
Please, confirm you want to execute the following operations:
Delete:
/bids_dataset/sub-01/ses-01/meg/sub-01_ses-01_acq-calibration_meg.dat
/bids_dataset/sub-01/ses-01/meg/sub-01_ses-01_acq-crosstalk_meg.fif
/bids_dataset/sub-01/ses-01/meg/sub-01_ses-01_coordsystem.json
/bids_dataset/sub-01/ses-01/meg/sub-01_ses-01_run-02_channels.tsv
/bids_dataset/sub-01/ses-01/meg/sub-01_ses-01_run-02_events.json
/bids_dataset/sub-01/ses-01/meg/sub-01_ses-01_run-02_events.tsv
/bids_dataset/sub-01/ses-01/meg/sub-01_ses-01_run-02_meg.fif
/bids_dataset/sub-01/ses-01/meg/sub-01_ses-01_run-02_meg.json
/bids_dataset/sub-01/ses-01/sub-01_ses-01_scans.tsv
/bids_dataset/sub-01
Update:
/bids_dataset/participants.tsv
I confirm [y/N]>? y
"""
# only proceed if root is defined
if self.root is None:
raise RuntimeError("The root must not be None to remove files.")

# Planning:
paths_matched = self.match(ignore_json=False, check=self.check)
subjects = set()
paths_to_delete = list()
paths_to_update = {}
subjects_paths_to_delete = []
participants_tsv_fpath = None
for bids_path in paths_matched:
paths_to_delete.append(bids_path)
# if a datatype is present, then check
# if a scan is deleted or not
if bids_path.datatype is not None:
# read in the corresponding scans file
scans_fpath = (
bids_path.copy()
.update(datatype=None)
.find_matching_sidecar(
suffix="scans",
extension=".tsv",
on_error="raise",
)
)
paths_to_update.setdefault(scans_fpath, []).append(bids_path)
subjects.add(bids_path.subject)

files_to_delete = set(p.fpath for p in paths_to_delete)
for subject in subjects:
# check existence of files in the subject dir
subj_path = BIDSPath(root=self.root, subject=subject)
subj_files = [
fpath for fpath in subj_path.directory.rglob("*") if fpath.is_file()
]
if set(subj_files) <= files_to_delete:
subjects_paths_to_delete.append(subj_path)
participants_tsv_fpath = self.root / "participants.tsv"

# Informing:
pretty_delete_paths = "\n".join(
[
str(p)
for p in paths_to_delete
+ [p.directory for p in subjects_paths_to_delete]
]
)
pretty_update_paths = "\n".join(
[
str(p)
for p in list(paths_to_update.keys())
+ (
[participants_tsv_fpath]
if participants_tsv_fpath is not None
else []
)
]
)
summary = ""
if pretty_delete_paths:
summary += f"Delete:\n{pretty_delete_paths}\n"
if pretty_update_paths:
summary += f"Update:\n{pretty_update_paths}\n"

if safe_remove:
choice = input(
"Please, confirm you want to execute the following operations:\n"
f"{summary}\nI confirm [y/N]"
)
if choice.lower() != "y":
return
else:
logger.info(f"Executing the following operations:\n{summary}")

# Execution:
for bids_path in paths_to_delete:
bids_path.fpath.unlink()

for scans_fpath, bids_paths in paths_to_update.items():
if not scans_fpath.exists():
continue
# get the relative datatype of these bids files
bids_fnames = [op.join(p.datatype, p.fpath.name) for p in bids_paths]

scans_tsv = _from_tsv(scans_fpath)
scans_tsv = _drop(scans_tsv, bids_fnames, "filename")
_to_tsv(scans_tsv, scans_fpath)

subjects_to_delete = []
for subj_path in subjects_paths_to_delete:
if subj_path.directory.exists():
sh.rmtree(subj_path.directory)
subjects_to_delete.append(subj_path.subject)
if subjects_to_delete and participants_tsv_fpath.exists():
participants_tsv = _from_tsv(participants_tsv_fpath)
participants_tsv = _drop(
participants_tsv, subjects_to_delete, "participant_id"
)
_to_tsv(participants_tsv, participants_tsv_fpath)

return self

@property
def fpath(self):
"""Full filepath for this BIDS file.
Expand Down
68 changes: 67 additions & 1 deletion mne_bids/tests/test_path.py
Expand Up @@ -39,7 +39,6 @@

from test_read import _read_raw_fif, warning_str


subject_id = "01"
session_id = "01"
run = "01"
Expand Down Expand Up @@ -259,6 +258,73 @@ def test_make_folders(tmp_path):
os.chdir(curr_dir)


@testing.requires_testing_data
def test_rm(return_bids_test_dir, capsys, tmp_path_factory):
"""Test BIDSPath's rm method to remove files."""
# for some reason, mne's logger can't be captured by caplog....
bids_root = str(tmp_path_factory.mktemp("test_rm") / "mnebids_utils_test_bids_ds")
shutil.copytree(return_bids_test_dir, bids_root)

# without providing all the entities, ambiguous when trying
# to use fpath
bids_path = BIDSPath(
subject=subject_id,
session=session_id,
run="01",
acquisition=acq,
task=task,
root=bids_root,
)

# Delete one run:
deleted_paths = bids_path.match(ignore_json=False)
updated_paths = [
bids_path.copy()
.update(datatype=None)
.find_matching_sidecar(
suffix="scans",
extension=".tsv",
on_error="raise",
)
]
expected = ["Executing the following operations:", "Delete:", "Update:", ""]
expected += [str(p) for p in deleted_paths + updated_paths]
bids_path.rm(safe_remove=False, verbose="INFO")
captured = capsys.readouterr().out
assert set(captured.splitlines()) == set(expected)

# delete the last run of a subject:
bids_path = BIDSPath(
subject=subject_id,
session=session_id,
root=bids_root,
)
deleted_paths = bids_path.match(ignore_json=False)
deleted_paths += [
BIDSPath(
root=bids_path.root,
subject=bids_path.subject,
).directory
]
updated_paths = [
bids_path.copy()
.update(datatype=None)
.find_matching_sidecar(
suffix="scans",
extension=".tsv",
on_error="raise",
),
bids_path.root / "participants.tsv",
]
expected = ["Executing the following operations:", "Delete:", "Update:", ""]
expected += [str(p) for p in deleted_paths + updated_paths]
bids_path.rm(safe_remove=False, verbose="INFO")
captured2 = capsys.readouterr().out
assert set(captured2.splitlines()) == set(expected)
print("\n".join(captured))
print("\n".join(captured2))


def test_parse_ext():
"""Test the file extension extraction."""
f = "sub-05_task-matchingpennies.vhdr"
Expand Down
1 change: 1 addition & 0 deletions setup.cfg
Expand Up @@ -86,6 +86,7 @@ filterwarnings =
ignore:numpy.ufunc size changed.*:RuntimeWarning
ignore:tostring\(\) is deprecated.*:DeprecationWarning
ignore:MEG ref channel RMSP did not.*:RuntimeWarning
ignore:`product` is deprecated as of NumPy.*:DeprecationWarning
# Python 3.10+ and NumPy 1.22 (and maybe also newer NumPy versions?)
ignore:.*distutils\.sysconfig module is deprecated.*:DeprecationWarning
# numba with NumPy dev
Expand Down

0 comments on commit acc77bb

Please sign in to comment.