Skip to content
Draft
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
2 changes: 1 addition & 1 deletion .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
check: ['style', 'spellcheck']
check: ['style', 'spellcheck', 'type']

steps:
- uses: actions/checkout@v5
Expand Down
26 changes: 26 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ classifiers = [
]
requires-python = ">=3.10"
dependencies = [
"acres >=0.5",
"attrs >=24.1",
"bidsschematools >=1.1",
"orjson>=3.11.3",
Expand Down Expand Up @@ -152,3 +153,28 @@ test = [
"datalad >=1.1",
"cattrs>=24.1.3",
]
types = [
"mypy>=1.18.2",
]

[tool.mypy]
strict = true
exclude = [
"^tests/data",
]

[[tool.mypy.overrides]]
module = 'bids_validator._version'
ignore_errors = true

[[tool.mypy.overrides]]
module = 'datalad.*'
ignore_missing_imports = true

[[tool.mypy.overrides]]
module = 'fsspec'
ignore_missing_imports = true

[[tool.mypy.overrides]]
module = 'upath'
ignore_missing_imports = true
12 changes: 6 additions & 6 deletions src/bids_validator/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
from typing import Annotated

from bidsschematools.schema import load_schema
from bidsschematools.types import Namespace
from bidsschematools.types.context import Subject
from bidsschematools.types.namespace import Namespace

from bids_validator import BIDSValidator
from bids_validator.context import Context, Dataset, Sessions
Expand All @@ -22,11 +22,11 @@
app = typer.Typer()


def is_subject_dir(tree):
def is_subject_dir(tree: FileTree) -> bool:
return tree.name.startswith('sub-')


def walk(tree: FileTree, dataset: Dataset, subject: Subject = None) -> Iterator[Context]:
def walk(tree: FileTree, dataset: Dataset, subject: Subject | None = None) -> Iterator[Context]:
"""Iterate over children of a FileTree and check if they are a directory or file.

If it's a directory then run again recursively, if it's a file file check the file name is
Expand All @@ -52,7 +52,7 @@ def walk(tree: FileTree, dataset: Dataset, subject: Subject = None) -> Iterator[
yield Context(child, dataset, subject)


def validate(tree: FileTree, schema: Namespace):
def validate(tree: FileTree, schema: Namespace) -> None:
"""Check if the file path is BIDS compliant.

Parameters
Expand All @@ -71,14 +71,14 @@ def validate(tree: FileTree, schema: Namespace):
print(f'{file.path} is not a valid bids filename')


def show_version():
def show_version() -> None:
"""Show bids-validator version."""
from . import __version__

print(f'bids-validator {__version__} (Python {sys.version.split()[0]})')


def version_callback(value: bool):
def version_callback(value: bool) -> None:
"""Run the callback for CLI version flag.

Parameters
Expand Down
42 changes: 28 additions & 14 deletions src/bids_validator/bids_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
import bidsschematools.utils
import bidsschematools.validator

from .types import _typings as t


class LoggingContext:
# From logging cookbook (CC0):
Expand All @@ -35,20 +37,31 @@ class LoggingContext:

"""

def __init__(self, logger, level=None, handler=None, close=True):
def __init__(
self,
logger: logging.Logger,
level: int | None = None,
handler: logging.Handler | None = None,
close: bool = True,
) -> None:
self.logger = logger
self.level = level
self.handler = handler
self.close = close

def __enter__(self):
def __enter__(self) -> None:
if self.level is not None:
self.old_level = self.logger.level
self.logger.setLevel(self.level)
if self.handler:
self.logger.addHandler(self.handler)

def __exit__(self, et, ev, tb):
def __exit__(
self,
et: type[BaseException] | None,
ev: BaseException | None,
tb: t.TracebackType,
) -> None:
if self.level is not None:
self.logger.setLevel(self.old_level)
if self.handler:
Expand All @@ -66,7 +79,7 @@ class BIDSValidator:

regexes = None

def __init__(self, index_associated=True):
def __init__(self, index_associated: bool = True) -> None:
"""Initialize BIDSValidator object.

Parameters
Expand All @@ -81,9 +94,9 @@ def __init__(self, index_associated=True):
self.index_associated = index_associated

@classmethod
def _init_regexes(cls):
def _init_regexes(cls) -> None:
if cls.regexes is None:
with LoggingContext(bst.utils.get_logger(), level=logging.WARNING):
with LoggingContext(bst.utils.get_logger(), level=logging.WARNING): # type: ignore[no-untyped-call]
schema = bst.schema.load_schema()

all_rules = chain.from_iterable(
Expand All @@ -93,7 +106,7 @@ def _init_regexes(cls):
cls.regexes = [rule['regex'] for rule in all_rules]

@classmethod
def parse(cls, path):
def parse(cls, path: str) -> dict[str, str]:
"""Parse a file path into a dictionary of BIDS entities.

Parameters
Expand Down Expand Up @@ -136,6 +149,7 @@ def parse(cls, path):
"""
if cls.regexes is None:
cls._init_regexes()
assert cls.regexes is not None # noqa: S101

if path.startswith(os.sep):
path = path.replace(os.sep, '/')
Expand All @@ -155,7 +169,7 @@ def parse(cls, path):

@classmethod
@lru_cache
def is_bids(cls, path):
def is_bids(cls, path: str) -> bool:
"""Check if file path adheres to BIDS.

Main method of the validator. Uses other class methods for checking
Expand Down Expand Up @@ -199,14 +213,14 @@ def is_bids(cls, path):
return False

@classmethod
def is_top_level(cls, path):
def is_top_level(cls, path: str) -> bool:
"""Check if the file has appropriate name for a top-level file."""
parts = cls.parse(path)
if not parts:
return False
return parts.get('subject') is None

def is_associated_data(self, path):
def is_associated_data(self, path: str) -> bool:
"""Check if file is appropriate associated data."""
if not self.index_associated:
return False
Expand All @@ -217,31 +231,31 @@ def is_associated_data(self, path):
return parts.get('path') in ('code', 'derivatives', 'stimuli', 'sourcedata')

@classmethod
def is_session_level(cls, path):
def is_session_level(cls, path: str) -> bool:
"""Check if the file has appropriate name for a session level."""
parts = cls.parse(path)
if not parts:
return False
return parts.get('datatype') is None and parts.get('suffix') != 'sessions'

@classmethod
def is_subject_level(cls, path):
def is_subject_level(cls, path: str) -> bool:
"""Check if the file has appropriate name for a subject level."""
parts = cls.parse(path)
if not parts:
return False
return parts.get('suffix') == 'sessions'

@classmethod
def is_phenotypic(cls, path):
def is_phenotypic(cls, path: str) -> bool:
"""Check if file is phenotypic data."""
parts = cls.parse(path)
if not parts:
return False
return parts.get('datatype') == 'phenotype'

@classmethod
def is_file(cls, path):
def is_file(cls, path: str) -> bool:
"""Check if file is a data file or non-inherited metadata file."""
parts = cls.parse(path)
if not parts:
Expand Down
2 changes: 1 addition & 1 deletion src/bids_validator/bidsignore.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def from_file(cls, pathlike: os.PathLike):

def match(self, relpath: str) -> bool:
"""Match a relative path against a collection of ignore patterns."""
if any(compile_pat(pattern).match(relpath) for pattern in self.patterns if pattern):
if any(regex.match(relpath) for pat in self.patterns if (regex := compile_pat(pat))):
self.history.append(relpath)
return True
return False
Expand Down
Loading
Loading