Skip to content

Honor the download-flag contract across all datasets and harden the NEMAR sourcedata path - #1146

Open
bruAristimunha wants to merge 9 commits into
developfrom
fix/download-flags-nemar
Open

Honor the download-flag contract across all datasets and harden the NEMAR sourcedata path#1146
bruAristimunha wants to merge 9 commits into
developfrom
fix/download-flags-nemar

Conversation

@bruAristimunha

Copy link
Copy Markdown
Collaborator

What

Two related fix-sets, landed together because they share the same download layer (datasets/base.py, datasets/download.py):

1. The download-flag contract — enforced for every dataset

An AST audit of every data_path implementation found flags accepted but silently ignored across the catalogue:

  • CacheConfig.overwrite_* was a no-op with the default use=Falseget_data(cache_config=dict(overwrite_raw=True)) erased nothing. The docstring also claimed save_* was ignored in that case while the code honored it. Overwrite now always erases; docs match behavior.
  • CompoundDataset.data_path dropped all four flags when delegating to the wrapped dataset.
  • 22 dataset data_path implementations ignored path and/or force_update (full list in the commit message) — including every recent dataset that resolved its storage dir via get_dataset_path(SIGN, None) with the user's path hardcoded away.
  • Kojima2024a/b narrowed the data_path signature to (subject, path), so BaseDataset.download() raised TypeError: unexpected keyword argument 'force_update'.
  • force_update now also re-extracts stale archives; download_if_missing gains a force_update parameter as the choke point for the manifest-based datasets (dreyer2023, erpcore2021, kojima2024a/b, mainsah2025).
  • update_path is left as documented-deprecated (download.py: "Unused, kept for compatibility purpose").

Guard rails: a new AST regression test fails CI if any dataset's data_path ever ignores path or force_update again (exempt: the abstract base and FakeDataset), and a new cache test proves overwrite_raw=True, use=False actually erases.

2. NEMAR sourcedata hardening (follow-ups to #1139)

  • Per-subject upstream fallback: one failing subject no longer discards the whole NEMAR batch — only the failed subjects fall back to data_path, keeping what NEMAR already served. The now-unused batch helper _download_nemar_sourcedata is removed.
  • sourcedata_path resolves subjects through nemar_subject_template, matching the sibling BIDS path (_download_nemar). Datasets like ErpCore2021 ({subject:03d}) previously sent the raw int to the provenance matcher.
  • Honest error taxonomy: only SelectionError maps to "deposit publishes no sourcedata"; transport/verification/S3 failures now report as download failures with the original error text.
  • Glob safety: manifest filenames are glob.escaped before being used as nemar include patterns and local verification globs, so upstream names containing [ ] * ? still match.
  • Docs: the changelog's nemar_sourcedata_include attribute (never implemented) is replaced with the real provenance mechanism; nemar_dl/nemar_sourcedata_dl are registered in api.rst; docstrings no longer promise a NemarDownloadError for a missing nemar-py (that raises ImportError at import time).
  • Direct tests for set_download_provider/get_download_provider (previously only exercised through monkeypatched seams): validation, case normalization, None reset, env-var precedence, unknown-value fallback — using the isolated MNE config fixture so nothing touches the real ~/.mne config.

Not in scope (deliberate, candidates for follow-ups)

  • A per-call provider= override / context manager instead of the persisted global (set_download_provider still writes the MNE config).
  • Lazy import nemar (today import moabb requires nemar-py even with provider="upstream").
  • Wiring the NEMAR sourcedata cache into data_path/get_data so a prefetch warms the evaluation path — needs a design decision on cache-location mapping.
  • verbose in the pooch-based downloaders (progress bars are hardcoded; no slot to honor it).

Test plan

  • pytest moabb/tests/test_download.py moabb/tests/test_datasets.py -k 'not doi' → 1464 passed
  • pytest moabb/tests/test_paradigms.py → 102 passed; pytest moabb/tests/test_evaluations.py → 93 passed
  • AST audit sweep → every dataset honors path + force_update
  • pre-commit run --all-files → clean

…urcedata path

Flag contract:
- CacheConfig.overwrite_* now erases the cache even when use=False
  (previously a silent no-op with the default config); docstring and
  make() examples corrected to match behavior.
- CompoundDataset.data_path forwards path/force_update/update_path/verbose
  to the wrapped dataset instead of dropping all four.
- data_path implementations that ignored path or force_update now honor
  them: Weibo2014, alphawaves, bbci_eeg_fnirs (Shin2017 helpers), beetl,
  chailloux2020, dreyer2023, epfl, erpcore2021, hinss2021, huebner_llp,
  kaneshiro2015, lee2024, liu2024, mainsah2025, phmd_ml, physionet_mi,
  sosulski2019, speier2017, ssvep_mamem, stieger2021, zhang2025, zheng2020.
- Kojima2024a/b data_path regains the standard signature; its narrowed
  form made BaseDataset.download() raise TypeError.
- force_update also re-extracts stale zip folders; download_if_missing
  gains a force_update parameter as the choke point for manifest-based
  datasets.
- New AST regression test enforces that every dataset's data_path uses
  path and force_update; new test proves overwrite works with use=False.

NEMAR sourcedata:
- download() falls back to the upstream host per failing subject instead
  of discarding the whole NEMAR batch; the unused batch helper is removed.
- sourcedata_path resolves subjects through nemar_subject_template,
  matching the sibling BIDS path.
- Transport/verification failures are no longer misreported as 'deposit
  publishes no sourcedata'; SelectionError keeps that message.
- Manifest filenames are glob-escaped before being used as include
  patterns and local verification globs.
- Stale nemar_sourcedata_include changelog claim replaced with the real
  provenance mechanism; nemar_dl/nemar_sourcedata_dl added to api.rst;
  docstrings no longer promise a NemarDownloadError for a missing
  nemar-py (that raises ImportError at import time).
- Direct round-trip tests for set/get_download_provider (validation,
  normalization, None reset, env-var precedence, unknown-value fallback)
  and a per-subject fallback test.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

…anifest errors honestly

Real provenance manifests (nm000266, nm000221) key subjects by the raw
MOABB id, while nemar_subject_template documents how a deposit labels
its subjects -- so sourcedata_path now offers both forms as aliases and
_sourcedata_files_for_subject matches any of them, instead of betting on
a single convention. Manifest entries without a usable 'file' key are
skipped rather than raising KeyError, and a missing
sourcedata_provenance.json (SelectionError) is now distinguished from a
transport failure when fetching the manifest.
@bruAristimunha

Copy link
Copy Markdown
Collaborator Author

Follow-up push (bd7c95c) after verifying against live NEMAR deposits:

  • Fetched the real sourcedata_provenance.json for nm000266 (Sosulski2019) and nm000221 (Cattan2019/alphawaves): both key subjects by the raw MOABB id ("1", "10", …). nm000132 (ErpCore2021) publishes no sourcedata at all, and on004362 (PhysionetMI) has no provenance manifest — so neither templated dataset can exercise the sourcedata path today.
  • Consequence: sourcedata_path no longer bets on one convention. It now passes the raw id and the nemar_subject_template label as aliases, and _sourcedata_files_for_subject matches either. Works with today's raw-id manifests and with template-labelled ones if enrichment ever produces them.
  • Also fixed while in there: a missing sourcedata_provenance.json (SelectionError) is now reported as "publishes no sourcedata manifest" instead of the generic "could not read", transport failures fetching the manifest carry the original error text, and manifest entries without a file key are skipped instead of raising KeyError.
  • New tests: alias matching against both manifest conventions (including a glob-metachar filename), and the template-alias plumbing through sourcedata_path.

Still deliberately open (needs a design decision): wiring the NEMAR sourcedata cache into data_path/get_data, per-call provider= override, lazy import nemar.

The reader was designed for the original distribution where each of the
seven components was a standalone BIDS dataset in its own
MNE-erpcore{task}2021-data folder with a per-task MNE config sign. The
dataset is one BIDS tree whose components are separated by the task-
entity (as the NEMAR deposit nm000132 also publishes it), so all seven
classes now share a single MNE-erpcore2021-data root under one
ERPCORE2021 sign; BIDSPath(task=...) already selects each component's
files, and the per-component filenames never collide because each
carries its task. A pre-existing download in the legacy separated layout
is detected per subject and read as-is instead of re-fetching. The load
path is unchanged apart from reading from the shared root.
@bruAristimunha

Copy link
Copy Markdown
Collaborator Author

Third commit (3627d8d): ERP CORE combined-layout fix.

The reader was built for the original distribution where every component was its own standalone BIDS dataset (seven MNE-erpcore{task}2021-data roots, each with its own MNE config sign keyed by the task name). ERP CORE is one BIDS dataset whose components are separated by the task- entity — the same shape the NEMAR deposit nm000132 publishes (tasks=[MMN,N170,N2pc,N400,P3,flankers]).

  • All seven classes now share a single MNE-erpcore2021-data root under one ERPCORE2021 sign; BIDSPath(task=...) already selected per component, so the load path is unchanged apart from reading the shared root. Per-component filenames carry their task, so nothing collides when the trees merge.
  • A pre-existing download in the legacy separated layout is detected per subject and read as-is — no forced re-download for anyone who already has the seven folders.
  • Two offline tests pin it: both components resolve to the same root with the component prefix stripped, and the legacy fallback triggers without fetching.

A side benefit: the local layout now mirrors nm000132's combined BIDS tree one-to-one, which makes eventually wiring the NEMAR BIDS copy into this reader a root-swap rather than a restructure.

…ranges

Same disease as ERP CORE's separated layout: the A/B/C classes and the
combined class all read the same globally numbered deposit (nm000250,
one OSF manifest keyed by sub-NN.zip), yet each stored into its own
MNE-Dreyer2023{X}-data folder -- up to four copies of the same files and
dataset-level assets. All four classes now share MNE-dreyer2023-data
under one Dreyer2023 config sign; a pre-existing legacy per-class
download is detected per subject and read as-is. get_subject_info moves
to the shared root too. A sweep of every multi-class dataset module
confirms no other family splits one source across per-code folders
(Shin2017, BEETL and Huebner already share a root; Lee2019, Kojima2024,
MAMEM and BrainInvaders classes fetch genuinely distinct deposits).
@bruAristimunha

Copy link
Copy Markdown
Collaborator Author

Fourth commit (833afc5): the layout audit found one more family with ERP CORE's disease — Dreyer2023.

Systematic sweep: grouped every dataset class by NEMAR deposit and scanned every multi-class module for per-self.code storage of a single source. Two families shared one deposit across classes: nm000132 (ErpCore2021 ×7, fixed in the previous commit) and nm000250 (Dreyer2023 A/B/C + combined) — four classes reading one globally numbered dataset (one OSF manifest keyed by sub-NN.zip) but each storing into its own MNE-Dreyer2023{X}-data folder, i.e. up to four copies of the same files.

  • All four classes now share MNE-dreyer2023-data under one Dreyer2023 sign; A/B/C just select their subject ranges (sub-01..60 / 61..81 / 82..87), which coexist in one root with no collisions.
  • Legacy per-class downloads are detected per subject and read as-is — no forced re-fetch.
  • get_subject_info (performance.csv) moves to the shared root as well.
  • Tests: A and B resolve to the same root with real zip extraction exercised; the legacy fallback triggers without downloading.

Everything else checked clean: Shin2017, BEETL and Huebner already share a root; Lee2019, Kojima2024, MAMEM and BrainInvaders classes fetch genuinely distinct deposits, so their per-code folders are correct. Full suites: 1470 passed, pre-commit clean.

@bruAristimunha

Copy link
Copy Markdown
Collaborator Author

Coverage verification, per class rather than per file, since most of MOABB's 167 registered dataset classes (161 regular + 6 compound) inherit a shared data_path:

  • Resolved every registered class to the actual data_path definition it executes (96 distinct definitions) and audited each for path/force_update usage.
  • On this branch: 165 compliant + 2 exempt (FakeDataset variants) = 0 violations.
  • Same audit against the pre-PR state: 64 violating classes (the per-implementation fixes fan out — e.g. one mainsah2025.py fix covers its 21 registered classes, the compound-dataset fix covers all *_Il classes).

The file-based AST regression test in test_datasets.py subsumes the per-class view (every definition a class can resolve to lives in the swept files), so this stays enforced.

On "why not all datasets" for the NEMAR side: 73 of 161 classes declare a nemar_id today. The remaining ones have no public NEMAR deposit to point at — that is a data-publication task on the NEMAR side (as in #1145, which flipped 15 datasets the moment their deposits went public), not a code change this PR can make.

@bruAristimunha

Copy link
Copy Markdown
Collaborator Author

Correction to my previous comment: the "73 of 161 classes declare a nemar_id" figure was wrong — that grep only matched nm-prefixed, single-line declarations in top-level dataset files.

Counted properly at class level (resolved cls.nemar_id for every registered class, base default is None): 156 of 161 classes declare a nemar_id (prefixes nm, on, ds). The only ones without: FakeDataset / FakeVirtualRealityDataset (nothing to download) and three 2026 additions whose deposits aren't up yet (Lenaig2026, Schrag2026Pediatric, Wang2026).

This changes the weight of the open wiring question: NEMAR coverage is nearly universal, so set_download_provider only affecting download() (and never get_data) is a gap for essentially the whole catalog, not half of it. Worth prioritizing in the NeuroAI discussion.

data_path now forwards force_update to download_if_missing, so the
monkeypatched stand-in must take keyword arguments. This was the single
real CI failure; the other five matrix legs were fail-fast cancellations
cascading from it.
The store fetched by download() landed in a layout no loader read, so
get_data() re-contacted the upstream host and NEMAR mirroring protected
nothing (gh-1147). Loading now resolves files from the governed store
first, with the URL-derived trees demoted to lookups so existing
installs never re-download.

Mechanics, kept deliberately small after review:
- BaseDataset._sourcedata_store() is the one place that resolves a
  dataset's store (nemar_id + provider policy + shared nemar_store()
  root used by both writer and readers).
- The loader publishes that store via a contextvar around
  _get_single_subject_data; data_dl, the deprecated data_path, and
  download_if_missing (manifest-based datasets) each consult it with a
  one-line hook.
- The store keeps the upstream relative layout, so files are found by
  probing the trailing URL-path segments -- no globbing, no ranking.
- force_update refetches upstream; provider 'upstream' opts loading out.

Verified live on the gh-1147 reproducer: Schirrmeister2017 download()
fetched 490 MB from NEMAR (subject 1); get_data() then loaded both runs
in 1s with all sockets blocked.
Serving the store path directly let datasets that move their downloaded
files (Schirrmeister2017 does shutil.move on data_dl's return) gut the
store on first use -- caught by re-running the live gh-1147 reproducer.
data_dl and the deprecated data_path now hardlink (copy on cross-device)
the store file into the normal URL-derived destination and return that,
so the dataset owns its copy, the store stays pristine, and the return
contract is byte-identical to a normal download. Also per review: drop
the compound-dataset wraps and the download_if_missing hook to keep the
wiring surgical, and dedupe the test doubles into two module-level
helpers with a socket-level network guard.

Verified live twice in a row on Schirrmeister2017 with all sockets
blocked; the store survives the dataset's file moves.
- The ErpCore2021 and Dreyer2023 split-family pairs collapse into one
  spec table, one shared fake downloader, and two parametrized tests.
- The three data_dl source-resolution scenarios (store hit, no active
  store, force_update) become one parametrized test; the fake retrieve
  now honors known_hash like pooch so a filled destination is kept.
- The gh-1147 move-survival regression keeps its own test; the network
  guard and retrieve fake are two shared module-level helpers, imports
  hoisted to the top of the file.
- The sourcedata subject-alias test parametrizes over the default and a
  custom nemar_subject_template.
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.

1 participant