Skip to content

Populate per-asset dataStandard for BIDS, HED, NWB, and extensions - #1811

Open
yarikoptic wants to merge 10 commits into
masterfrom
hed
Open

yarikoptic wants to merge 10 commits into
masterfrom
hed

Conversation

@yarikoptic

@yarikoptic yarikoptic commented Feb 24, 2026 •

Copy link
Copy Markdown
Member

Populates the per-asset dataStandard introduced in dandi/dandi-schema#371 so the archive can tell which standards, versions and extensions each asset (and thus dandiset) uses:

  • NWB: version from the file, ndx-* extensions (latest cached version of each) from the HDF5 specifications group (get_nwb_extensions()).
  • BIDS: BIDSVersion from dataset_description.json, declared on every asset of the BIDS dataset.
  • HED: HEDVersion, on dataset_description.json; library schemas ([prefix:]library_version, e.g. sc:score_1.0.0) become extensions.
  • OME/NGFF: for .ome.zarr assets.

All of it is gated on _SCHEMA_BAREASSET_HAS_DATASTANDARD, so it is a no-op with released dandischema (< 0.15.0). The last commit (TEMP) installs dandischema from the dandi-schema PR branch and is to be replaced with a version bound once that is released.

Details: review responses, changes, testing

Review responses

  • Previous description's TODO about a CI run against the schema PR: covered by the TEMP commit pinning enh-hed (dandi-schema's own CI runs dandi-cli master against its branch).
  • @bendichter: "line 542 of bases appears to have similar logic" -- the append-if-missing logic now lives in a single _add_data_standard() in files/bases.py, used by NWB, BIDS and Zarr assets; NWB extraction moved into _get_nwb_data_standard().
  • HED stored inside NWB: captured as the ndx-hed extension for now (as seen on 001636); anything more specific waits until the representation settles.
  • CodeQL "cyclic import" alerts on files/bids.py: bids.py imported from .bases already on master, so this is not new.

Changes in this round (rebased onto current master)

  • Rebased; resolved conflicts in .gitignore, CLAUDE.md (now defers to DEVELOPMENT.md; the import-placement guidance went there), pyproject.toml, tests.
  • Expect the feature in dandischema 0.15.0 (guard and TODOs said 0.12.2).
  • get_nwb_extensions(): after rebase Version was no longer imported in pynwb_utils, and the broad except Exception silently dropped every extension. Now imports it, catches only InvalidVersion, and falls back to the lexicographically last version instead of dropping the extension.
  • HED parsing followed "sc:1.0.0", which is not the spec'd format: entries are [prefix:]version or [prefix:]library_version; the prefix is a dataset-local alias, so extensions are now named by library (score, not sc), and prefix-less partnered libraries (lang_1.1.0) work.
  • No crash on non-string BIDSVersion, or on a dataset_description.json that is not a JSON object; NWB files whose version/extensions cannot be read still get a (bare) NWB standard with ignore_errors=True.
  • OME/NGFF also declared for non-BIDS .ome.zarr assets (detected by the .ome.zarr ending, so dots in the name are fine).
  • BIDS is declared on all assets of a BIDS dataset (it is the field's "standard(s) applicable to this asset"), and NWB files in BIDS datasets keep it: their merged metadata used to let the NWB dataStandard override the BIDS one. dataset_description.json is read once, as UTF-8.
  • Unit test that _maybe_downgrade_metadata() drops a populated per-asset dataStandard for 0.7.0/0.8.0 servers (needs the downgrade in Add per-asset dataStandard, HED standard, and extensions to StandardsType dandi-schema#371).
  • TEMP note in pyproject.toml: when restoring the version bound after the dandischema release, set the version checked by the _SCHEMA_BAREASSET_HAS_DATASTANDARD guard (assumed 0.15.0) to that release's.

Testing

  • Full test suite incl. docker-compose integration tests (dandischema from the PR branch, server with dandischema 0.12.1): 1229 passed, 1 skipped. With released dandischema 0.14.0 the feature is a no-op and its tests skip. mypy clean against both dandischema versions; flake8, black, isort clean.
  • End-to-end against a local dandi-archive (this repo's docker-compose fixture), uploading generated NWB files using ndx-hed/ndx-pose, a .ome.zarr, and bids-examples eeg_ds003645s_hed_library:
server w/ dandischema 0.12.1 (schema 0.7.0, as production) server w/ dandi/dandi-schema#371 (schema 0.9.0)
asset validation all Valid (3 NWB/Zarr, 32 BIDS) all Valid
asset schemaVersion downgraded by the client to 0.7.0 0.9.0
asset dataStandard dropped by the downgrade NWB 2.11.0 + ndx-hed 0.2.0 (+ ndx-pose 0.4.0); OME/NGFF; BIDS 1.11.1 on every BIDS asset; HED 8.4.0 + score 1.0.0, testlib 1.0.2 on dataset_description.json
assetsSummary.dataStandard OME/NGFF, NWB; BIDS (path heuristics, as before) OME/NGFF, NWB 2.11.0 + {ndx-hed, ndx-pose}; BIDS 1.11.1, HED 8.4.0 + {score, testlib} -- one entry per standard

(Images from mirror.gcr.io; the 0.9.0 server is the stock dandiarchive/dandiarchive-api with the dandischema wheel from the schema PR branch installed, as dandi-schema's tools/api-with-schema.Dockerfile does. The summary is aggregated only from Valid assets when the draft becomes pending, so the test touched the dandiset metadata after validation to get it re-aggregated.)

🤖 Generated with Claude Code

https://claude.ai/code/session_01P3VJkUsu17gjmUEf4ZY1RW

Comment thread dandi/files/bids.py Fixed
@codecov

codecov Bot commented Feb 24, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.74775% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.96%. Comparing base (ad8f64d) to head (d05dcca).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
dandi/files/bids.py 94.33% 3 Missing ⚠️
dandi/files/bases.py 92.00% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1811      +/-   ##
==========================================
+ Coverage   90.78%   90.96%   +0.18%     
==========================================
  Files          92       92              
  Lines       14130    14382     +252     
==========================================
+ Hits        12828    13083     +255     
+ Misses       1302     1299       -3     
Flag Coverage Δ
unittests 90.96% <97.74%> (+0.18%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@yarikoptic yarikoptic added the minor Increment the minor version when merged label Mar 3, 2026
Comment thread dandi/files/bids.py Outdated
BIDS_DATASET_ERRORS = ("BIDS.MANDATORY_FILE_MISSING_PLACEHOLDER",)


def _add_standard(

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.

line 542 of bases appears to have similar logic

Comment thread dandi/files/bases.py Outdated
Comment thread dandi/files/bids.py Fixed
@bendichter

Copy link
Copy Markdown
Member

@yarikoptic lgtm! Let me just check on some of my own data

@bendichter

Copy link
Copy Markdown
Member

Added NWB spec version extraction in 279c297.

get_nwb_version() already existed in pynwb_utils.py but wasn't being used when constructing the StandardsType. Now the version is populated from the HDF5 nwb_version attribute.

Tested against dandiset 001636 (346 NWB files, macaque electrophysiology):

Before:

Neurodata Without Borders (NWB) (version=None)
  ext: ndx-events v0.4.0
  ext: ndx-hed v0.2.0

After:

Neurodata Without Borders (NWB) (version=2.9.0)
  ext: ndx-events v0.4.0
  ext: ndx-hed v0.2.0

@bendichter
bendichter self-requested a review March 3, 2026 20:23

@bendichter bendichter left a comment

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.

ok, I made a minor correction. LGTM now!

yarikoptic and others added 10 commits October 13, 2026 20:36
- BIDSDatasetDescriptionAsset.get_metadata(): always sets BIDS standard
  (with BIDSVersion), adds HED when HEDVersion present in JSON, warns
  on read failure
- NWBAsset.get_metadata(): sets NWB standard
- ZarrBIDSAsset.get_metadata(): sets OME/NGFF for .ome.zarr assets
- Guard for older dandischema without dataStandard on BareAsset;
  RuntimeError if dandischema >= 0.12.2 lacks it
- Register ai_generated pytest marker in tox.ini

Requires dandischema with per-asset dataStandard support (0.12.2+).
Works silently with older dandischema.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add get_nwb_extensions() to pynwb_utils that reads h5file["specifications"]
  to discover ndx-* namespaces and their versions (filtering out core/hdmf)
- NWBAsset.get_metadata() populates StandardsType.extensions with ndx-*
  extensions found in the NWB file
- BIDSDatasetDescriptionAsset.get_metadata() extracts HED library schemas
  from list-valued HEDVersion (e.g. ["8.2.0", "sc:1.0.0"]) into extensions
- Add tests for both NWB extension extraction and HED library schema parsing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…S_DATASTANDARD

Single bool in bases.py guards all dataStandard/version/extensions
features that ship together in dandischema >= 0.12.2, replacing
scattered "field in Model.model_fields" checks.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…PMENT.md

Move json, h5py, get_nwb_extensions, and _SCHEMA_BAREASSET_HAS_DATASTANDARD
imports to module top level in tests.  Keep pynwb_utils import deferred in
bases.py (heavy transitive deps: h5py/pynwb/hdmf/numpy) per existing
convention.  Add import guidance to DEVELOPMENT.md (CLAUDE.md defers to it).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
hed_standard does not exist in dandischema < 0.12.2, so gate the import
on _SCHEMA_BAREASSET_HAS_DATASTANDARD (all new symbols ship together).
HED detection is skipped when unavailable.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move _SCHEMA_BAREASSET_HAS_DATASTANDARD guard block after all imports
to fix E402.  Use getattr/setattr for dataStandard access and
type: ignore[call-arg] for StandardsType(version=...) to satisfy
mypy against released dandischema that lacks these fields.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use the existing get_nwb_version() to populate the version field on the
NWB StandardsType entry so per-asset metadata reports the NWB spec
version (e.g. 2.9.0) alongside extensions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Factor out _add_data_standard() and _get_nwb_data_standard() into
  files/bases.py and use them from NWB, BIDS and Zarr assets, instead of
  the duplicated append-if-missing logic (review by @bendichter).
- NWB: a file whose version/extensions cannot be read no longer makes
  get_metadata(ignore_errors=True) raise; it gets a bare NWB standard.
- get_nwb_extensions(): import packaging's Version (it was no longer
  imported after rebase, and the broad `except Exception` silently dropped
  every extension), catch only InvalidVersion, and fall back to the
  lexicographically last version instead of dropping the extension.
- HED: parse HEDVersion entries per HED spec as `[prefix:]version` or
  `[prefix:]library_version` (e.g. "sc:score_1.0.0", partnered
  "lang_1.1.0"), recording libraries as extensions named by library, not
  by the dataset-specific prefix.  Ignore non-string entries.
- BIDS: declare BIDS (with BIDSVersion) on every asset of a BIDS dataset,
  not only dataset_description.json, as dataStandard lists the standards
  applicable to the asset; keep it for NWB files in BIDS datasets, whose
  merged metadata had the NWB dataStandard override the BIDS one.  Read
  dataset_description.json once, as UTF-8, and do not crash on
  non-string BIDSVersion or a file which is not a JSON object.
- OME/NGFF: also declare it for non-BIDS .ome.zarr assets, detected by
  the ".ome.zarr" ending.
- Expect the feature in dandischema 0.15.0 (DANDI schema 0.9.0).
- Tests: cover the above, incl. NWBAsset with an embedded extension, a
  BIDS dataset with NWB/Zarr/other assets, and dropping of per-asset
  dataStandard by _maybe_downgrade_metadata() for 0.7.0/0.8.0 servers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P3VJkUsu17gjmUEf4ZY1RW
This enables CI to test against the unreleased dandischema that has
the dataStandard field and StandardsType enhancements.

TO BE REVERTED before merge.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Comment thread dandi/files/bids.py
Comment on lines +23 to +29
from .bases import (
_SCHEMA_BAREASSET_HAS_DATASTANDARD,
GenericAsset,
LocalFileAsset,
NWBAsset,
_add_data_standard,
)

Check notice

Code scanning / CodeQL

Cyclic import Note

Import of module
dandi.files.bases
begins an import cycle.

Copilot Autofix

AI about 11 hours ago

Best general approach: break the cycle by removing one side of the top-level import relationship. In this file, that means avoiding eager import from .bases at module import time, and instead doing lazy/local imports only where needed. This preserves behavior while preventing the cycle during initialization.

Single best fix in this snippet:

  • In dandi/files/bids.py, keep only type-only imports of GenericAsset, LocalFileAsset, and NWBAsset under TYPE_CHECKING.
  • Replace runtime inheritance/import dependency on LocalFileAsset by using postponed annotations (already enabled) plus a runtime alias guarded by local import logic, or (more simply in this file) keep class bases as-is but defer the import block from .bases into function/class scopes where _add_data_standard and _SCHEMA_BAREASSET_HAS_DATASTANDARD are used.
  • Because only shown lines are available, the concrete edit we can safely apply is to remove eager runtime import of .bases symbols and move them to type-checking context at top level; then add local imports at usage points in shown regions if any. In the provided snippet, usage points for helper constants/functions are outside shown region, so we can only prepare safe type-only separation here.
Suggested changeset 1
dandi/files/bids.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/dandi/files/bids.py b/dandi/files/bids.py
--- a/dandi/files/bids.py
+++ b/dandi/files/bids.py
@@ -7,7 +7,7 @@
 import json
 from pathlib import Path
 from threading import Lock
-from typing import Any
+from typing import TYPE_CHECKING, Any
 import weakref
 
 from dandischema.models import (
@@ -20,13 +20,8 @@
 import dandi
 from dandi.bids_validator_deno import bids_validate
 
-from .bases import (
-    _SCHEMA_BAREASSET_HAS_DATASTANDARD,
-    GenericAsset,
-    LocalFileAsset,
-    NWBAsset,
-    _add_data_standard,
-)
+if TYPE_CHECKING:
+    from .bases import GenericAsset, LocalFileAsset, NWBAsset
 from .zarr import ZarrAsset
 from ..consts import ZARR_MIME_TYPE, dandiset_metadata_file
 from ..metadata.core import add_common_metadata, prepare_metadata
EOF
@@ -7,7 +7,7 @@
import json
from pathlib import Path
from threading import Lock
from typing import Any
from typing import TYPE_CHECKING, Any
import weakref

from dandischema.models import (
@@ -20,13 +20,8 @@
import dandi
from dandi.bids_validator_deno import bids_validate

from .bases import (
_SCHEMA_BAREASSET_HAS_DATASTANDARD,
GenericAsset,
LocalFileAsset,
NWBAsset,
_add_data_standard,
)
if TYPE_CHECKING:
from .bases import GenericAsset, LocalFileAsset, NWBAsset
from .zarr import ZarrAsset
from ..consts import ZARR_MIME_TYPE, dandiset_metadata_file
from ..metadata.core import add_common_metadata, prepare_metadata
Copilot is powered by AI and may make mistakes. Always verify output.
Comment thread dandi/files/zarr.py
)

from .bases import LocalDirectoryAsset
from .bases import LocalDirectoryAsset, _add_data_standard

Check notice

Code scanning / CodeQL

Cyclic import Note

Import of module
dandi.files.bases
begins an import cycle.

Copilot Autofix

AI about 11 hours ago

To break the cycle without changing functionality, the best fix is to defer the import from .bases until runtime at the point of use, instead of importing it at module load time.

In this file (dandi/files/zarr.py), the problematic top-level line is:

  • from .bases import LocalDirectoryAsset, _add_data_standard (line 58 in the snippet)

A robust cycle-breaking pattern is:

  1. Remove LocalDirectoryAsset and _add_data_standard from top-level imports.
  2. Keep static typing intact by importing LocalDirectoryAsset only under TYPE_CHECKING.
  3. Add small helper functions that lazily import these symbols from .bases.
  4. Replace direct use sites:
    • use a string annotation ("LocalDirectoryAsset") or TYPE_CHECKING import for type hints
    • call helper(s) where _add_data_standard is needed.

This preserves behavior while avoiding import-time cycles.

Suggested changeset 1
dandi/files/zarr.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/dandi/files/zarr.py b/dandi/files/zarr.py
--- a/dandi/files/zarr.py
+++ b/dandi/files/zarr.py
@@ -20,6 +20,7 @@
 
 if TYPE_CHECKING:
     from ..upload import ZarrMode
+    from .bases import LocalDirectoryAsset
 
 from dandischema.models import BareAsset, DigestType, StandardsType, ome_ngff_standard
 from pydantic import BaseModel, ConfigDict, ValidationError
@@ -55,7 +56,6 @@
     pre_upload_size_check,
 )
 
-from .bases import LocalDirectoryAsset, _add_data_standard
 from ..validate._types import (
     ORIGIN_VALIDATION_DANDI_ZARR,
     MissingFileContent,
@@ -71,6 +71,18 @@
 lgr = get_logger()
 
 
+def _get_local_directory_asset_class():
+    from .bases import LocalDirectoryAsset
+
+    return LocalDirectoryAsset
+
+
+def _add_data_standard_lazy(*args: Any, **kwargs: Any) -> Any:
+    from .bases import _add_data_standard
+
+    return _add_data_standard(*args, **kwargs)
+
+
 class _Zarr3Metadata(BaseModel):
     """
     Metadata for Zarr format V3 stored in the zarr.json file
EOF
@@ -20,6 +20,7 @@

if TYPE_CHECKING:
from ..upload import ZarrMode
from .bases import LocalDirectoryAsset

from dandischema.models import BareAsset, DigestType, StandardsType, ome_ngff_standard
from pydantic import BaseModel, ConfigDict, ValidationError
@@ -55,7 +56,6 @@
pre_upload_size_check,
)

from .bases import LocalDirectoryAsset, _add_data_standard
from ..validate._types import (
ORIGIN_VALIDATION_DANDI_ZARR,
MissingFileContent,
@@ -71,6 +71,18 @@
lgr = get_logger()


def _get_local_directory_asset_class():
from .bases import LocalDirectoryAsset

return LocalDirectoryAsset


def _add_data_standard_lazy(*args: Any, **kwargs: Any) -> Any:
from .bases import _add_data_standard

return _add_data_standard(*args, **kwargs)


class _Zarr3Metadata(BaseModel):
"""
Metadata for Zarr format V3 stored in the zarr.json file
Copilot is powered by AI and may make mistakes. Always verify output.
Comment thread dandi/files/zarr.py
)

from .bases import LocalDirectoryAsset
from .bases import LocalDirectoryAsset, _add_data_standard

Check notice

Code scanning / CodeQL

Unused import Note

Import of 'LocalDirectoryAsset' is not used.

Copilot Autofix

AI about 11 hours ago

Remove the unused symbol from the import statement in dandi/files/zarr.py without changing runtime behavior.

Best fix:

  • In dandi/files/zarr.py, edit the import on line 58.
  • Change from .bases import LocalDirectoryAsset, _add_data_standard to from .bases import _add_data_standard.
  • No other code changes, methods, or new imports are needed.

This preserves existing functionality and keeps only the actually used import.

Suggested changeset 1
dandi/files/zarr.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/dandi/files/zarr.py b/dandi/files/zarr.py
--- a/dandi/files/zarr.py
+++ b/dandi/files/zarr.py
@@ -55,7 +55,7 @@
     pre_upload_size_check,
 )
 
-from .bases import LocalDirectoryAsset, _add_data_standard
+from .bases import _add_data_standard
 from ..validate._types import (
     ORIGIN_VALIDATION_DANDI_ZARR,
     MissingFileContent,
EOF
@@ -55,7 +55,7 @@
pre_upload_size_check,
)

from .bases import LocalDirectoryAsset, _add_data_standard
from .bases import _add_data_standard
from ..validate._types import (
ORIGIN_VALIDATION_DANDI_ZARR,
MissingFileContent,
Copilot is powered by AI and may make mistakes. Always verify output.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

minor Increment the minor version when merged

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants