镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

ENH: Add _soft_import check for sklearn in mne.decoding - #14409

Open
SohamXYZDev wants to merge 1 commit into
mne-tools:mainfrom
SohamXYZDev:fix/decoding-sklearn-soft-import
Open

SohamXYZDev wants to merge 1 commit into
mne-tools:mainfrom
SohamXYZDev:fix/decoding-sklearn-soft-import

Conversation

@SohamXYZDev

Copy link
Copy Markdown

Reference issue (if any)

Fixes #14394.

What does this implement/fix?

Adds _soft_import("sklearn", "decoding") before top-level sklearn imports across the mne.decoding submodules (_fixes.py, base.py, ems.py, receptive_field.py, search_light.py, time_delaying_ridge.py, time_frequency.py, and transformer.py) so that accessing mne.decoding functionality without scikit-learn installed raises MNE's informative RuntimeError instead of a raw ModuleNotFoundError.

Also updates mne/tests/test_docstring_parameters.py to handle the RuntimeError from _soft_import when walking mne.decoding in minimal environments without scikit-learn, and adds test coverage in mne/utils/tests/test_check.py.

Additional information

Because ruff (I001) sorts third-party from sklearn... imports before relative from .base import ... imports, placing _soft_import only in base.py would still allow from sklearn... in submodules like transformer.py or search_light.py (as well as ems.py and _fixes.py, which do not import from base.py) to fail with ModuleNotFoundError before base.py is reached. Placing _soft_import("sklearn", "decoding") right before the sklearn imports in each submodule (with # noqa: E402, matching mne/export/_edf_bdf.py and _eeglab.py) ensures every entry point in mne.decoding.__all__ consistently raises the _soft_import error while keeping import mne and import mne.decoding lazy.

I used Gemini 3.8 Flash to help check import ordering across the decoding submodules and draft the unit test, and reviewed and verified all changes locally.

@larsoner

Copy link
Copy Markdown
Member

@tsbinns can you look? If not, I can

@tsbinns tsbinns self-assigned this Oct 13, 2026
@tsbinns

tsbinns commented Oct 13, 2026

Copy link
Copy Markdown
Contributor

@larsoner Yeah, can do

@tsbinns

tsbinns commented Oct 13, 2026

Copy link
Copy Markdown
Contributor

@SohamXYZDev Thanks for submitting this PR.

Because ruff (I001) sorts third-party from sklearn... imports before relative from .base import ... imports, placing _soft_import only in base.py would still allow from sklearn... in submodules like transformer.py or search_light.py (as well as ems.py and _fixes.py, which do not import from base.py) to fail with ModuleNotFoundError before base.py is reached.

Good point, that does complicate things, but having to noqa each sklearn import line is a bit unwieldy.

At the very least, we could nest the sklearn imports that are being moved to the end between # ruff: disable[E402] and # ruff: enable[E402], which would avoid the need to specify this per-line.


However, I wonder if a cleaner approach is to still try and centralise the check in base.py, with something like

from ..utils import _soft_import  # isort: skip
_soft_import("sklearn", name="mne.decoding")

at the top of base.py, and then in the submodules import .base # isort: skip # noqa: E402 at the top of each file (noqa if it's never used).

For the warnings this would cause, we could either nest the affected imports in # ruff: disable[E402] ... # ruff: enable[E402], or add E402 to the ruff per-file-ignores in pyproject.toml, e.g.:

"mne/decoding/*.py" = [
  "E402",  # Module level import not at top of file
]

@larsoner WDYT about that?

Comment thread mne/utils/check.py
Comment on lines +436 to +439
class _SoftImportError(RuntimeError, ImportError):
"""Raised when an optional dependency is missing or incompatible."""

pass

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIUC, the ImportError comes from ._fixes.py, but shouldn't that never be encountered since we are adding the _soft_import check at the top of that file?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the minimal / 3.12 CI build (where scikit-learn is not installed), pytest collects doctests using --doctest-modules --doctest-ignore-import-errors.

That pytest flag only ignores errors that inherit from ImportError. Because _soft_import raised a raw RuntimeError, pytest failed during test collection when importing _fixes.py. Making _SoftImportError inherit from both RuntimeError and ImportError preserves MNE's RuntimeError behavior while letting pytest's doctest collector skip it cleanly when scikit-learn is absent.

@larsoner

Copy link
Copy Markdown
Member

Yeah I think centralizing is better, that way if we ever want to change it's easier

@SohamXYZDev
SohamXYZDev force-pushed the fix/decoding-sklearn-soft-import branch from 59ac762 to 17d0141 Compare October 14, 2026 01:57
@SohamXYZDev

Copy link
Copy Markdown
Author

@tsbinns @larsoner Updated! Rebased on latest main and refactored as suggested:

  1. Centralized _soft_import("sklearn", "decoding") at the top of mne/decoding/base.py.
  2. Submodules now import base first with # isort: skip.
  3. Added "mne/decoding/*.py" = ["E402"] to per-file-ignores in pyproject.toml and stripped the per-line noqa comments.
  4. Resolved circular dependencies by keeping MNETransformerMixin in base.py and localizing Vectorizer inside get_coef.

All 1,311 decoding tests pass locally, and the missing-sklearn import checks verified cleanly.

@SohamXYZDev
SohamXYZDev force-pushed the fix/decoding-sklearn-soft-import branch from 17d0141 to 0b0bdcd Compare October 14, 2026 06:49
@mne-tools mne-tools deleted a comment from Zhaokaixlx Oct 14, 2026
Comment thread mne/decoding/base.py Outdated
Comment on lines +60 to +61
class MNETransformerMixin(TransformerMixin):
"""TransformerMixin plus some helpers."""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, didn't realise this would introduce a circular import conflict. It's preferable if we don't have to move the mixin around.

Perhaps we can have the import check in a new _check.py file to avoid the circular imports. CC @larsoner

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sounds good to me, happy to move the check into a dedicated _check.py and restore the mixin once confirmed

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that works for me

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated: moved the check into mne/decoding/_check.py and restored the mixin and transformer imports back to transformer.py. nesting checks and tests pass locally.

@SohamXYZDev
SohamXYZDev force-pushed the fix/decoding-sklearn-soft-import branch from 0b0bdcd to 30b33c4 Compare October 23, 2026 17:54

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add a check for sklearn availability to decoding module

3 participants