Skip to content

Commit 10d7de7

Browse files
committed
Merge remote-tracking branch 'upstream/maint/1.10.1'
2 parents e69e54a + 83f8940 commit 10d7de7

File tree

13 files changed

+242
-109
lines changed

13 files changed

+242
-109
lines changed

readthedocs.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ version: 2
33
build:
44
os: ubuntu-lts-latest
55
tools:
6-
python: latest
6+
python: '3.13'
77
jobs:
88
# The *create_environment steps replace RTD's virtual environment
99
# steps with uv, a much faster alternative to virtualenv+pip.

tools/schemacode/.readthedocs.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ version: 2
33
build:
44
os: ubuntu-lts-latest
55
tools:
6-
python: latest
6+
python: '3.13'
77
jobs:
88
create_environment:
99
- asdf plugin add uv https://github.com/asdf-community/asdf-uv.git

tools/schemacode/pyproject.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,3 +155,13 @@ select = [
155155

156156
[tool.ruff.lint.extend-per-file-ignores]
157157
"*/__init__.py" = ["F401"]
158+
159+
[dependency-groups]
160+
types = [
161+
"lxml>=6.0.2",
162+
"mypy>=1.18.2",
163+
"pandas-stubs>=2.2.2.240807",
164+
"types-jsonschema>=4.25.1.20251009",
165+
"types-pyyaml>=6.0.12.20250915",
166+
"types-tabulate>=0.9.0.20241207",
167+
]

tools/schemacode/src/bidsschematools/__init__.py

Lines changed: 3 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -11,43 +11,10 @@
1111

1212

1313
def __getattr__(attr: str) -> str:
14-
"""Lazily load the schema version and BIDS version from the filesystem."""
15-
from typing import TypeVar
14+
if attr in __all__:
15+
from . import _version
1616

17-
from .data import load
18-
19-
T = TypeVar("T")
20-
21-
def document(obj: T, docstring: str) -> T:
22-
tp = type(obj)
23-
return type(tp.__name__, (tp,), {"__doc__": docstring})(obj)
24-
25-
versions = {
26-
"__version__": (
27-
"schema/SCHEMA_VERSION",
28-
"schema_version",
29-
"Schema version",
30-
),
31-
"__bids_version__": (
32-
"schema/BIDS_VERSION",
33-
"bids_version",
34-
"BIDS specification version",
35-
),
36-
}
37-
38-
if attr in versions:
39-
dir_path, schema_path, docstring = versions[attr]
40-
41-
# Fast path if the schema directory is present (editable mode)
42-
if (version_file := load.readable(dir_path)).is_file():
43-
version = version_file.read_text().strip()
44-
else:
45-
# If version files are absent, the schema.json has been packaged.
46-
# If we're going to read it, we might as well cache it with load_schema().
47-
from .schema import load_schema
48-
49-
version = load_schema()[schema_path]
50-
globals()[attr] = document(version, docstring)
17+
globals()[attr] = getattr(_version, attr)
5118
return globals()[attr]
5219

5320
raise AttributeError(f"module {__spec__.name!r} has no attribute {attr!r}")

tools/schemacode/src/bidsschematools/__main__.py

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import re
55
import sys
66
from itertools import chain
7+
from pathlib import Path
78

89
import click
910

@@ -18,19 +19,19 @@
1819
@click.group()
1920
@click.option("-v", "--verbose", count=True)
2021
@click.option("-q", "--quiet", count=True)
21-
def cli(verbose, quiet):
22+
def cli(verbose: int, quiet: int) -> None:
2223
"""BIDS Schema Tools"""
2324
verbose = verbose - quiet
2425
configure_logger(get_logger(level=logging.WARNING - verbose * 10))
2526

2627

2728
@cli.command()
28-
@click.option("--schema")
29+
@click.option("--schema", "-s", "schema_path", type=click.Path(), help="Path to the BIDS schema")
2930
@click.option("--output", default="-")
3031
@click.pass_context
31-
def export(ctx, schema, output):
32+
def export(ctx: click.Context, schema_path: Path | None, output: Path | str) -> None:
3233
"""Export BIDS schema to JSON document"""
33-
schema = load_schema(schema)
34+
schema = load_schema(schema_path)
3435
text = schema.to_json()
3536
if output == "-":
3637
lgr.debug("Writing to stdout")
@@ -45,7 +46,7 @@ def export(ctx, schema, output):
4546
@cli.command()
4647
@click.option("--output", default="-")
4748
@click.pass_context
48-
def export_metaschema(ctx, output):
49+
def export_metaschema(ctx: click.Context, output: Path | str) -> None:
4950
"""Export BIDS schema to JSON document"""
5051
from .data import load
5152

@@ -59,7 +60,7 @@ def export_metaschema(ctx, output):
5960

6061

6162
@cli.command("pre-receive-hook")
62-
@click.option("--schema", "-s", type=click.Path(), help="Path to the BIDS schema")
63+
@click.option("--schema", "-s", "schema_path", type=click.Path(), help="Path to the BIDS schema")
6364
@click.option(
6465
"--input", "-i", "input_", default="-", type=click.Path(), help="Input file (default: stdin)"
6566
)
@@ -71,7 +72,7 @@ def export_metaschema(ctx, output):
7172
type=click.Path(),
7273
help="Output file (default: stdout)",
7374
)
74-
def pre_receive_hook(schema, input_, output):
75+
def pre_receive_hook(schema_path: Path | None, input_: Path | str, output: Path | str) -> None:
7576
"""Validate filenames from a list of files against the BIDS schema
7677
7778
The expected input takes the following form:
@@ -101,7 +102,7 @@ def pre_receive_hook(schema, input_, output):
101102
102103
This is intended to be used in a git pre-receive hook.
103104
"""
104-
schema = load_schema(schema)
105+
schema = load_schema(schema_path)
105106

106107
# Slurp inputs for now; we can think about streaming later
107108
if input_ == "-":
@@ -151,12 +152,12 @@ def pre_receive_hook(schema, input_, output):
151152

152153
regexes = [rule["regex"] for rule in all_rules]
153154

154-
output = sys.stdout if output == "-" else open(output, "w")
155+
out_stream = sys.stdout if output == "-" else open(output, "w")
155156

156157
rc = 0
157158
any_files = False
158159
valid_files = 0
159-
with output:
160+
with out_stream:
160161
for filename in stream:
161162
if not any_files:
162163
lgr.debug("Validating files, first file: %s", filename)
@@ -167,7 +168,7 @@ def pre_receive_hook(schema, input_, output):
167168
):
168169
continue
169170
if not any(re.match(regex, filename) for regex in regexes):
170-
print(filename, file=output)
171+
print(filename, file=out_stream)
171172
rc = 1
172173
else:
173174
valid_files += 1
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
"""Typing symbols for the bidsschematools package.
2+
3+
This module aims to make the types used by the bidsschematools package available for
4+
type checking without requiring the original packages to be imported at runtime.
5+
6+
The expected usage is::
7+
8+
from __future__ import annotations
9+
10+
import bidsschematools._lazytypes as lt
11+
12+
def func(arg: lt.Any) -> ReturnType:
13+
...
14+
15+
16+
If used outside of type annotations, for example, in an ``isinstance()`` check,
17+
the attribute will trigger an import.
18+
"""
19+
20+
import sys
21+
22+
__all__ = (
23+
# typing
24+
"Any",
25+
"Callable",
26+
"Literal",
27+
"NotRequired",
28+
"Protocol",
29+
"Self",
30+
"TYPE_CHECKING",
31+
"TypeVar",
32+
"TypedDict",
33+
# collections.abc
34+
"Iterator",
35+
"Mapping",
36+
# contextlib
37+
"AbstractContextManager",
38+
# third-party
39+
"FormatChecker",
40+
"JsonschemaValidator",
41+
"Traversable",
42+
)
43+
44+
_type_map = {
45+
"AbstractContextManager": ("contextlib", "AbstractContextManager"),
46+
"Iterator": ("collections.abc", "Iterator"),
47+
"Mapping": ("collections.abc", "Mapping"),
48+
"Traversable": ("acres.typ", "Traversable"),
49+
"FormatChecker": ("jsonschema", "FormatChecker"),
50+
"JsonschemaValidator": ("jsonschema.protocols", "JsonschemaValidator"),
51+
}
52+
53+
RUNTIME_IMPORT: bool
54+
55+
TYPE_CHECKING = False
56+
if TYPE_CHECKING or "sphinx.ext.autodoc" in sys.modules: # pragma: no cover
57+
from typing import Any, Callable, Literal, NotRequired, Protocol, Self, TypeVar, TypedDict
58+
59+
from collections.abc import Iterator, Mapping
60+
from contextlib import AbstractContextManager
61+
62+
from acres.typ import Traversable
63+
from jsonschema import FormatChecker
64+
from jsonschema.protocols import Validator as JsonschemaValidator
65+
66+
# Helpful TypeVars for generic classes and functions.
67+
# These should never be accessed at runtime, only for type checking, so exclude from __all__.
68+
T = TypeVar("T")
69+
T_co = TypeVar("T_co", covariant=True)
70+
T_contra = TypeVar("T_contra", contravariant=True)
71+
else: # pragma: no cover
72+
73+
def __getattr__(name: str):
74+
# Set the BIDSSCHEMATOOLS_DISABLE_RUNTIME_TYPES environment variable to
75+
# crash out if attributes are ever accessed at runtime.
76+
# Currently, we do not use this feature.
77+
global RUNTIME_IMPORT
78+
try:
79+
throw = not RUNTIME_IMPORT
80+
except NameError:
81+
import os
82+
83+
RUNTIME_IMPORT = not os.getenv("BIDSSCHEMATOOLS_DISABLE_RUNTIME_TYPES", "")
84+
if name == "RUNTIME_IMPORT":
85+
return RUNTIME_IMPORT
86+
throw = not RUNTIME_IMPORT
87+
88+
if throw:
89+
raise AttributeError(
90+
f"Attribute {name!r} in module {__name__!r} should only be used for type checking."
91+
)
92+
93+
if name in __all__:
94+
if name in _type_map:
95+
mod, attr = _type_map[name]
96+
else:
97+
mod, attr = "typing", name
98+
globals()[name] = getattr(__import__(mod), attr)
99+
return globals()[name]
100+
101+
msg = f"Module {__name__!r} has no attribute {name!r}"
102+
raise AttributeError(msg)
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
"""Lazy version strings
2+
3+
.. autodata:: __version__
4+
.. autodata:: __bids_version__
5+
"""
6+
7+
from __future__ import annotations
8+
9+
from . import _lazytypes as lt
10+
11+
__version__: str
12+
__bids_version__: str
13+
14+
__all__ = ("__version__", "__bids_version__")
15+
16+
17+
def document(obj: lt.T, docstring: str) -> lt.T:
18+
tp = type(obj)
19+
return type(tp.__name__, (tp,), {"__doc__": docstring})(obj)
20+
21+
22+
def __getattr__(attr: str) -> str:
23+
"""Lazily load the schema version and BIDS version from the filesystem."""
24+
from .data import load
25+
26+
versions = {
27+
"__version__": (
28+
"schema/SCHEMA_VERSION",
29+
"schema_version",
30+
"Schema version",
31+
),
32+
"__bids_version__": (
33+
"schema/BIDS_VERSION",
34+
"bids_version",
35+
"BIDS specification version",
36+
),
37+
}
38+
39+
if attr in versions:
40+
dir_path, schema_path, docstring = versions[attr]
41+
42+
# Fast path if the schema directory is present (editable mode)
43+
if (version_file := load.readable(dir_path)).is_file():
44+
version = version_file.read_text().strip()
45+
else:
46+
# If version files are absent, the schema.json has been packaged.
47+
# If we're going to read it, we might as well cache it with load_schema().
48+
from .schema import load_schema
49+
50+
version = load_schema()[schema_path]
51+
globals()[attr] = document(version, docstring)
52+
return globals()[attr]
53+
54+
raise AttributeError(f"module {__spec__.name!r} has no attribute {attr!r}")

tools/schemacode/src/bidsschematools/schema.py

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,7 @@
1313
from . import data, utils
1414
from .types import Namespace
1515

16-
TYPE_CHECKING = False
17-
if TYPE_CHECKING:
18-
from typing import Any, Callable
19-
20-
from acres import typ as at
21-
from jsonschema.protocols import Validator as JsonschemaValidator
16+
from . import _lazytypes as lt
2217

2318
lgr = utils.get_logger()
2419

@@ -27,7 +22,7 @@ class BIDSSchemaError(Exception):
2722
"""Errors indicating invalid values in the schema itself"""
2823

2924

30-
def _get_schema_version(schema_dir: str | at.Traversable) -> str:
25+
def _get_schema_version(schema_dir: str | lt.Traversable) -> str:
3126
"""
3227
Determine schema version for given schema directory, based on file specification.
3328
"""
@@ -38,7 +33,7 @@ def _get_schema_version(schema_dir: str | at.Traversable) -> str:
3833
return schema_version_path.read_text().strip()
3934

4035

41-
def _get_bids_version(schema_dir: str | at.Traversable) -> str:
36+
def _get_bids_version(schema_dir: str | lt.Traversable) -> str:
4237
"""
4338
Determine BIDS version for given schema directory, with directory name, file specification,
4439
and string fallback.
@@ -58,7 +53,7 @@ def _get_bids_version(schema_dir: str | at.Traversable) -> str:
5853
return str(schema_dir)
5954

6055

61-
def _find(obj: object, predicate: Callable[[Any], bool]) -> Iterable[object]:
56+
def _find(obj: object, predicate: lt.Callable[[lt.Any], bool]) -> Iterable[object]:
6257
"""Find objects in an arbitrary object that satisfy a predicate.
6358
6459
Note that this does not cut branches, so every iterable sub-object
@@ -122,7 +117,7 @@ def _dereference(namespace: MutableMapping, base_schema: Namespace) -> None:
122117

123118

124119
@cache
125-
def get_schema_validator() -> JsonschemaValidator:
120+
def get_schema_validator() -> lt.JsonschemaValidator:
126121
"""Get the jsonschema validator for validating BIDS schemas."""
127122
metaschema = json.loads(data.load.readable("metaschema.json").read_text())
128123
return utils.jsonschema_validator(metaschema, check_format=True)
@@ -210,7 +205,7 @@ def flatten_enums(namespace: Namespace, inplace=True) -> Namespace:
210205

211206

212207
@lru_cache
213-
def load_schema(schema_path: at.Traversable | str | None = None) -> Namespace:
208+
def load_schema(schema_path: lt.Traversable | str | None = None) -> Namespace:
214209
"""Load the schema into a dict-like structure.
215210
216211
This function allows the schema, like BIDS itself, to be specified in

tools/schemacode/src/bidsschematools/tests/test_render_text.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,9 @@ def test_make_glossary(schema_obj, schema_dir):
6060
for line in glossary.split("\n"):
6161
if line.startswith('<a name="objects.'):
6262
# Are all objects objects?
63-
assert any([line.startswith(f'<a name="objects.{i}') for i in object_files])
63+
assert any(line.startswith(f'<a name="objects.{i}.') for i in object_files)
6464
# Are rules loaded incorrectly?
65-
assert not any([line.startswith(f'<a name="objects.{i}') for i in rules_only])
65+
assert not any(line.startswith(f'<a name="objects.{i}.') for i in rules_only)
6666

6767

6868
@pytest.mark.parametrize("placeholders", [True, False])

0 commit comments

Comments
 (0)