Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-json
- id: check-toml
- id: check-added-large-files
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.4.3
hooks:
- id: ruff
args: [ --fix ]
- id: ruff-format
- id: ruff
args: [ --select, ISC001, --fix ]
44 changes: 44 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,47 @@ exclude_lines = [
"if __name__ == .__main__.:",
"if TYPE_CHECKING:",
]

# Disable black
[tool.black]
exclude = ".*"

[tool.ruff]
line-length = 99
extend-exclude = ["_version.py"]

[tool.ruff.lint]
extend-select = [
"F",
"E",
"W",
"I",
"UP",
"YTT",
"S",
"BLE",
"B",
"A",
# "CPY",
"C4",
"DTZ",
"T10",
# "EM",
"EXE",
"ISC",
"ICN",
"PT",
"Q",
]
ignore = [
"ISC001",
]

[tool.ruff.lint.flake8-quotes]
inline-quotes = "single"

[tool.ruff.lint.extend-per-file-ignores]
"*/test_*.py" = ["S101"]

[tool.ruff.format]
quote-style = "single"
3 changes: 3 additions & 0 deletions src/bids_validator/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
"""BIDS validator common Python package."""

from .bids_validator import BIDSValidator

__all__ = ['BIDSValidator']

from . import _version

__version__ = _version.get_versions()['version']
7 changes: 5 additions & 2 deletions src/bids_validator/bids_validator.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Validation class for BIDS projects."""

import logging
import os
import re
Expand Down Expand Up @@ -139,8 +140,10 @@ def parse(cls, path):
path = path.replace(os.sep, '/')

if not path.startswith('/'):
raise ValueError("Path must be relative to root of a BIDS dataset,"
" and must include a leading forward slash `/`.")
raise ValueError(
'Path must be relative to root of a BIDS dataset,'
' and must include a leading forward slash `/`.'
)

for regex in cls.regexes:
match = re.match(regex, path[1:])
Expand Down
152 changes: 84 additions & 68 deletions src/bids_validator/test_bids_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,16 @@

import os

import pytest
import datalad.api
import pytest

from bids_validator import BIDSValidator

HOME = os.path.expanduser('~')

TEST_DATA_DICT = {
'eeg_matchingpennies': (
'https://gin.g-node.org/sappelhoff/eeg_matchingpennies'
),
}
'eeg_matchingpennies': ('https://gin.g-node.org/sappelhoff/eeg_matchingpennies'),
}

EXCLUDE_KEYWORDS = ['git', 'datalad', 'sourcedata', 'bidsignore']

Expand Down Expand Up @@ -61,94 +59,112 @@ def test_datasets(validator, fname):
assert validator.is_bids(fname)


@pytest.mark.parametrize('fname, matches', [
('/T1w.json', True),
('/dataset_description.json', True),
('/README', True),
('/CHANGES', True),
('/participants.tsv', True),
('/sub-01/anat/sub-01_T1w.nii.gz', False),
])
@pytest.mark.parametrize(
('fname', 'matches'),
[
('/T1w.json', True),
('/dataset_description.json', True),
('/README', True),
('/CHANGES', True),
('/participants.tsv', True),
('/sub-01/anat/sub-01_T1w.nii.gz', False),
],
)
def test_top_level(validator, fname, matches):
"""Test that is_top_level returns true for top-level files."""
assert validator.is_top_level(fname) is matches


@pytest.mark.parametrize('fname, matches', [
('/sourcedata/unstructured_data.nii.gz', True),
('/sourcedata/dicom_dir/xyz.dcm', True),
('/code/my_analysis/analysis.py', True),
('/derivatives/preproc/sub-01/anat/sub-01_desc-preproc_T1w.nii.gz', True),
('/stimuli/pic.jpg', True),
('/sub-01/anat/sub-01_T1w.nii.gz', False),
])
@pytest.mark.parametrize(
('fname', 'matches'),
[
('/sourcedata/unstructured_data.nii.gz', True),
('/sourcedata/dicom_dir/xyz.dcm', True),
('/code/my_analysis/analysis.py', True),
('/derivatives/preproc/sub-01/anat/sub-01_desc-preproc_T1w.nii.gz', True),
('/stimuli/pic.jpg', True),
('/sub-01/anat/sub-01_T1w.nii.gz', False),
],
)
def test_associated_data(validator, fname, matches):
"""Test that is_associated_data returns true for associated data."""
assert validator.is_associated_data(fname) is matches


@pytest.mark.parametrize('fname, matches', [
('/sub-01/ses-1/sub-01_ses-1_scans.tsv', True),
('/sub-01/ses-1/sub-01_ses-1_scans.json', True),
('/sub-01/sub-01_scans.tsv', True),
('/sub-01/sub-01_scans.json', True),
('/sub-01/ses-1/sub-01_ses-1_task-rest_bold.json', True),
('/sub-01/sub-01_task-rest_bold.json', True),
('/sub-01/ses-1/sub-01_ses-1_asl.json', True),
('/sub-01/sub-01_asl.json', True),
('/sub-01/ses-1/sub-01_ses-1_pet.json', True),
('/sub-01/sub-01_pet.json', True),
('/sub-01/ses-1/sub-01_ses-1_proc-test_channels.tsv', True),
('/sub-01/ses-1/sub-01_ses-1_channels.json', True),
('/sub-01/sub-01_proc-test_channels.tsv', True),
('/sub-01/sub-01_channels.json', True),
('/sub-01/ses-1/sub-01_ses-1_space-CapTrak_electrodes.tsv', True),
('/sub-01/ses-1/sub-01_ses-1_coordsystem.json', True),
('/sub-01/sub-01_space-CapTrak_electrodes.tsv', True),
('/sub-01/sub-01_coordsystem.json', True),
('/sub-01/ses-1/sub-01_ses-1_motion.json', True),
('/sub-01/sub-01_motion.json', True),
('/sub-01/ses-1/sub-01_ses-1_TEM.json', True),
('/sub-01/sub-01_TEM.json', True),
('/sub-01/ses-1/sub-01_ses-1_nirs.json', True),
('/sub-01/sub-01_nirs.json', True),
# Mismatch sessions
('/sub-01/sub-01_ses-1_scans.tsv', False),
('/sub-01/sub-01_ses-1_scans.json', False),
('/sub-01/ses-1/sub-01_ses-2_scans.tsv', False),
# File-level
('/sub-01/ses-1/func/sub-01_ses-1_task-rest_bold.nii.gz', False),
('/sub-01/anat/sub-01_T1w.nii.gz', False),
])
@pytest.mark.parametrize(
('fname', 'matches'),
[
('/sub-01/ses-1/sub-01_ses-1_scans.tsv', True),
('/sub-01/ses-1/sub-01_ses-1_scans.json', True),
('/sub-01/sub-01_scans.tsv', True),
('/sub-01/sub-01_scans.json', True),
('/sub-01/ses-1/sub-01_ses-1_task-rest_bold.json', True),
('/sub-01/sub-01_task-rest_bold.json', True),
('/sub-01/ses-1/sub-01_ses-1_asl.json', True),
('/sub-01/sub-01_asl.json', True),
('/sub-01/ses-1/sub-01_ses-1_pet.json', True),
('/sub-01/sub-01_pet.json', True),
('/sub-01/ses-1/sub-01_ses-1_proc-test_channels.tsv', True),
('/sub-01/ses-1/sub-01_ses-1_channels.json', True),
('/sub-01/sub-01_proc-test_channels.tsv', True),
('/sub-01/sub-01_channels.json', True),
('/sub-01/ses-1/sub-01_ses-1_space-CapTrak_electrodes.tsv', True),
('/sub-01/ses-1/sub-01_ses-1_coordsystem.json', True),
('/sub-01/sub-01_space-CapTrak_electrodes.tsv', True),
('/sub-01/sub-01_coordsystem.json', True),
('/sub-01/ses-1/sub-01_ses-1_motion.json', True),
('/sub-01/sub-01_motion.json', True),
('/sub-01/ses-1/sub-01_ses-1_TEM.json', True),
('/sub-01/sub-01_TEM.json', True),
('/sub-01/ses-1/sub-01_ses-1_nirs.json', True),
('/sub-01/sub-01_nirs.json', True),
# Mismatch sessions
('/sub-01/sub-01_ses-1_scans.tsv', False),
('/sub-01/sub-01_ses-1_scans.json', False),
('/sub-01/ses-1/sub-01_ses-2_scans.tsv', False),
# File-level
('/sub-01/ses-1/func/sub-01_ses-1_task-rest_bold.nii.gz', False),
('/sub-01/anat/sub-01_T1w.nii.gz', False),
],
)
def test_session_level(validator, fname, matches):
"""Test that is_session_level returns true for session level files."""
assert validator.is_session_level(fname) is matches


@pytest.mark.parametrize('fname, matches', [
('/sub-01/sub-01_sessions.tsv', True),
('/sub-01/sub-01_sessions.json', True),
('/sub-01/anat/sub-01_T1w.nii.gz', False),
])
@pytest.mark.parametrize(
('fname', 'matches'),
[
('/sub-01/sub-01_sessions.tsv', True),
('/sub-01/sub-01_sessions.json', True),
('/sub-01/anat/sub-01_T1w.nii.gz', False),
],
)
def test_subject_level(validator, fname, matches):
"""Test that is_subject_level returns true for subject level files."""
assert validator.is_subject_level(fname) is matches


@pytest.mark.parametrize('fname, matches', [
('/phenotype/measure.tsv', True),
('/phenotype/measure.json', True),
('/sub-01/anat/sub-01_T1w.nii.gz', False),
])
@pytest.mark.parametrize(
('fname', 'matches'),
[
('/phenotype/measure.tsv', True),
('/phenotype/measure.json', True),
('/sub-01/anat/sub-01_T1w.nii.gz', False),
],
)
def test_phenotpic(validator, fname, matches):
"""Test that is_phenotypic returns true for phenotypic files."""
assert validator.is_phenotypic(fname) is matches


@pytest.mark.parametrize('fname, matches', [
('/sub-01/ses-1/func/sub-01_ses-1_task-rest_bold.nii.gz', True),
('/sub-01/anat/sub-01_T1w.nii.gz', True),
])
@pytest.mark.parametrize(
('fname', 'matches'),
[
('/sub-01/ses-1/func/sub-01_ses-1_task-rest_bold.nii.gz', True),
('/sub-01/anat/sub-01_T1w.nii.gz', True),
],
)
def test_file_level(validator, fname, matches):
"""Test that is_file returns true for file level files."""
assert validator.is_file(fname) is matches