Skip to content

Add expression interpreter - #71

Open
ubdbra001 wants to merge 30 commits into
bids-standard:mainfrom
ubdbra001:i48-add-espression-interpreter
Open

Add expression interpreter#71
ubdbra001 wants to merge 30 commits into
bids-standard:mainfrom
ubdbra001:i48-add-espression-interpreter

Conversation

@ubdbra001

Copy link
Copy Markdown
Collaborator

Here's the initial attempt at adding all the parts of the expression language interpreter.

These successfully cover the full range of tests in the bidsschematools expression tests.
Two things that still need addressing:

  • exists passes the tests but needs more work to actually work as described in the schema tools docs
  • I'm not sure yet how to integrate them with the Context

@ubdbra001
ubdbra001 requested a review from effigies March 10, 2026 22:51
@ubdbra001 ubdbra001 self-assigned this Mar 10, 2026
@ubdbra001
ubdbra001 force-pushed the i48-add-espression-interpreter branch from 4229095 to 19cdfd0 Compare March 11, 2026 09:59
@ubdbra001
ubdbra001 force-pushed the i48-add-espression-interpreter branch from 19cdfd0 to 5bda559 Compare March 11, 2026 10:06
@codecov

codecov Bot commented Mar 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.34199% with 20 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.72%. Comparing base (5a361c4) to head (b5e5852).

Additional details and impacted files
@@            Coverage Diff             @@
##             main      #71      +/-   ##
==========================================
+ Coverage   90.56%   90.72%   +0.16%     
==========================================
  Files          13       15       +2     
  Lines         890     1121     +231     
  Branches      130      194      +64     
==========================================
+ Hits          806     1017     +211     
- Misses         50       61      +11     
- Partials       34       43       +9     
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

=== Do not change lines below ===
{
 "chain": [],
 "cmd": "uv lock",
 "exit": 0,
 "extra_inputs": [],
 "inputs": [
  "pyproject.toml",
  "uv.lock"
 ],
 "outputs": [
  "uv.lock"
 ],
 "pwd": "."
}
^^^ Do not change lines above ^^^
=== Do not change lines below ===
{
 "chain": [],
 "cmd": "uv lock",
 "exit": 0,
 "extra_inputs": [],
 "inputs": [
  "pyproject.toml",
  "uv.lock"
 ],
 "outputs": [
  "uv.lock"
 ],
 "pwd": "."
}
^^^ Do not change lines above ^^^
@effigies

effigies commented Mar 16, 2026

Copy link
Copy Markdown
Contributor

From today's call, here's a quick-and-dirty proxy to allow you to provide multiple namespaces and treat them as one:

class LookupProxy:
    def __init__(self, *objs):
        self.objs = objs

    def __getitem__(self, key):
        for obj in self.objs:
            if isinstance(obj, dict) and key in obj:
                return obj[key]
            if hasattr(obj, key):
                return getattr(obj, key)
        raise AttributeError

    # Provide either attr or item lookup
    __getattr__ = __getitem__

Example usage:

import bids_validator as bv
import bids_validator.context
from bids_validator import expression_language as el
from bids_validator.types.files import FileTree
from bidsschematools.schema import load_schema

schema = load_schema()

root = FileTree.read_from_filesystem('tests/data/bids-examples/ds000117/')
ds = bv.context.Dataset(root, schema)

file = root / 'sub-01' / 'ses-mri' / 'anat' / 'sub-01_ses-mri_acq-mprage_T1w.nii.gz'
context = bv.context.Context(file, ds, None)

namespace = LookupProxy(el.ids, context)

@ubdbra001

Copy link
Copy Markdown
Collaborator Author

The LookupProxy class suggested works as is, but I had to make a couple of tweaks to the new_evaluator function to get it working with both the expression tests and actual data.
I had a go with parsing and evaluating some rules from the schema, and, of the limited subset I tried, they all produced the correct output.

@ubdbra001

ubdbra001 commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator Author

Needs further work:

  • Implement exists properly
  • Add tests (improve coverage)
  • Add high-level interpret function that creates the proxy, parses the rule, and then evaluates it.

For future work:

  • Caching results in expression language
  • Return info for reporting failures (similar to how pytest reports e.g. Validation rule abc failed, expected x got y)

@ubdbra001

ubdbra001 commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator Author

Okay, I have created a function for exists that works for everything except "bids-uri".
I vaguely recall that you suggested that it would make most sense as a method for the Context class? Am I remembering correctly?

@effigies

Copy link
Copy Markdown
Contributor

I think as a method on the context is probably simplest. The alternative would be as a function that takes a context as an argument, and then the proxy setup uses functools.partial to partially apply it. I think that's probably more trouble than it's worth.

@ubdbra001

Copy link
Copy Markdown
Collaborator Author

Yep that works, and the evaluator can find and use it from the namespace produced by the LookupProxy.

@effigies effigies left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good. A few notes.

Comment thread src/bids_validator/expression_language.py Outdated
Comment thread src/bids_validator/context.py Outdated
Comment thread src/bids_validator/context.py Outdated
To match the TS version
@ubdbra001

Copy link
Copy Markdown
Collaborator Author

f5ed5cf is my initial attempt at handling bids-uris to an external dataset e.g. bids:raw:path/to/file.
It's probably a bit crude, but as far as I can tell it works (though the only example of this type of bids-uri I found was in the derivatives for the synthetic dataset, but I think the paths there are incorrect).

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an initial implementation of the BIDS schema expression language interpreter and extends Context with an exists(...) helper used by schema rules, along with tests and dependency updates to support expression parsing.

Changes:

  • Introduce src/bids_validator/expression_language.py implementing expression parsing + evaluation with a small built-in function/value namespace.
  • Add Context.exists(...) supporting dataset/stimuli/file/subject/bids-uri existence checks, with new tests in tests/test_context.py.
  • Update pyproject.toml to install bidsschematools with the expressions extra; add a demo dependency group.

Reviewed changes

Copilot reviewed 4 out of 5 changed files in this pull request and generated 9 comments.

File Description
tests/test_context.py Adds coverage for Context.exists(...) across multiple rule modes.
src/bids_validator/expression_language.py New expression interpreter (AST evaluation + built-in functions).
src/bids_validator/context.py Adds Context.exists(...) implementation including bids-uri handling.
pyproject.toml Enables bidsschematools expressions extra; adds demo dependencies.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +318 to +328
match var:
case bool():
return 'boolean'
case int() | float():
return 'number'
case list():
return 'array'
case dict():
return 'object'
case _:
return 'null'
Comment on lines +397 to +403
def __getitem__(self, key):
for obj in self.objs:
if isinstance(obj, dict) and key in obj:
return obj[key]
if hasattr(obj, key):
return getattr(obj, key)
raise AttributeError
Comment on lines +451 to +455
def interpret(rule: str, context: Context) -> Any:
"""Interpret a rule from the schema in a given file context"""
namespace = LookupProxy(el_namespace, context)
expr = bst_expr.parse(rule)
return evaluate(expr, namespace)
Comment thread src/bids_validator/expression_language.py Outdated
Comment thread src/bids_validator/expression_language.py
Comment thread src/bids_validator/expression_language.py Outdated
if isinstance(arg, (float, int)):
return arg

return max(filter_strs(arg))
if isinstance(arg, (float, int)):
return arg

return min(filter_strs(arg))
Comment thread src/bids_validator/context.py
Uses tests from schema.meta.expression_tests
@ubdbra001

Copy link
Copy Markdown
Collaborator Author

Test for the expression language has been added, but I'm not 100% happy about having to import the schema again. It was either this or looping through the expression tests in the test, but that treats it as a single test, and so doesn't give info on which expression has failed.

@ubdbra001
ubdbra001 force-pushed the i48-add-espression-interpreter branch from 9b3675a to 2aa437f Compare July 6, 2026 19:01
@ubdbra001
ubdbra001 marked this pull request as ready for review July 6, 2026 19:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants