Skip to content

feat: Compile schema.json and bundle in Python package #2115

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

Merged
merged 5 commits into from
May 15, 2025
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
29 changes: 29 additions & 0 deletions tools/schemacode/pdm_build.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import sys

sys.path.insert(0, "src")

import bidsschematools.schema


def pdm_build_initialize(context):
context.ensure_build_dir()

# Retrieve package version from schema
schema = bidsschematools.schema.load_schema()
context.config.metadata["version"] = schema.schema_version

if context.target == "editable":
return

# src/ layout makes sdists different from wheels
base_dir = context.build_dir / ("src" if context.target == "sdist" else "")

# Write compiled schema to JSON
schema_json = base_dir / "bidsschematools/data/schema.json"
schema_json.parent.mkdir(parents=True, exist_ok=True)
schema_json.write_text(schema.to_json())


def pdm_build_update_files(context, files):
# Dereference symlinks
files.update({relpath: path.resolve() for relpath, path in files.items()})
17 changes: 2 additions & 15 deletions tools/schemacode/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[build-system]
requires = ["setuptools>=61.2"]
build-backend = "setuptools.build_meta"
requires = ["pdm-backend", "acres", "pyyaml"]
build-backend = "pdm.backend"

[project]
name = "bidsschematools"
Expand Down Expand Up @@ -62,19 +62,6 @@ bst = "bidsschematools.__main__:cli"
[project.urls]
Homepage = "https://github.com/bids-standard/bids-specification"

[tool.setuptools.package-data]
bidsschematools = [
"data/metaschema.json",
"data/schema.json",
"data/schema/BIDS_VERSION",
"data/schema/SCHEMA_VERSION",
"data/schema/**/*.yaml",
"tests/data/*",
]

[tool.setuptools.dynamic]
version = {file = ["src/bidsschematools/data/schema/SCHEMA_VERSION"]}

[tool.black]
line-length = 99
include = '\.pyi?$'
Expand Down
26 changes: 22 additions & 4 deletions tools/schemacode/src/bidsschematools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,31 @@ def document(obj: T, docstring: str) -> T:
return type(tp.__name__, (tp,), {"__doc__": docstring})(obj)

versions = {
"__version__": ("schema/SCHEMA_VERSION", "Schema version"),
"__bids_version__": ("schema/BIDS_VERSION", "BIDS specification version"),
"__version__": (
"schema/SCHEMA_VERSION",
"schema_version",
"Schema version",
),
"__bids_version__": (
"schema/BIDS_VERSION",
"bids_version",
"BIDS specification version",
),
}

if attr in versions:
resource, docstring = versions[attr]
globals()[attr] = document(load.readable(resource).read_text().strip(), docstring)
dir_path, schema_path, docstring = versions[attr]

# Fast path if the schema directory is present (editable mode)
if (version_file := load.readable(dir_path)).is_file():
version = version_file.read_text().strip()
else:
# If version files are absent, the schema.json has been packaged.
# If we're going to read it, we might as well cache it with load_schema().
from .schema import load_schema

version = load_schema()[schema_path]
globals()[attr] = document(version, docstring)
return globals()[attr]

raise AttributeError(f"module {__spec__.name!r} has no attribute {attr!r}")
4 changes: 2 additions & 2 deletions tools/schemacode/src/bidsschematools/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import click

from .rules import regexify_filename_rules
from .schema import export_schema, load_schema
from .schema import load_schema
from .utils import configure_logger, get_logger
from .validator import _bidsignore_check

Expand All @@ -31,7 +31,7 @@ def cli(verbose, quiet):
def export(ctx, schema, output):
"""Export BIDS schema to JSON document"""
schema = load_schema(schema)
text = export_schema(schema)
text = schema.to_json()
if output == "-":
lgr.debug("Writing to stdout")
print(text)
Expand Down
25 changes: 18 additions & 7 deletions tools/schemacode/src/bidsschematools/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
import json
import os
import re
import tempfile
from collections.abc import Iterable, Mapping
from copy import deepcopy
from functools import lru_cache
from pathlib import Path

from . import __bids_version__, __version__, data, utils
from . import data, utils
from .types import Namespace

lgr = utils.get_logger()
Expand Down Expand Up @@ -201,8 +201,19 @@ def load_schema(schema_path=None):
This function is cached, so it will only be called once per schema path.
"""
if schema_path is None:
schema_path = data.load.readable("schema")
# Default to bundled JSON, fall back to bundled YAML directory
schema_path = data.load.readable("schema.json")
if not schema_path.is_file():
schema_path = data.load.readable("schema")
lgr.info("No schema path specified, defaulting to the bundled schema, `%s`.", schema_path)
elif isinstance(schema_path, str):
schema_path = Path(schema_path)

# JSON file: just load it
if schema_path.is_file():
return Namespace.from_json(schema_path.read_text())

# YAML directory: load, dereference and set versions
schema = Namespace.from_directory(schema_path)
if not schema.objects:
raise ValueError(f"objects subdirectory path not found in {schema_path}")
Expand Down Expand Up @@ -231,9 +242,7 @@ def export_schema(schema):
json : str
The schema serialized as a JSON string.
"""
versioned = Namespace.build({"schema_version": __version__, "bids_version": __bids_version__})
versioned.update(schema)
return versioned.to_json()
return schema.to_json()


def filter_schema(schema, **kwargs):
Expand Down Expand Up @@ -307,10 +316,12 @@ def validate_schema(schema: Namespace):
try:
validate(instance=schema.to_dict(), schema=metaschema)
except ValidationError as e:
import tempfile

with tempfile.NamedTemporaryFile(
prefix="schema_error_", suffix=".txt", delete=False, mode="w+"
) as file:
file.write(str(e))
# ValidationError does not have an add_note method yet
# e.add_note(f"See {file.name} for full error log.")
raise e
raise e
4 changes: 2 additions & 2 deletions tools/schemacode/src/bidsschematools/types/namespace.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@
from collections.abc import ItemsView, KeysView, Mapping, MutableMapping, ValuesView
from pathlib import Path

import yaml


def _expand_dots(entry: ty.Tuple[str, ty.Any]) -> ty.Tuple[str, ty.Any]:
# Helper function for expand
Expand Down Expand Up @@ -277,6 +275,8 @@ def _read_yaml_dir(path: Path) -> dict:
if subpath.is_dir():
mapping[subpath.name] = _read_yaml_dir(subpath)
elif subpath.name.endswith("yaml"):
import yaml

mapping[subpath.stem] = yaml.safe_load(subpath.read_text())
return mapping

Expand Down