Skip to content

feat: Create validation context classes from schema.meta.context #2

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 28 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
42cab94
add minimal cli
ubdbra001 Feb 18, 2025
ba8ac5c
add types.files module from PR#2
ubdbra001 Feb 18, 2025
6c8bb72
add types.files tests
ubdbra001 Mar 14, 2025
b17abe1
update cli
ubdbra001 Mar 14, 2025
294e3ff
test: Add bids-examples submodule in tests/data
effigies Mar 14, 2025
b6e442a
chore(test): Update test discovery
effigies Mar 14, 2025
dd97c3c
chore: Set default doctest_optionflags
effigies Mar 14, 2025
4543e19
test: Fix check for missing submodule
effigies Mar 14, 2025
0af2d62
chore(ci): Fetch submodules for testing
effigies Mar 14, 2025
00f1936
fix: Satisfy older importlib.resources
effigies Mar 14, 2025
f8020d4
chore: Fix coverage config to work outside pytest-cov
effigies Mar 14, 2025
ef53577
chore(tox): Run coverage on CLI
effigies Mar 14, 2025
f418e31
Merge branch 'main' into issue_5/add_cli
effigies Mar 14, 2025
f502a29
feat: Create validation context classes from schema.meta.context
effigies May 27, 2024
84f1600
test(filetree): Add docstring, update style
effigies Jun 19, 2024
6f9b448
chore: Add attrs and httpx to dependencies
effigies May 27, 2024
d77790d
fix: Nested f-strings are not permitted in older Python
effigies May 28, 2024
d8a64ff
DOC: Add module docstrings
effigies May 28, 2024
037455d
PY39: Use explicit Union
effigies Jun 18, 2024
ca8129a
RF: Factor out typespec_to_type
effigies Jun 18, 2024
15c8be8
feat(bidsignore): Add initial bidsignore implementation
effigies Jun 19, 2024
6ec0a98
feat(test): Validate Ignore class functionality
effigies Jun 19, 2024
1977a90
feat(ignore): Add tree filtering function, record filtered files
effigies Jun 19, 2024
c7aa699
refactor(ignore): Use an explicit chain of ignores so each Ignore can…
effigies Jun 19, 2024
6fa3148
Update to schema 0.10.0+
effigies Jul 11, 2024
09f56bc
fix: Import Self from typing_extensions
effigies Aug 15, 2024
b445af2
fix: Get type names in a py<310 compatible manner
effigies Sep 1, 2024
985542d
Update src/bids_validator/context_generator.py
effigies Nov 11, 2024
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
4 changes: 4 additions & 0 deletions .github/workflows/build-test-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: true
- uses: hynek/build-and-inspect-python-package@v2
with:
# Use attestation only if the action is triggered inside the repo
Expand Down Expand Up @@ -65,6 +66,9 @@ jobs:

steps:
- uses: actions/checkout@v4
with:
submodules: true

- name: Set git name/email
run: |
git config --global user.email "[email protected]"
Expand Down
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "tests/data/bids-examples"]
path = tests/data/bids-examples
url = https://github.com/bids-standard/bids-examples
8 changes: 7 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ classifiers = [
requires-python = ">=3.9"
dependencies = [
"bidsschematools >=1.0",
"typing_extensions",
"attrs",
"httpx",
]

[project.optional-dependencies]
Expand Down Expand Up @@ -55,12 +58,14 @@ tag_prefix = ""
parentdir_prefix = ""

[tool.pytest.ini_options]
norecursedirs = ["data"]
doctest_optionflags = ["NORMALIZE_WHITESPACE", "IGNORE_EXCEPTION_DETAIL"]


[tool.coverage.run]
branch = true
parallel = true
source = ["bids_validator"]
source = ["bids_validator", "tests"]
omit = [
"setup.py",
"*/_version.py",
Expand All @@ -77,6 +82,7 @@ exclude_lines = [
"no cov",
"if __name__ == .__main__.:",
"if TYPE_CHECKING:",
"pytest.skip",
]

# Disable black
Expand Down
56 changes: 56 additions & 0 deletions src/bids_validator/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import typer

from bids_validator import BIDSValidator
from bids_validator.types.files import FileTree

validator = BIDSValidator()

app = typer.Typer()


def check_children(tree: FileTree):
"""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
BIDS compliant.

Parameters
----------
tree : FileTree
FileTree object to iterate over

"""
for child in tree.children.values():
if child.is_dir:
check_children(child)
else:
check_bids(child.relative_path)


def check_bids(path: str):
"""Check if the file path is BIDS compliant.

Parameters
----------
path : str
Path to check of compliance

"""
# The output of the FileTree.relative_path method always drops the initial for the path which
# makes it fail the validator.is_bids check. Not sure if it's a Windows specific thing.
# This line adds it back.
path = f'/{path}'

if not validator.is_bids(path):
print(f'{path} is not a valid bids filename')


@app.command()
def main(bids_path: str):
root_path = FileTree.read_from_filesystem(bids_path)

check_children(root_path)


if __name__ == '__main__':
app()
129 changes: 129 additions & 0 deletions src/bids_validator/bidsignore.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
"""Utilities for working with .bidsignore files."""

import os
import re
from functools import lru_cache
from typing import Protocol, Union

import attrs

from .types.files import FileTree


@lru_cache
def compile_pat(pattern: str) -> Union[re.Pattern, None]:
"""Compile .gitignore-style ignore lines to regular expressions."""
orig = pattern
# A line starting with # serves as a comment.
if pattern.startswith('#'):
return None

# An optional prefix "!" which negates the pattern;
invert = pattern.startswith('!')

# Put a backslash ("\") in front of the first hash for patterns that begin with a hash.
# Put a backslash ("\") in front of the first "!" for patterns that begin with a literal "!"
if pattern.startswith((r'\#', r'\!')):
pattern = pattern[1:] # Unescape

Check warning on line 27 in src/bids_validator/bidsignore.py

View check run for this annotation

Codecov / codecov/patch

src/bids_validator/bidsignore.py#L27

Added line #L27 was not covered by tests

# Trailing spaces are ignored unless they are quoted with backslash ("\").
pattern = re.sub(r'(?<!\\) +$', '', pattern)

# A blank line matches no files, so it can serve as a separator for readability.
if pattern == '':
return None

# If there is a separator at the beginning or middle (or both) of the pattern,
# then the pattern is relative to the [root]
relative_match = pattern == '/' or '/' in pattern[:-1]
# If there is a separator at the end of the pattern then the pattern will only match
# directories, otherwise the pattern can match both files and directories.
directory_match = pattern.endswith('/')

# This does not handle character ranges correctly except when they are also valid regex
parts = [
'.*'
if part == '**'
else part.replace('*', '[^/]*').replace('?', '[^/]').replace('.', r'\.')
for part in pattern.strip('/').split('/')
]

prefix = '^' if relative_match else '^(.*/|)'
postfix = r'/\Z' if directory_match else r'/?\Z'

# "**/" matches zero or more directories, so the separating slash needs to be optional
out_pattern = '/'.join(parts).replace('.*/', '.*/?')
out_pattern = f'{prefix}{out_pattern}{postfix}'

if invert:
raise ValueError(f'Inverted patterns not supported: {orig}')

Check warning on line 59 in src/bids_validator/bidsignore.py

View check run for this annotation

Codecov / codecov/patch

src/bids_validator/bidsignore.py#L59

Added line #L59 was not covered by tests
# out_pattern = f'(?!{out_pattern})'

return re.compile(out_pattern)


class HasMatch(Protocol): # noqa: D101
def match(self, relpath: str) -> bool: ... # noqa: D102


@attrs.define
class Ignore:
"""Collection of .gitignore-style patterns.

Tracks successfully matched files for reporting.
"""

patterns: list[str] = attrs.field(factory=list)
history: list[str] = attrs.field(factory=list, init=False)

@classmethod
def from_file(cls, pathlike: os.PathLike):
"""Load Ignore contents from file."""
with open(pathlike) as fobj:
return cls([line.rstrip('\n') for line in fobj])

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):
self.history.append(relpath)
return True
return False

Check warning on line 90 in src/bids_validator/bidsignore.py

View check run for this annotation

Codecov / codecov/patch

src/bids_validator/bidsignore.py#L90

Added line #L90 was not covered by tests


@attrs.define
class IgnoreMany:
"""Match against several ignore filters."""

ignores: list[Ignore] = attrs.field()

def match(self, relpath: str) -> bool:
"""Return true if any filters match the given file.

Will short-circuit, so ordering is significant for side-effects,
such as recording files ignored by a particular filter.
"""
return any(ignore.match(relpath) for ignore in self.ignores)

Check warning on line 105 in src/bids_validator/bidsignore.py

View check run for this annotation

Codecov / codecov/patch

src/bids_validator/bidsignore.py#L105

Added line #L105 was not covered by tests


def filter_file_tree(filetree: FileTree) -> FileTree:
"""Read .bidsignore and filter file tree."""
bidsignore = filetree.children.get('.bidsignore')

Check warning on line 110 in src/bids_validator/bidsignore.py

View check run for this annotation

Codecov / codecov/patch

src/bids_validator/bidsignore.py#L110

Added line #L110 was not covered by tests
if not bidsignore:
return filetree
ignore = IgnoreMany([Ignore.from_file(bidsignore), Ignore(['/.bidsignore'])])
return _filter(filetree, ignore)

Check warning on line 114 in src/bids_validator/bidsignore.py

View check run for this annotation

Codecov / codecov/patch

src/bids_validator/bidsignore.py#L112-L114

Added lines #L112 - L114 were not covered by tests


def _filter(filetree: FileTree, ignore: HasMatch) -> FileTree:
items = filetree.children.items()
children = {

Check warning on line 119 in src/bids_validator/bidsignore.py

View check run for this annotation

Codecov / codecov/patch

src/bids_validator/bidsignore.py#L118-L119

Added lines #L118 - L119 were not covered by tests
name: _filter(child, ignore)
for name, child in items
if not ignore.match(child.relative_path)
}

# XXX This check may not be worth the time. Profile this.
if any(children.get(name) is not child for name, child in items):
filetree = attrs.evolve(filetree, children=children)

Check warning on line 127 in src/bids_validator/bidsignore.py

View check run for this annotation

Codecov / codecov/patch

src/bids_validator/bidsignore.py#L127

Added line #L127 was not covered by tests

return filetree

Check warning on line 129 in src/bids_validator/bidsignore.py

View check run for this annotation

Codecov / codecov/patch

src/bids_validator/bidsignore.py#L129

Added line #L129 was not covered by tests
38 changes: 38 additions & 0 deletions src/bids_validator/context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""Validation context for schema-based BIDS validation."""

from .context_generator import get_schema, load_schema_into_namespace

schema = get_schema()
load_schema_into_namespace(schema['meta']['context'], globals(), 'Context')


__all__ = [ # noqa: F822
'Context',
'Schema',
'Dataset',
'DatasetDescription',
'Tree',
'Subjects',
'Subject',
'Sessions',
'Entities',
'Sidecar',
'Associations',
'Events',
'Aslcontext',
'M0scan',
'Magnitude',
'Magnitude1',
'Bval',
'Bvec',
'Channels',
'Coordsystem',
'Columns',
'Json',
'Gzip',
'NiftiHeader',
'DimInfo',
'XyztUnits',
'Ome',
'Tiff',
]
Loading
Loading