From 42cab948af73ffabeb803e7f7cb7988d1197c07e Mon Sep 17 00:00:00 2001 From: Dan Brady Date: Tue, 18 Feb 2025 09:35:02 +0000 Subject: [PATCH 01/27] add minimal cli checks individual filenames --- src/bids_validator/__main__.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 src/bids_validator/__main__.py diff --git a/src/bids_validator/__main__.py b/src/bids_validator/__main__.py new file mode 100644 index 0000000..7a389fd --- /dev/null +++ b/src/bids_validator/__main__.py @@ -0,0 +1,16 @@ + +from bids_validator import BIDSValidator +import typer + +validator = BIDSValidator() + +app = typer.Typer() + +@app.command() +def main(bids_path: str): + + if not validator.is_bids(bids_path): + print(f"{bids_path} is not a valid bids filename") + +if __name__ == '__main__': + app() \ No newline at end of file From ba8ac5c4e4cac6f9a3644d3dd06340bbcb61750e Mon Sep 17 00:00:00 2001 From: Dan Brady Date: Tue, 18 Feb 2025 09:37:12 +0000 Subject: [PATCH 02/27] add types.files module from PR#2 Adds FileTree class to walk through supplied directory --- src/bids_validator/types/__init__.py | 1 + src/bids_validator/types/files.py | 126 +++++++++++++++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 src/bids_validator/types/__init__.py create mode 100644 src/bids_validator/types/files.py diff --git a/src/bids_validator/types/__init__.py b/src/bids_validator/types/__init__.py new file mode 100644 index 0000000..d5e90e9 --- /dev/null +++ b/src/bids_validator/types/__init__.py @@ -0,0 +1 @@ +"""Modules for providing types.""" diff --git a/src/bids_validator/types/files.py b/src/bids_validator/types/files.py new file mode 100644 index 0000000..e4e8256 --- /dev/null +++ b/src/bids_validator/types/files.py @@ -0,0 +1,126 @@ +"""Types for working with file trees.""" + +import os +import posixpath +import stat +from functools import cached_property +from pathlib import Path +from typing import Dict, Union + +import attrs +from typing_extensions import Self # PY310 + +__all__ = ('FileTree',) + + +@attrs.define +class UserDirEntry: + """Partial reimplementation of :class:`os.DirEntry`. + + :class:`os.DirEntry` can't be instantiated from Python, but this can. + """ + + path: str = attrs.field(repr=False, converter=os.fspath) + name: str = attrs.field(init=False) + _stat: os.stat_result = attrs.field(init=False, repr=False, default=None) + _lstat: os.stat_result = attrs.field(init=False, repr=False, default=None) + + def __attrs_post_init__(self) -> None: + self.name = os.path.basename(self.path) + + def __fspath__(self) -> str: + return self.path + + def stat(self, *, follow_symlinks: bool = True) -> os.stat_result: + """Return stat_result object for the entry; cached per entry.""" + if follow_symlinks: + if self._stat is None: + self._stat = os.stat(self.path, follow_symlinks=True) + return self._stat + else: + if self._lstat is None: + self._lstat = os.stat(self.path, follow_symlinks=False) + return self._lstat + + def is_dir(self, *, follow_symlinks: bool = True) -> bool: + """Return True if the entry is a directory; cached per entry.""" + _stat = self.stat(follow_symlinks=follow_symlinks) + return stat.S_ISDIR(_stat.st_mode) + + def is_file(self, *, follow_symlinks: bool = True) -> bool: + """Return True if the entry is a file; cached per entry.""" + _stat = self.stat(follow_symlinks=follow_symlinks) + return stat.S_ISREG(_stat.st_mode) + + def is_symlink(self) -> bool: + """Return True if the entry is a symlink; cached per entry.""" + _stat = self.stat(follow_symlinks=False) + return stat.S_ISLNK(_stat.st_mode) + + +def as_direntry(obj: os.PathLike) -> Union[os.DirEntry, UserDirEntry]: + """Convert PathLike into DirEntry-like object.""" + if isinstance(obj, os.DirEntry): + return obj + return UserDirEntry(obj) + + +@attrs.define +class FileTree: + """Represent a FileTree with cached metadata.""" + + direntry: Union[os.DirEntry, UserDirEntry] = attrs.field(repr=False, converter=as_direntry) + parent: Union['FileTree', None] = attrs.field(repr=False, default=None) + is_dir: bool = attrs.field(default=False) + children: Dict[str, 'FileTree'] = attrs.field(repr=False, factory=dict) + name: str = attrs.field(init=False) + + def __attrs_post_init__(self): + self.name = self.direntry.name + self.children = { + name: attrs.evolve(child, parent=self) for name, child in self.children.items() + } + + @classmethod + def read_from_filesystem( + cls, + direntry: os.PathLike, + parent: Union['FileTree', None] = None, + ) -> Self: + """Read a FileTree from the filesystem. + + Uses :func:`os.scandir` to walk the directory tree. + """ + self = cls(direntry, parent=parent) + if self.direntry.is_dir(): + self.is_dir = True + self.children = { + entry.name: FileTree.read_from_filesystem(entry, parent=self) + for entry in os.scandir(self.direntry) + } + return self + + def __contains__(self, relpath: os.PathLike) -> bool: + parts = Path(relpath).parts + if len(parts) == 0: + return False + child = self.children.get(parts[0], False) + return child and (len(parts) == 1 or posixpath.join(*parts[1:]) in child) + + def __fspath__(self): + return self.direntry.path + + @cached_property + def relative_path(self) -> str: + """The path of the current FileTree, relative to the root. + + Follows parents up to the root and joins with POSIX separators (/). + Directories include trailing slashes for simpler matching. + """ + if self.parent is None: + return '' + + return posixpath.join( + self.parent.relative_path, + f'{self.name}/' if self.is_dir else self.name, + ) From 6c8bb72da31d45e751d81f601b567402a0e6fcf4 Mon Sep 17 00:00:00 2001 From: Dan Brady Date: Fri, 14 Mar 2025 10:54:00 +0000 Subject: [PATCH 03/27] add types.files tests --- tests/__init__.py | 1 + tests/conftest.py | 18 ++++++++++++++++++ tests/types/__init__.py | 1 + tests/types/test_files.py | 16 ++++++++++++++++ 4 files changed, 36 insertions(+) create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/types/__init__.py create mode 100644 tests/types/test_files.py diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..78fa1d5 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""bids_validator tests.""" diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..0cecb70 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,18 @@ +"""Pytest configuration.""" + +import importlib.resources +import os +from pathlib import Path + +import pytest + + +@pytest.fixture(scope='session') +def examples() -> Path: + """Get bids-examples from submodule, allow environment variable override.""" + ret = os.getenv('BIDS_EXAMPLES') + if not ret: + ret = importlib.resources.files(__package__) / 'data' / 'bids-examples' + if not ret.exists(): + pytest.skip('Missing examples') + return Path(ret) diff --git a/tests/types/__init__.py b/tests/types/__init__.py new file mode 100644 index 0000000..93961ce --- /dev/null +++ b/tests/types/__init__.py @@ -0,0 +1 @@ +"""Tests for bids_validator.types.""" diff --git a/tests/types/test_files.py b/tests/types/test_files.py new file mode 100644 index 0000000..6c76d9b --- /dev/null +++ b/tests/types/test_files.py @@ -0,0 +1,16 @@ +import attrs +from bids_validator.types.files import FileTree + + +def test_FileTree(examples): + """Test the FileTree class.""" + ds000117 = FileTree.read_from_filesystem(examples / 'ds000117') + assert 'sub-01/ses-mri/anat/sub-01_ses-mri_acq-mprage_T1w.nii.gz' in ds000117 + assert ds000117.children['sub-01'].parent is ds000117 + + # Verify that evolving FileTrees creates consistent structures + evolved = attrs.evolve(ds000117) + assert evolved.children['sub-01'].parent is not ds000117 + assert evolved.children['sub-01'].parent is evolved + assert evolved.children['sub-01'].children['ses-mri'].parent is not ds000117.children['sub-01'] + assert evolved.children['sub-01'].children['ses-mri'].parent is evolved.children['sub-01'] From b17abe16e2e75c81059d7087ca490c5161ab2795 Mon Sep 17 00:00:00 2001 From: Dan Brady Date: Fri, 14 Mar 2025 10:55:35 +0000 Subject: [PATCH 04/27] update cli --- src/bids_validator/__main__.py | 48 +++++++++++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 4 deletions(-) diff --git a/src/bids_validator/__main__.py b/src/bids_validator/__main__.py index 7a389fd..24b5fdd 100644 --- a/src/bids_validator/__main__.py +++ b/src/bids_validator/__main__.py @@ -1,16 +1,56 @@ +import typer from bids_validator import BIDSValidator -import typer +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 not validator.is_bids(bids_path): - print(f"{bids_path} is not a valid bids filename") if __name__ == '__main__': - app() \ No newline at end of file + app() From 294e3ff4d4733a8da6b3e5291165c7be9db37825 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Fri, 14 Mar 2025 07:53:43 -0400 Subject: [PATCH 05/27] test: Add bids-examples submodule in tests/data --- .gitmodules | 3 +++ tests/data/bids-examples | 1 + 2 files changed, 4 insertions(+) create mode 100644 .gitmodules create mode 160000 tests/data/bids-examples diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..28c8e93 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "tests/data/bids-examples"] + path = tests/data/bids-examples + url = https://github.com/bids-standard/bids-examples diff --git a/tests/data/bids-examples b/tests/data/bids-examples new file mode 160000 index 0000000..789cc7a --- /dev/null +++ b/tests/data/bids-examples @@ -0,0 +1 @@ +Subproject commit 789cc7abf83313cc98c961b2f1dfde42071972db From b6e442a06387857cb119437a2027955479520417 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Fri, 14 Mar 2025 07:54:26 -0400 Subject: [PATCH 06/27] chore(test): Update test discovery --- pyproject.toml | 3 +++ tox.ini | 9 ++------- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6764b15..e578362 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,6 +54,9 @@ versionfile_build = "bids_validator/_version.py" tag_prefix = "" parentdir_prefix = "" +[tool.pytest.ini_options] +norecursedirs = ["data"] + [tool.coverage.run] branch = true parallel = true diff --git a/tox.ini b/tox.ini index 116d894..79c027d 100644 --- a/tox.ini +++ b/tox.ini @@ -49,13 +49,8 @@ deps = min: bidsschematools ==0.11.0 pre: git+https://github.com/bids-standard/bids-specification.git\#subdirectory=tools/schemacode commands = - pytest --doctest-modules --cov bids_validator --cov-report xml --cov-report term \ - --junitxml=test-results.xml -v src/ {posargs} - -[testenv:long] -commands = - pytest --doctest-modules --cov fmripost_aroma --cov-report xml \ - --junitxml=test-results.xml -v tests/ {posargs} + python -m pytest --doctest-modules --cov tests --cov bids_validator --cov-report xml --cov-report term \ + --junitxml=test-results.xml {posargs} [testenv:docs] description = Build documentation site From dd97c3cfdbf7aae38d7de1a48f56dddfba6fda2d Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Fri, 14 Mar 2025 08:02:45 -0400 Subject: [PATCH 07/27] chore: Set default doctest_optionflags --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index e578362..5742a03 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,6 +56,8 @@ parentdir_prefix = "" [tool.pytest.ini_options] norecursedirs = ["data"] +doctest_optionflags = ["NORMALIZE_WHITESPACE", "IGNORE_EXCEPTION_DETAIL"] + [tool.coverage.run] branch = true From 4543e190dafc8a1a9f7e7ef87f4833ae8ac0f0ca Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Fri, 14 Mar 2025 08:06:54 -0400 Subject: [PATCH 08/27] test: Fix check for missing submodule --- tests/conftest.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 0cecb70..3310edc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,7 +12,7 @@ def examples() -> Path: """Get bids-examples from submodule, allow environment variable override.""" ret = os.getenv('BIDS_EXAMPLES') if not ret: - ret = importlib.resources.files(__package__) / 'data' / 'bids-examples' - if not ret.exists(): - pytest.skip('Missing examples') + ret = importlib.resources.files() / 'data' / 'bids-examples' + if not any(ret.iterdir()): + pytest.skip('bids-examples submodule is not checked out') return Path(ret) From 0af2d620d897a186fa882bd933d8a59efa489c25 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Fri, 14 Mar 2025 08:07:25 -0400 Subject: [PATCH 09/27] chore(ci): Fetch submodules for testing --- .github/workflows/build-test-deploy.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/build-test-deploy.yml b/.github/workflows/build-test-deploy.yml index 79fd4d4..6b78086 100644 --- a/.github/workflows/build-test-deploy.yml +++ b/.github/workflows/build-test-deploy.yml @@ -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 @@ -65,6 +66,9 @@ jobs: steps: - uses: actions/checkout@v4 + with: + submodules: true + - name: Set git name/email run: | git config --global user.email "bids.maintenance@gmail.com" From 00f19364937f8d60a9205659c83679b81c7ff53c Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Fri, 14 Mar 2025 08:21:43 -0400 Subject: [PATCH 10/27] fix: Satisfy older importlib.resources --- tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 3310edc..ab22e62 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,7 +12,7 @@ def examples() -> Path: """Get bids-examples from submodule, allow environment variable override.""" ret = os.getenv('BIDS_EXAMPLES') if not ret: - ret = importlib.resources.files() / 'data' / 'bids-examples' + ret = importlib.resources.files(__spec__.parent) / 'data' / 'bids-examples' if not any(ret.iterdir()): pytest.skip('bids-examples submodule is not checked out') return Path(ret) From f8020d47b1abc704fea3bd4938c0664f71108aff Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Fri, 14 Mar 2025 10:56:57 -0400 Subject: [PATCH 11/27] chore: Fix coverage config to work outside pytest-cov --- pyproject.toml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 5742a03..8e5dcbd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,15 +62,23 @@ doctest_optionflags = ["NORMALIZE_WHITESPACE", "IGNORE_EXCEPTION_DETAIL"] [tool.coverage.run] branch = true parallel = true +source = ["bids_validator", "tests"] omit = [ "*/_version.py", ] +[tool.coverage.paths] +source = [ + "src/bids_validator/", + "*/site-packages/bids_validator/", +] + [tool.coverage.report] exclude_lines = [ "no cov", "if __name__ == .__main__.:", "if TYPE_CHECKING:", + "pytest.skip", ] # Disable black From ef535779efb97986cc51e219e460d3483c0b95eb Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Fri, 14 Mar 2025 10:58:53 -0400 Subject: [PATCH 12/27] chore(tox): Run coverage on CLI --- tox.ini | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 79c027d..9f9612f 100644 --- a/tox.ini +++ b/tox.ini @@ -46,11 +46,15 @@ pass_env = CLICOLOR_FORCE extras = test deps = + typer min: bidsschematools ==0.11.0 pre: git+https://github.com/bids-standard/bids-specification.git\#subdirectory=tools/schemacode commands = - python -m pytest --doctest-modules --cov tests --cov bids_validator --cov-report xml --cov-report term \ - --junitxml=test-results.xml {posargs} + coverage erase + coverage run -p -m bids_validator tests/data/bids-examples/ds000117 + python -m pytest --doctest-modules --cov . --cov-append --cov-report term \ + --junitxml=test-results.xml {posargs} + coverage xml [testenv:docs] description = Build documentation site From f502a29f5e15352c42c70fa533d80b2cd87eeab0 Mon Sep 17 00:00:00 2001 From: Chris Markiewicz Date: Mon, 27 May 2024 18:54:20 -0400 Subject: [PATCH 13/27] feat: Create validation context classes from schema.meta.context --- src/bids_validator/context.py | 36 +++++ src/bids_validator/context_generator.py | 175 ++++++++++++++++++++++++ src/bids_validator/types/files.py | 4 +- tests/types/test_files.py | 1 + 4 files changed, 214 insertions(+), 2 deletions(-) create mode 100644 src/bids_validator/context.py create mode 100644 src/bids_validator/context_generator.py diff --git a/src/bids_validator/context.py b/src/bids_validator/context.py new file mode 100644 index 0000000..e07ca07 --- /dev/null +++ b/src/bids_validator/context.py @@ -0,0 +1,36 @@ +from .context_generator import get_schema, load_schema_into_namespace + +schema = get_schema() +load_schema_into_namespace(schema['meta']['context']['context'], globals(), 'Context') + + +__all__ = [ # noqa: F822 + 'Context', + 'Schema', + 'Dataset', + 'Dataset_description', + 'Tree', + 'Subjects', + 'Subject', + 'Sessions', + 'Entities', + 'Sidecar', + 'Associations', + 'Events', + 'Aslcontext', + 'M0scan', + 'Magnitude', + 'Magnitude1', + 'Bval', + 'Bvec', + 'Channels', + 'Coordsystem', + 'Columns', + 'Json', + 'Gzip', + 'Nifti_header', + 'Dim_info', + 'Xyzt_units', + 'Ome', + 'Tiff', +] diff --git a/src/bids_validator/context_generator.py b/src/bids_validator/context_generator.py new file mode 100644 index 0000000..738a878 --- /dev/null +++ b/src/bids_validator/context_generator.py @@ -0,0 +1,175 @@ +from typing import Any + +import attrs +import bidsschematools as bst +import bidsschematools.schema +import httpx + +LATEST_SCHEMA_URL = 'https://bids-specification.readthedocs.io/en/latest/schema.json' +STABLE_SCHEMA_URL = 'https://bids-specification.readthedocs.io/en/stable/schema.json' + + +def get_schema(url: str | None = None) -> dict[str, Any]: + """Load a BIDS schema from a URL or return the bundled schema if no URL is provided. + + Parameters + ---------- + url : str | None + The URL to load the schema from. If None, the bundled schema is returned. + The strings 'latest' and 'stable' are also accepted as shortcuts. + + Returns + ------- + Dict[str, Any] + The loaded schema as a dictionary. + + """ + if url is None: + return bst.schema.load_schema() + + if url == 'latest': + url = LATEST_SCHEMA_URL + elif url == 'stable': + url = STABLE_SCHEMA_URL + + with httpx.Client() as client: + return client.get(url).json() + + +def create_attrs_class( + class_name: str, description: str | None, properties: dict[str, Any] +) -> type: + """Dynamically create an attrs class with the given properties. + + Parameters + ---------- + class_name : str + The name of the class to create. + description : str | None + A short description of the class, included in the docstring. + properties : Dict[str, Any] + A dictionary of property names and their corresponding schema information. + If a nested object is encountered, a nested class is created. + + Returns + ------- + cls : type + The dynamically created attrs class. + + """ + attributes = {} + for prop_name, prop_info in properties.items(): + prop_type = prop_info.get('type') + + if prop_type == 'object': + nested_class = create_attrs_class( + prop_name.capitalize(), + prop_info.get('description'), + prop_info.get('properties', {}), + ) + attributes[prop_name] = attrs.field(type=nested_class, default=None) + elif prop_type == 'array': + item_info = prop_info.get('items', {}) + item_type = item_info.get('type') + + if item_type == 'object': + nested_class = create_attrs_class( + prop_name.capitalize(), + item_info.get('description'), + item_info.get('properties', {}), + ) + attributes[prop_name] = attrs.field(type=list[nested_class], default=None) + else: + # Default to List[Any] for arrays of simple types + attributes[prop_name] = attrs.field(type=list[Any], default=None) + else: + # Default to Any for simple types + attributes[prop_name] = attrs.field(type=Any, default=None) + + return attrs.make_class( + class_name, + attributes, + class_body={ + '__doc__': f"""\ +{description} + +attrs data class auto-generated from BIDS schema + +Attributes +---------- +{''.join([f'{k}: {v.type.__name__}\n' for k, v in attributes.items()])} +""" + }, + ) + + +def generate_attrs_classes_from_schema( + schema: dict[str, Any], + root_class_name: str, +) -> type: + """Generate attrs classes from a JSON schema. + + Parameters + ---------- + schema : Dict[str, Any] + The JSON schema to generate classes from. Must contain a 'properties' field. + root_class_name : str + The name of the root class to create. + + Returns + ------- + cls : type + The root class created from the schema. + + """ + if 'properties' not in schema: + raise ValueError("Invalid schema: 'properties' field is required") + + return create_attrs_class( + root_class_name, + schema.get('description'), + schema['properties'], + ) + + +def populate_namespace(attrs_class: type, namespace: dict[str, Any]) -> None: + """Populate a namespace with nested attrs classes. + + Parameters + ---------- + attrs_class : type + The root attrs class to add to the namespace. + namespace : Dict[str, Any] + The namespace to populate with nested classes. + + """ + for attr in attrs_class.__attrs_attrs__: + attr_type = attr.type + + if isinstance(attr_type, type) and hasattr(attr_type, '__attrs_attrs__'): + namespace[attr_type.__name__] = attr_type + populate_namespace(attr_type, namespace) + + +def load_schema_into_namespace( + schema: dict[str, Any], + namespace: dict[str, Any], + root_class_name: str, +) -> None: + """Load a JSON schema into a namespace as attrs classes. + + Intended to be used with globals() or locals() to create classes in the current module. + + Parameters + ---------- + schema : Dict[str, Any] + The JSON schema to load into the namespace. + namespace : Dict[str, Any] + The namespace to load the schema into. + root_class_name : str + The name of the root class to create. + + """ + attrs_class = generate_attrs_classes_from_schema(schema, root_class_name) + namespace[root_class_name] = attrs_class + populate_namespace(attrs_class, namespace) diff --git a/src/bids_validator/types/files.py b/src/bids_validator/types/files.py index e4e8256..2b4d731 100644 --- a/src/bids_validator/types/files.py +++ b/src/bids_validator/types/files.py @@ -5,7 +5,7 @@ import stat from functools import cached_property from pathlib import Path -from typing import Dict, Union +from typing import Union import attrs from typing_extensions import Self # PY310 @@ -72,7 +72,7 @@ class FileTree: direntry: Union[os.DirEntry, UserDirEntry] = attrs.field(repr=False, converter=as_direntry) parent: Union['FileTree', None] = attrs.field(repr=False, default=None) is_dir: bool = attrs.field(default=False) - children: Dict[str, 'FileTree'] = attrs.field(repr=False, factory=dict) + children: dict[str, 'FileTree'] = attrs.field(repr=False, factory=dict) name: str = attrs.field(init=False) def __attrs_post_init__(self): diff --git a/tests/types/test_files.py b/tests/types/test_files.py index 6c76d9b..fc9d247 100644 --- a/tests/types/test_files.py +++ b/tests/types/test_files.py @@ -1,4 +1,5 @@ import attrs + from bids_validator.types.files import FileTree From 84f160072502bdf8f847ad74efaca36af469c8e9 Mon Sep 17 00:00:00 2001 From: Chris Markiewicz Date: Wed, 19 Jun 2024 20:51:08 +0900 Subject: [PATCH 14/27] test(filetree): Add docstring, update style --- tests/types/test_files.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/types/test_files.py b/tests/types/test_files.py index fc9d247..717ee74 100644 --- a/tests/types/test_files.py +++ b/tests/types/test_files.py @@ -1,3 +1,5 @@ +"""Tests for bids_validator.types.files.""" + import attrs from bids_validator.types.files import FileTree From 6f9b4489275c3f2950dffcec7c3376dc0be9dfde Mon Sep 17 00:00:00 2001 From: Chris Markiewicz Date: Mon, 27 May 2024 19:43:05 -0400 Subject: [PATCH 15/27] chore: Add attrs and httpx to dependencies --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 564a172..a869e85 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,8 @@ classifiers = [ requires-python = ">=3.9" dependencies = [ "bidsschematools >=1.0", + "attrs", + "httpx", ] [project.optional-dependencies] From d77790d0c45f2e35714263f6a5ba3f39e992199c Mon Sep 17 00:00:00 2001 From: Chris Markiewicz Date: Mon, 27 May 2024 20:25:56 -0400 Subject: [PATCH 16/27] fix: Nested f-strings are not permitted in older Python --- src/bids_validator/context_generator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bids_validator/context_generator.py b/src/bids_validator/context_generator.py index 738a878..15a43de 100644 --- a/src/bids_validator/context_generator.py +++ b/src/bids_validator/context_generator.py @@ -97,8 +97,8 @@ def create_attrs_class( Attributes ---------- -{''.join([f'{k}: {v.type.__name__}\n' for k, v in attributes.items()])} """ + + '\n'.join([f'{k}: {v.type.__name__}' for k, v in attributes.items()]), }, ) From d8a64fffe75d88f2a113c9b43ad5302f71bc7337 Mon Sep 17 00:00:00 2001 From: Chris Markiewicz Date: Mon, 27 May 2024 20:30:06 -0400 Subject: [PATCH 17/27] DOC: Add module docstrings --- src/bids_validator/context.py | 2 ++ src/bids_validator/context_generator.py | 15 +++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/src/bids_validator/context.py b/src/bids_validator/context.py index e07ca07..38db217 100644 --- a/src/bids_validator/context.py +++ b/src/bids_validator/context.py @@ -1,3 +1,5 @@ +"""Validation context for schema-based BIDS validation.""" + from .context_generator import get_schema, load_schema_into_namespace schema = get_schema() diff --git a/src/bids_validator/context_generator.py b/src/bids_validator/context_generator.py index 15a43de..3d89009 100644 --- a/src/bids_validator/context_generator.py +++ b/src/bids_validator/context_generator.py @@ -1,3 +1,18 @@ +"""Utilities for generating validation context classes from a BIDS schema. + +For default contexts based on the installed BIDS schema, use the `context` module. +These functions allow generating classes from alternative schemas. + +Basic usage: + +.. python:: + + from bids_validator.context_generator import get_schema, load_schema_into_namespace + + schema = get_schema('https://bids-specification.readthedocs.io/en/stable/schema.json') + load_schema_into_namespace(schema['meta']['context']['context'], globals(), 'Context') +""" + from typing import Any import attrs From 037455d92dc0032c7e50854fcd3196c787bfb8dd Mon Sep 17 00:00:00 2001 From: Chris Markiewicz Date: Tue, 18 Jun 2024 18:04:10 -0400 Subject: [PATCH 18/27] PY39: Use explicit Union --- src/bids_validator/context_generator.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/bids_validator/context_generator.py b/src/bids_validator/context_generator.py index 3d89009..a439923 100644 --- a/src/bids_validator/context_generator.py +++ b/src/bids_validator/context_generator.py @@ -13,7 +13,7 @@ load_schema_into_namespace(schema['meta']['context']['context'], globals(), 'Context') """ -from typing import Any +from typing import Any, Union import attrs import bidsschematools as bst @@ -24,7 +24,7 @@ STABLE_SCHEMA_URL = 'https://bids-specification.readthedocs.io/en/stable/schema.json' -def get_schema(url: str | None = None) -> dict[str, Any]: +def get_schema(url: Union[str, None] = None) -> dict[str, Any]: """Load a BIDS schema from a URL or return the bundled schema if no URL is provided. Parameters @@ -52,7 +52,7 @@ def get_schema(url: str | None = None) -> dict[str, Any]: def create_attrs_class( - class_name: str, description: str | None, properties: dict[str, Any] + class_name: str, description: Union[str, None], properties: dict[str, Any] ) -> type: """Dynamically create an attrs class with the given properties. From ca8129a1be2c44cff4a3bc5e6d74bebe11133318 Mon Sep 17 00:00:00 2001 From: Chris Markiewicz Date: Tue, 18 Jun 2024 19:01:51 -0400 Subject: [PATCH 19/27] RF: Factor out typespec_to_type --- src/bids_validator/context.py | 8 +- src/bids_validator/context_generator.py | 98 ++++++++++++++----------- 2 files changed, 59 insertions(+), 47 deletions(-) diff --git a/src/bids_validator/context.py b/src/bids_validator/context.py index 38db217..29d0294 100644 --- a/src/bids_validator/context.py +++ b/src/bids_validator/context.py @@ -10,7 +10,7 @@ 'Context', 'Schema', 'Dataset', - 'Dataset_description', + 'DatasetDescription', 'Tree', 'Subjects', 'Subject', @@ -30,9 +30,9 @@ 'Columns', 'Json', 'Gzip', - 'Nifti_header', - 'Dim_info', - 'Xyzt_units', + 'NiftiHeader', + 'DimInfo', + 'XyztUnits', 'Ome', 'Tiff', ] diff --git a/src/bids_validator/context_generator.py b/src/bids_validator/context_generator.py index a439923..61c7d0c 100644 --- a/src/bids_validator/context_generator.py +++ b/src/bids_validator/context_generator.py @@ -13,6 +13,7 @@ load_schema_into_namespace(schema['meta']['context']['context'], globals(), 'Context') """ +import json from typing import Any, Union import attrs @@ -35,7 +36,7 @@ def get_schema(url: Union[str, None] = None) -> dict[str, Any]: Returns ------- - Dict[str, Any] + dict[str, Any] The loaded schema as a dictionary. """ @@ -51,20 +52,56 @@ def get_schema(url: Union[str, None] = None) -> dict[str, Any]: return client.get(url).json() +def snake_to_pascal(val: str): + """Convert snake_case string to PascalCase.""" + return ''.join(sub.capitalize() for sub in val.split('_')) + + +def typespec_to_type(name: str, typespec: dict[str, Any]): + """Convert JSON-schema style specification to type and metadata dictionary.""" + tp = typespec.get('type') + if not tp: + raise ValueError(f'Invalid typespec: {json.dumps(typespec)}') + metadata = {key: typespec[key] for key in ('name', 'description') if key in typespec} + if tp == 'object': + properties = typespec.get('properties') + if properties: + type_ = create_attrs_class(name, properties=properties, metadata=metadata) + else: + type_ = dict[str, Any] + elif tp == 'array': + if 'items' in typespec: + subtype, md = typespec_to_type(name, typespec['items']) + else: + subtype = Any + type_ = list[subtype] + else: + type_ = { + 'number': float, + 'float': float, # Fix in schema + 'string': str, + 'integer': int, + 'int': int, # Fix in schema + }[tp] + return type_, metadata + + def create_attrs_class( - class_name: str, description: Union[str, None], properties: dict[str, Any] + class_name: str, + properties: dict[str, Any], + metadata: dict[str, Any], ) -> type: """Dynamically create an attrs class with the given properties. Parameters ---------- - class_name : str + class_name The name of the class to create. - description : str | None - A short description of the class, included in the docstring. - properties : Dict[str, Any] + properties A dictionary of property names and their corresponding schema information. If a nested object is encountered, a nested class is created. + metadata + A short description of the class, included in the docstring. Returns ------- @@ -74,39 +111,17 @@ def create_attrs_class( """ attributes = {} for prop_name, prop_info in properties.items(): - prop_type = prop_info.get('type') - - if prop_type == 'object': - nested_class = create_attrs_class( - prop_name.capitalize(), - prop_info.get('description'), - prop_info.get('properties', {}), - ) - attributes[prop_name] = attrs.field(type=nested_class, default=None) - elif prop_type == 'array': - item_info = prop_info.get('items', {}) - item_type = item_info.get('type') - - if item_type == 'object': - nested_class = create_attrs_class( - prop_name.capitalize(), - item_info.get('description'), - item_info.get('properties', {}), - ) - attributes[prop_name] = attrs.field(type=list[nested_class], default=None) - else: - # Default to List[Any] for arrays of simple types - attributes[prop_name] = attrs.field(type=list[Any], default=None) - else: - # Default to Any for simple types - attributes[prop_name] = attrs.field(type=Any, default=None) + type_, md = typespec_to_type(prop_name, prop_info) + attributes[prop_name] = attrs.field( + type=type_, repr=prop_name != 'schema', default=None, metadata=md + ) return attrs.make_class( - class_name, + snake_to_pascal(class_name), attributes, class_body={ '__doc__': f"""\ -{description} +{metadata.get('description', '')} attrs data class auto-generated from BIDS schema @@ -126,7 +141,7 @@ def generate_attrs_classes_from_schema( Parameters ---------- - schema : Dict[str, Any] + schema : dict[str, Any] The JSON schema to generate classes from. Must contain a 'properties' field. root_class_name : str The name of the root class to create. @@ -140,11 +155,8 @@ def generate_attrs_classes_from_schema( if 'properties' not in schema: raise ValueError("Invalid schema: 'properties' field is required") - return create_attrs_class( - root_class_name, - schema.get('description'), - schema['properties'], - ) + type_, _ = typespec_to_type(root_class_name, schema) + return type_ def populate_namespace(attrs_class: type, namespace: dict[str, Any]) -> None: @@ -154,7 +166,7 @@ def populate_namespace(attrs_class: type, namespace: dict[str, Any]) -> None: ---------- attrs_class : type The root attrs class to add to the namespace. - namespace : Dict[str, Any] + namespace : dict[str, Any] The namespace to populate with nested classes. """ @@ -177,9 +189,9 @@ def load_schema_into_namespace( Parameters ---------- - schema : Dict[str, Any] + schema : dict[str, Any] The JSON schema to load into the namespace. - namespace : Dict[str, Any] + namespace : dict[str, Any] The namespace to load the schema into. root_class_name : str The name of the root class to create. From 15c8be84b8d897802eeb8e3c7b950318bc4694a5 Mon Sep 17 00:00:00 2001 From: Chris Markiewicz Date: Wed, 19 Jun 2024 13:52:26 +0900 Subject: [PATCH 20/27] feat(bidsignore): Add initial bidsignore implementation --- src/bids_validator/bidsignore.py | 84 ++++++++++++++++++++++++++++++++ tests/test_bidsignore.py | 49 +++++++++++++++++++ 2 files changed, 133 insertions(+) create mode 100644 src/bids_validator/bidsignore.py create mode 100644 tests/test_bidsignore.py diff --git a/src/bids_validator/bidsignore.py b/src/bids_validator/bidsignore.py new file mode 100644 index 0000000..a2ed45d --- /dev/null +++ b/src/bids_validator/bidsignore.py @@ -0,0 +1,84 @@ +"""Utilities for working with .bidsignore files.""" + +import os +import re +from functools import lru_cache +from typing import Union + +import attrs + +from .types.files import FileTree + + +def filter_file_tree(filetree: FileTree) -> FileTree: + """Stub.""" + return 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 + + # Trailing spaces are ignored unless they are quoted with backslash ("\"). + pattern = re.sub(r'(? Date: Wed, 19 Jun 2024 14:15:30 +0900 Subject: [PATCH 21/27] feat(test): Validate Ignore class functionality --- tests/test_bidsignore.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/tests/test_bidsignore.py b/tests/test_bidsignore.py index 69d3f49..eaab519 100644 --- a/tests/test_bidsignore.py +++ b/tests/test_bidsignore.py @@ -2,7 +2,8 @@ import pytest -from bids_validator.bidsignore import compile_pat +from bids_validator.bidsignore import Ignore, compile_pat +from bids_validator.types.files import FileTree @pytest.mark.parametrize( @@ -47,3 +48,19 @@ def test_skipped_patterns(): assert compile_pat('') is None assert compile_pat('# commented line') is None assert compile_pat(' ') is None + + +def test_Ignore_ds000117(examples): + """Test that we can load a .bidsignore file and match a file.""" + ds000117 = FileTree.read_from_filesystem(examples / 'ds000117') + ignore = Ignore.from_file(ds000117.children['.bidsignore']) + assert 'run-*_echo-*_FLASH.json' in ignore.patterns + assert 'sub-01/ses-mri/anat/sub-01_ses-mri_run-1_echo-1_FLASH.nii.gz' in ds000117 + assert ignore.match('sub-01/ses-mri/anat/sub-01_ses-mri_run-1_echo-1_FLASH.nii.gz') + flash_file = ( + ds000117.children['sub-01'] + .children['ses-mri'] + .children['anat'] + .children['sub-01_ses-mri_run-1_echo-1_FLASH.nii.gz'] + ) + assert ignore.match(flash_file.relative_path) From 1977a90d9c19067f78be8e35dad9b966c242529a Mon Sep 17 00:00:00 2001 From: Chris Markiewicz Date: Wed, 19 Jun 2024 16:07:54 +0900 Subject: [PATCH 22/27] feat(ignore): Add tree filtering function, record filtered files --- src/bids_validator/bidsignore.py | 46 +++++++++++++++++++++++++------- 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/src/bids_validator/bidsignore.py b/src/bids_validator/bidsignore.py index a2ed45d..370d583 100644 --- a/src/bids_validator/bidsignore.py +++ b/src/bids_validator/bidsignore.py @@ -3,18 +3,13 @@ import os import re from functools import lru_cache -from typing import Union +from typing import Self, Union import attrs from .types.files import FileTree -def filter_file_tree(filetree: FileTree) -> FileTree: - """Stub.""" - return filetree - - @lru_cache def compile_pat(pattern: str) -> Union[re.Pattern, None]: """Compile .gitignore-style ignore lines to regular expressions.""" @@ -69,9 +64,13 @@ def compile_pat(pattern: str) -> Union[re.Pattern, None]: @attrs.define class Ignore: - """Collection of .gitignore-style patterns.""" + """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): @@ -79,6 +78,35 @@ def from_file(cls, pathlike: os.PathLike): with open(pathlike) as fobj: return cls([line.rstrip('\n') for line in fobj]) - def match(self, relpath: str): + def match(self, relpath: str) -> bool: """Match a relative path against a collection of ignore patterns.""" - return any(compile_pat(pattern).match(relpath) for pattern in self.patterns) + if any(compile_pat(pattern).match(relpath) for pattern in self.patterns): + self.history.append(relpath) + return True + return False + + def __add__(self, other) -> Self: + return self.__class__(patterns=self.patterns + other.patterns) + + +def filter_file_tree(filetree: FileTree) -> FileTree: + """Stub.""" + bidsignore = filetree.children.get('.bidsignore') + if not bidsignore: + return filetree + ignore = Ignore.from_file(bidsignore) + Ignore(['/.bidsignore']) + return _filter(filetree, ignore) + + +def _filter(filetree: FileTree, ignore: Ignore) -> FileTree: + items = filetree.children.items() + children = { + name: _filter(child, ignore) + for name, child in items + if not ignore.match(child.relative_path) + } + + if any(children.get(name) is not child for name, child in items): + filetree = attrs.evolve(filetree, children=children) + + return filetree From c7aa699da524935191a73335b4b96822a95057cc Mon Sep 17 00:00:00 2001 From: Chris Markiewicz Date: Wed, 19 Jun 2024 16:37:13 +0900 Subject: [PATCH 23/27] refactor(ignore): Use an explicit chain of ignores so each Ignore can be inspected for history --- src/bids_validator/bidsignore.py | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/src/bids_validator/bidsignore.py b/src/bids_validator/bidsignore.py index 370d583..68273ac 100644 --- a/src/bids_validator/bidsignore.py +++ b/src/bids_validator/bidsignore.py @@ -3,7 +3,7 @@ import os import re from functools import lru_cache -from typing import Self, Union +from typing import Protocol, Union import attrs @@ -62,6 +62,10 @@ def compile_pat(pattern: str) -> Union[re.Pattern, None]: 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. @@ -85,20 +89,32 @@ def match(self, relpath: str) -> bool: return True return False - def __add__(self, other) -> Self: - return self.__class__(patterns=self.patterns + other.patterns) + +@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) def filter_file_tree(filetree: FileTree) -> FileTree: - """Stub.""" + """Read .bidsignore and filter file tree.""" bidsignore = filetree.children.get('.bidsignore') if not bidsignore: return filetree - ignore = Ignore.from_file(bidsignore) + Ignore(['/.bidsignore']) + ignore = IgnoreMany([Ignore.from_file(bidsignore), Ignore(['/.bidsignore'])]) return _filter(filetree, ignore) -def _filter(filetree: FileTree, ignore: Ignore) -> FileTree: +def _filter(filetree: FileTree, ignore: HasMatch) -> FileTree: items = filetree.children.items() children = { name: _filter(child, ignore) @@ -106,6 +122,7 @@ def _filter(filetree: FileTree, ignore: Ignore) -> FileTree: 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) From 6fa3148fb4d88bff9dc7f5fc0ccdd72eee29ada9 Mon Sep 17 00:00:00 2001 From: Chris Markiewicz Date: Thu, 11 Jul 2024 15:24:03 -0400 Subject: [PATCH 24/27] Update to schema 0.10.0+ --- src/bids_validator/bidsignore.py | 2 +- src/bids_validator/context.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/bids_validator/bidsignore.py b/src/bids_validator/bidsignore.py index 68273ac..11fcf6e 100644 --- a/src/bids_validator/bidsignore.py +++ b/src/bids_validator/bidsignore.py @@ -94,7 +94,7 @@ def match(self, relpath: str) -> bool: class IgnoreMany: """Match against several ignore filters.""" - ignores: List[Ignore] = attrs.field() + ignores: list[Ignore] = attrs.field() def match(self, relpath: str) -> bool: """Return true if any filters match the given file. diff --git a/src/bids_validator/context.py b/src/bids_validator/context.py index 29d0294..0db70d3 100644 --- a/src/bids_validator/context.py +++ b/src/bids_validator/context.py @@ -3,7 +3,7 @@ from .context_generator import get_schema, load_schema_into_namespace schema = get_schema() -load_schema_into_namespace(schema['meta']['context']['context'], globals(), 'Context') +load_schema_into_namespace(schema['meta']['context'], globals(), 'Context') __all__ = [ # noqa: F822 From 09f56bcb80be46c211ab10d615585e76144eb4f7 Mon Sep 17 00:00:00 2001 From: Chris Markiewicz Date: Thu, 15 Aug 2024 10:59:33 -0400 Subject: [PATCH 25/27] fix: Import Self from typing_extensions --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index a869e85..affdf31 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,7 @@ classifiers = [ requires-python = ">=3.9" dependencies = [ "bidsschematools >=1.0", + "typing_extensions", "attrs", "httpx", ] From b445af2d1dbcd11b4ea059fb535ed987a0f31e8e Mon Sep 17 00:00:00 2001 From: Chris Markiewicz Date: Sat, 31 Aug 2024 22:02:31 -0400 Subject: [PATCH 26/27] fix: Get type names in a py<310 compatible manner --- src/bids_validator/context_generator.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/bids_validator/context_generator.py b/src/bids_validator/context_generator.py index 61c7d0c..65d5ff0 100644 --- a/src/bids_validator/context_generator.py +++ b/src/bids_validator/context_generator.py @@ -86,6 +86,13 @@ def typespec_to_type(name: str, typespec: dict[str, Any]): return type_, metadata +def _type_name(tp: type) -> str: + try: + return tp.__name__ + except AttributeError: + return str(tp) + + def create_attrs_class( class_name: str, properties: dict[str, Any], @@ -128,7 +135,12 @@ def create_attrs_class( Attributes ---------- """ - + '\n'.join([f'{k}: {v.type.__name__}' for k, v in attributes.items()]), + + '\n'.join( + [ + f'{k}: {_type_name(v.type)}\n\t{v.metadata["description"]}' + for k, v in attributes.items() + ] + ), }, ) From 985542d09ce2897862c467a785c4089b7ae6bda1 Mon Sep 17 00:00:00 2001 From: Chris Markiewicz Date: Mon, 11 Nov 2024 13:22:18 -0500 Subject: [PATCH 27/27] Update src/bids_validator/context_generator.py --- src/bids_validator/context_generator.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/bids_validator/context_generator.py b/src/bids_validator/context_generator.py index 65d5ff0..0438c40 100644 --- a/src/bids_validator/context_generator.py +++ b/src/bids_validator/context_generator.py @@ -78,10 +78,8 @@ def typespec_to_type(name: str, typespec: dict[str, Any]): else: type_ = { 'number': float, - 'float': float, # Fix in schema 'string': str, 'integer': int, - 'int': int, # Fix in schema }[tp] return type_, metadata