diff --git a/.circleci/config.yml b/.circleci/config.yml index 2c03a025a..1366889bf 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -48,7 +48,13 @@ jobs: name: Build the documentation no_output_timeout: 30m command: | - make build-doc + make build-doc 2>&1 | tee sphinx_log.txt + + - run: + name: Check sphinx log for warnings (which are treated as errors) + when: always + command: | + ! grep "^.*WARNING: .*$" sphinx_log.txt - persist_to_workspace: root: doc/_build diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 45b8364c6..501e073d9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,6 +5,7 @@ repos: - id: ruff name: ruff mne_bids/ files: ^mne_bids/ + args: ["--fix"] - id: ruff name: ruff examples/ # D103: missing docstring in public function diff --git a/doc/whats_new.rst b/doc/whats_new.rst index 9fa966250..978d35460 100644 --- a/doc/whats_new.rst +++ b/doc/whats_new.rst @@ -22,6 +22,7 @@ The following authors contributed for the first time. Thank you so much! 🤩 The following authors had contributed before. Thank you for sticking around! 🤘 * `Daniel McCloy`_ +* `Eric Larson`_ * `Stefan Appelhoff`_ Detailed list of changes @@ -45,12 +46,12 @@ Detailed list of changes 🪲 Bug fixes ^^^^^^^^^^^^ -- When anonymizing the date of a recording, MNE-BIDS will no longer error during `~mne_bids.write_raw_bids` if passing a `~mne.io.Raw` instance to ``empty_room``. By `Daniel McCloy`_ (:gh:`1270`) +- When anonymizing the date of a recording, MNE-BIDS will no longer error during `~mne_bids.write_raw_bids` if passing a `~mne.io.Raw` instance to ``empty_room``, by `Daniel McCloy`_ (:gh:`1270`) ⚕️ Code health ^^^^^^^^^^^^^^ -- nothing yet +- Keep MNE-BIDS up to date with recent changes on participant birthday date handling in MNE-Python, by `Eric Larson`_ (gh:1278:) :doc:`Find out what was new in previous releases ` diff --git a/doc/whats_new_previous_releases.rst b/doc/whats_new_previous_releases.rst index 4b5d95cda..5652d09b7 100644 --- a/doc/whats_new_previous_releases.rst +++ b/doc/whats_new_previous_releases.rst @@ -536,7 +536,7 @@ API and behavior changes - Reading BIDS data with ``"HeadCoilFrequency"`` and ``"PowerLineFrequency"`` data specified in JSON sidecars will only "warn" in case of mismatches between Raw and JSON data, by `Franziska von Albedyll`_ (:gh:`855`) -- Accessing :attr:`mne_bids.BIDSPath.fpath` emit a warning anymore if the path does not exist. This behavior was unreliable and yielded confusing error messages in certain use cases. Use `mne_bids.BIDSPath.fpath.exists()` to check whether the path exists in the file system, by `Richard Höchenberger`_ (:gh:`904`) +- Accessing :attr:`mne_bids.BIDSPath.fpath` emit a warning anymore if the path does not exist. This behavior was unreliable and yielded confusing error messages in certain use cases. Use ``mne_bids.BIDSPath.fpath.exists()`` to check whether the path exists in the file system, by `Richard Höchenberger`_ (:gh:`904`) - :func:`mne_bids.get_entity_vals` gained a new parameter, ``ignore_dirs``, to exclude directories from the search, by `Adam Li`_ and `Richard Höchenberger`_ (:gh:`899`, :gh:`908`) @@ -587,13 +587,13 @@ Notable changes - You can now write preloaded and potentially modified data with :func:`mne_bids.write_raw_bids` by passing ``allow_preload=True``. This is a first step towards supporting derivative files. -- `mne_bids.BIDSPath` now has property getters and setters for all BIDS +- :func:`mne_bids.BIDSPath` now has property getters and setters for all BIDS entities. What this means is that you can now do things like ``bids_path.subject = '01'`` instead of ``bids_path.update(subject='01')``. - We now support Deep Brain Stimulation (DBS) data. - The way we handle anatomical landmarks was greatly revamped to ensure we're always using the correct coordinate systems. A new function, - `mne_bids.get_anat_landmarks`, helps with extracting fiducial points from + :func:`mne_bids.get_anat_landmarks`, helps with extracting fiducial points from anatomical scans. - When creating a BIDS dataset from FIFF files on macOS and Linux, MNE-BIDS can now optionally generate symbolic links to the original files instead of @@ -859,7 +859,7 @@ been cooking for you! Notable changes ~~~~~~~~~~~~~~~ -- We introduce `mne_bids.BIDSPath`, a new class for all BIDS file and folder +- We introduce :func:`mne_bids.BIDSPath`, a new class for all BIDS file and folder operations. All functions in MNE-BIDS that previously accepted filenames and folder locations (e.g. ``bids_root``) have been updated to work with ``BIDSPath``. Others have been removed. @@ -956,7 +956,7 @@ Bug fixes API changes ^^^^^^^^^^^ -In the transition to using `mne_bids.BIDSPath`, the following functions have been updated: +In the transition to using :func:`mne_bids.BIDSPath`, the following functions have been updated: - :func:`mne_bids.write_anat` now accepts a :class:`mne_bids.BIDSPath` instead of entities as keyword arguments, by `Adam Li`_ (:gh:`575`) - In :func:`mne_bids.write_raw_bids`, :func:`mne_bids.read_raw_bids`, and :func:`mne_bids.get_head_mri_trans`, the ``bids_basename`` and ``bids_root`` keyword arguments have been removed. The functions now expect ``bids_path``, an instance of :class:`mne_bids.BIDSPath`, by `Adam Li`_ (:gh:`525`) @@ -1110,7 +1110,7 @@ Bug API ~~~ -- :func:`make_dataset_description` is now available from `mne_bids` main namespace, all copyfile functions are available from `mne_bids.copyfiles` namespace, by `Stefan Appelhoff`_ (:gh:`196`) +- :func:`make_dataset_description` is now available from `mne_bids` main namespace, all copyfile functions are available from :func:`mne_bids.copyfiles` namespace, by `Stefan Appelhoff`_ (:gh:`196`) - Add support for non maxfiltered .fif files, by `Maximilien Chaumon`_ (:gh:`171`) - Remove support for Neuroscan ``.cnt`` data because its support is no longer planned in BIDS, by `Stefan Appelhoff`_ (:gh:`142`) - Remove support for Python 2 because it is no longer supported in MNE-Python, by `Teon Brooks`_ (:gh:`141`) diff --git a/examples/bidspath.py b/examples/bidspath.py index fac85e9a9..943821398 100644 --- a/examples/bidspath.py +++ b/examples/bidspath.py @@ -81,7 +81,7 @@ # **data type**, i.e., ``meg`` for MEG data, ``eeg`` and ``ieeg`` for EEG and # iEEG data, or ``anat`` for anatomical MRI scans. Typically, MNE-BIDS will # infer the data type of your data automatically, for example when writing data -# using `mne_bids.write_raw_bids`. For the sake of this example, however, we +# using :func:`mne_bids.write_raw_bids`. For the sake of this example, however, we # are going to specify the data type explicitly. datatype = "eeg" diff --git a/examples/read_bids_datasets.py b/examples/read_bids_datasets.py index b0d03ed95..00263d3b8 100644 --- a/examples/read_bids_datasets.py +++ b/examples/read_bids_datasets.py @@ -78,7 +78,7 @@ # # We can use MNE-BIDS to print a tree of all # included files and folders. We pass the ``max_depth`` parameter to -# `mne_bids.print_dir_tree` to the output to four levels of folders, for +# :func:`mne_bids.print_dir_tree` to the output to four levels of folders, for # better readability in this example. print_dir_tree(bids_root, max_depth=4) diff --git a/examples/update_bids_datasets.py b/examples/update_bids_datasets.py index 23a693348..9e2383972 100644 --- a/examples/update_bids_datasets.py +++ b/examples/update_bids_datasets.py @@ -48,7 +48,7 @@ # # We can use MNE-BIDS to print a tree of all # included files and folders. We pass the ``max_depth`` parameter to -# `mne_bids.print_dir_tree` to the output to three levels of folders, for +# :func:`mne_bids.print_dir_tree` to the output to three levels of folders, for # better readability in this example. print_dir_tree(bids_root, max_depth=3) diff --git a/mne_bids/conftest.py b/mne_bids/conftest.py index 784ef0479..8c1a7a110 100644 --- a/mne_bids/conftest.py +++ b/mne_bids/conftest.py @@ -13,6 +13,15 @@ def pytest_configure(config): config.addinivalue_line("usefixtures", "monkeypatch_mne") +@pytest.fixture(autouse=True) +def close_all(): + """Close all figures after each test.""" + yield + import matplotlib.pyplot as plt + + plt.close("all") + + @pytest.fixture(scope="session") def monkeypatch_mne(): """Monkeypatch MNE to ensure we have download=False everywhere in tests.""" diff --git a/mne_bids/path.py b/mne_bids/path.py index 6144e01a4..f3709c4e3 100644 --- a/mne_bids/path.py +++ b/mne_bids/path.py @@ -901,7 +901,7 @@ def update(self, *, check=None, **kwargs): If a boolean, controls whether to enforce BIDS conformity. This will set the ``.check`` attribute accordingly. If ``None``, rely on the existing ``.check`` attribute instead, which is set upon - `mne_bids.BIDSPath` instantiation. Defaults to ``None``. + :class:`mne_bids.BIDSPath` instantiation. Defaults to ``None``. **kwargs : dict It can contain updates for valid BIDSPath entities: 'subject', 'session', 'task', 'acquisition', 'processing', 'run', @@ -1018,7 +1018,7 @@ def match(self, ignore_json=True, check=False): check : bool If ``True``, only returns paths that conform to BIDS. If ``False`` (default), the ``.check`` attribute of the returned - `mne_bids.BIDSPath` object will be set to ``True`` for paths that + :class:`mne_bids.BIDSPath` object will be set to ``True`` for paths that do conform to BIDS, and to ``False`` for those that don't. Returns @@ -2382,7 +2382,7 @@ def find_matching_paths( check : bool If ``True``, only returns paths that conform to BIDS. If ``False`` (default), the ``.check`` attribute of the returned - `mne_bids.BIDSPath` object will be set to ``True`` for paths that + :class:`mne_bids.BIDSPath` object will be set to ``True`` for paths that do conform to BIDS, and to ``False`` for those that don't. Returns @@ -2459,7 +2459,7 @@ def _fnames_to_bidspaths(fnames, root, check=False): check : bool If ``True``, only returns paths that conform to BIDS. If ``False`` (default), the ``.check`` attribute of the returned - `mne_bids.BIDSPath` object will be set to ``True`` for paths that + :class:`mne_bids.BIDSPath` object will be set to ``True`` for paths that do conform to BIDS, and to ``False`` for those that don't. Returns diff --git a/mne_bids/tests/data/tiny_bids/code/make_tiny_bids_dataset.py b/mne_bids/tests/data/tiny_bids/code/make_tiny_bids_dataset.py index 3ce5f24a3..753e4f81e 100644 --- a/mne_bids/tests/data/tiny_bids/code/make_tiny_bids_dataset.py +++ b/mne_bids/tests/data/tiny_bids/code/make_tiny_bids_dataset.py @@ -5,6 +5,7 @@ # %% import json +from datetime import date from pathlib import Path import mne @@ -45,7 +46,7 @@ "last_name": "Musterperson", "first_name": "Maxi", "middle_name": "Luka", - "birthday": (1970, 10, 20), + "birthday": date(1970, 10, 20), "sex": 2, "hand": 3, } diff --git a/mne_bids/tests/test_read.py b/mne_bids/tests/test_read.py index 03ef3aeb3..09a61af4e 100644 --- a/mne_bids/tests/test_read.py +++ b/mne_bids/tests/test_read.py @@ -827,12 +827,9 @@ def test_handle_chpi_reading(tmp_path): with ( pytest.warns(RuntimeWarning, match="Defaulting to .* mne.Raw object"), - pytest.warns( - RuntimeWarning, match="This file contains raw Internal Active Shielding" - ), pytest.warns(RuntimeWarning, match="The unit for channel"), ): - raw_read = read_raw_bids(bids_path) + raw_read = read_raw_bids(bids_path, extra_params=dict(allow_maxshield="yes")) # cHPI "off" according to sidecar, but present in the data meg_json_data_chpi_mismatch = meg_json_data.copy() diff --git a/mne_bids/tests/test_write.py b/mne_bids/tests/test_write.py index 2a35c587d..368a879f8 100644 --- a/mne_bids/tests/test_write.py +++ b/mne_bids/tests/test_write.py @@ -14,7 +14,7 @@ import shutil as sh import sys import warnings -from datetime import datetime, timedelta, timezone +from datetime import date, datetime, timedelta, timezone from glob import glob from pathlib import Path @@ -102,6 +102,8 @@ def _wrap_read_raw(read_raw): def fn(fname, *args, **kwargs): + if str(fname).endswith(".mff") and check_version("mne", "1.8"): + kwargs["events_as_annotations"] = True raw = read_raw(fname, *args, **kwargs) raw.info["line_freq"] = 60 return raw @@ -222,9 +224,12 @@ def test_write_participants(_bids_validate, tmp_path): # add fake participants data raw.set_meas_date(datetime(year=1994, month=1, day=26, tzinfo=timezone.utc)) + birthday = (1993, 1, 26) + if check_version("mne", "1.8"): + birthday = date(*birthday) raw.info["subject_info"] = { "his_id": subject_id2, - "birthday": (1993, 1, 26), + "birthday": birthday, "sex": 1, "hand": 2, } @@ -707,9 +712,12 @@ def test_fif(_bids_validate, tmp_path): # data # change the gender but don't force overwrite. raw = _read_raw_fif(raw_fname) + birthday = (1994, 1, 26) + if check_version("mne", "1.8"): + birthday = date(*birthday) raw.info["subject_info"] = { "his_id": subject_id2, - "birthday": (1994, 1, 26), + "birthday": birthday, "sex": 2, "hand": 1, } diff --git a/mne_bids/write.py b/mne_bids/write.py index 19da86847..7d03ad322 100644 --- a/mne_bids/write.py +++ b/mne_bids/write.py @@ -11,7 +11,7 @@ import sys import warnings from collections import OrderedDict, defaultdict -from datetime import datetime, timedelta, timezone +from datetime import date, datetime, timedelta, timezone from pathlib import Path import mne @@ -456,12 +456,14 @@ def _participants_tsv(raw, subject_id, fname, overwrite=False): # determine the age of the participant age = subject_info.get("birthday", None) + if isinstance(age, tuple): # can be removed once MNE >= 1.8 is required + age = date(*age) meas_date = raw.info.get("meas_date", None) if isinstance(meas_date, (tuple, list, np.ndarray)): meas_date = meas_date[0] if meas_date is not None and age is not None: - bday = datetime(age[0], age[1], age[2], tzinfo=timezone.utc) + bday = datetime(age.year, age.month, age.day, tzinfo=timezone.utc) if isinstance(meas_date, datetime): meas_datetime = meas_date else: @@ -1408,7 +1410,7 @@ def write_raw_bids( already loaded from disk unless ``allow_preload`` is explicitly set to ``True``. See warning for the ``allow_preload`` parameter. bids_path : BIDSPath - The file to write. The `mne_bids.BIDSPath` instance passed here + The file to write. The :class:`mne_bids.BIDSPath` instance passed here **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, diff --git a/pyproject.toml b/pyproject.toml index 272befae4..ce3c1d677 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,12 +59,13 @@ full = [ ] # Dependencies for running the test infrastructure -test = ["mne_bids[full]", "pytest", "pytest-cov", "pytest-sugar", "ruff"] +test = ["mne_bids[full]", "pytest >= 8", "pytest-cov", "pytest-sugar", "ruff"] # Dependencies for building the documentation doc = [ "nilearn", - "sphinx", + # TODO: Remove pin once https://github.com/sphinx-doc/sphinx/issues/12589 is fixed + "sphinx!=7.4.6,!=7.4.5,!=7.4.4,!=7.4.3,!=7.4.2,!=7.4.1,!=7.4.0", "sphinx_gallery", "sphinx-copybutton", "pydata-sphinx-theme", @@ -148,4 +149,6 @@ filterwarnings = [ # old MNE _fake_click "ignore:The .*_event function was deprecated in Matplotlib.*:", "ignore:datetime\\.datetime\\.utcfromtimestamp.* is deprecated and scheduled for removal in a future version.*:DeprecationWarning", + # matplotlib + "ignore:Figure.*is non-interactive.*cannot be shown:UserWarning", ]