Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
885f7a1
Refactor validator return type to support warnings via ValidationResu…
sejalpunwatkar May 27, 2026
8e46bd4
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 27, 2026
3ee3ea3
Merge branch 'dev' into feature/1479-validation-result
rly May 30, 2026
c1225d4
Update docstring and import for ValidationResult in common init
sejalpunwatkar Jun 2, 2026
39499cf
Resolve merge conflicts and clean up validator tests
sejalpunwatkar Jun 2, 2026
669261e
style: resolve ruff line length check on test imports
sejalpunwatkar Jun 2, 2026
88bb8e9
style: fix ruff F811 duplicate import redefinition error
sejalpunwatkar Jun 2, 2026
964acbf
Fix PR feedback, update ValidationWarning equality, and test structure
sejalpunwatkar Jun 17, 2026
6670324
Merge branch 'dev' into feature/1479-validation-result
sejalpunwatkar Jun 17, 2026
6b88ba9
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 17, 2026
c0b0464
Add data type attribute to test builder setup
sejalpunwatkar Jun 17, 2026
9eff3b4
Merge branch 'dev' into feature/1479-validation-result
rly Jul 1, 2026
f756b03
Merge branch 'dev' into feature/1479-validation-result
rly Jul 24, 2026
eeb2975
Apply suggestion from @rly
rly Jul 24, 2026
d687dd3
Update __init__.py
sejalpunwatkar Jul 26, 2026
bc7299a
refactor: extract ValidationIssue base class from Error/ValidationWar…
sejalpunwatkar Aug 2, 2026
b16d889
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 2, 2026
83f0b88
Merge branch 'dev' into feature/1479-validation-result
sejalpunwatkar Aug 2, 2026
6ded0be
Merge branch 'feature/1479-validation-result' of https://github.com/s…
sejalpunwatkar Aug 2, 2026
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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## HDMF 6.2.0 (Upcoming)

### Enhancements
- Refactored validator return type to `ValidationResult` to support upcoming validation warnings. @sejalpunwatkar [#1480](https://github.com/hdmf-dev/hdmf/pull/1480)

### Documentation and tutorial enhancements
- Expanded the "Read HERD" section of the external resources tutorial to show how to inspect a `HERD` after reading it back with `HERD.from_zip`, using `to_dataframe`, the individual interlinked tables, and `get_object_entities`. This addresses confusion about a read `HERD` appearing empty in its default Jupyter display. @rly [#1535](https://github.com/hdmf-dev/hdmf/pull/1535)

Expand Down Expand Up @@ -116,6 +119,7 @@
- Removed `test-min-deps` dependency group and replaced it with `uv pip install --resolution lowest-direct` in tox, making the project compatible with uv. @h-mayorquin [#1408](https://github.com/hdmf-dev/hdmf/pull/1408)
- Changed `get_data_shape` to check `shape` before `maxshape`, so that objects with both attributes (e.g., h5py datasets) return their actual shape rather than their maximum shape. @rly [#1180](https://github.com/hdmf-dev/hdmf/pull/1180)


### Removed
- Dropped support for Python 3.9. The minimum supported version is now Python 3.10. @rly [#xxx](https://github.com/hdmf-dev/hdmf/pull/xxx)
- Replaced `typing` library calls with Python 3.10+ built-in type syntax (`X | Y`, `X | None`, `type[X]`, `tuple[X]`, etc.). @rly [#xxx](https://github.com/hdmf-dev/hdmf/pull/xxx)
Expand Down
7 changes: 4 additions & 3 deletions src/hdmf/common/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from ..utils import docval, getargs, get_docval, AllowPositional # noqa: E402
from ..backends.io import HDMFIO # noqa: E402
from ..backends.hdf5 import HDF5IO # noqa: E402
from ..validate import ValidatorMap # noqa: E402
from ..validate import ValidatorMap, ValidationResult # noqa: E402
from ..build import BuildManager, TypeMap # noqa: E402
from ..container import _set_exp # noqa: E402

Expand Down Expand Up @@ -207,10 +207,11 @@ def get_manager(**kwargs):
'doc': 'the namespace to validate against', 'default': CORE_NAMESPACE},
{'name': 'experimental', 'type': bool,
'doc': 'data type is an experimental data type', 'default': False},
returns="errors in the file", rtype=list,
returns="errors and warnings in the file", rtype=ValidationResult,
Comment thread
sejalpunwatkar marked this conversation as resolved.
is_method=False)
def validate(**kwargs):
"""Validate an file against a namespace"""
"""Validate a file against a namespace."""

io, namespace, experimental = getargs('io', 'namespace', 'experimental', kwargs)
if experimental:
namespace = EXP_NAMESPACE
Expand Down
44 changes: 39 additions & 5 deletions src/hdmf/validate/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@
"MissingDataType",
"IllegalLinkError",
"IncorrectDataType",
"IncorrectQuantityError"
"IncorrectQuantityError",
"ValidationWarning",
"ValidationResult"
]


class Error:
class ValidationIssue:

@docval({'name': 'name', 'type': str, 'doc': 'the name of the component that is erroneous'},
{'name': 'reason', 'type': str, 'doc': 'the reason for the error'},
Expand Down Expand Up @@ -55,8 +57,11 @@ def __format_str(name, location, reason):
def __repr__(self):
return self.__str__()

def __eq__(self, other):
return type(self) is type(other) and hash(self) == hash(other)

def __hash__(self):
"""Returns the hash value of this Error
"""Returns the hash value of this validation issue

Note: if the location property is set after creation, the hash value will
change. Therefore, it is important to finalize the value of location
Expand All @@ -81,8 +86,37 @@ def __equatable_str(self):
equatable_name = self.name
return self.__format_str(equatable_name, self.location, self.reason)

def __eq__(self, other):
return hash(self) == hash(other)

class Error(ValidationIssue):
"""A validation error"""
pass


class ValidationWarning(ValidationIssue):
"""A validation warning"""
pass


class ValidationResult:

def __init__(self, errors = None, warnings = None):
self.errors = list(errors) if errors is not None else []
self.warnings = list(warnings) if warnings is not None else []

def __iter__(self):
return iter(self.errors)

def __len__(self):
return len(self.errors)

def __bool__(self):
return bool(self.errors)

def __getitem__(self, i):
return self.errors[i]
Comment thread
rly marked this conversation as resolved.

def __repr__(self):
return "ValidationResult(errors=%r, warnings=%r)" % (self.errors, self.warnings)


class DtypeError(Error):
Expand Down
7 changes: 4 additions & 3 deletions src/hdmf/validate/validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import numpy as np

from .errors import Error, DtypeError, MissingError, MissingDataType, ShapeError, IllegalLinkError, IncorrectDataType
from .errors import ExpectedArrayError, IncorrectQuantityError
from .errors import ExpectedArrayError, IncorrectQuantityError, ValidationResult
from ..build import GroupBuilder, DatasetBuilder, LinkBuilder, ReferenceBuilder
from ..build.builders import BaseBuilder
from ..spec import Spec, AttributeSpec, GroupSpec, DatasetSpec, RefSpec, LinkSpec
Expand Down Expand Up @@ -325,7 +325,7 @@ def get_validator(self, **kwargs):
raise ValueError(msg)

@docval({'name': 'builder', 'type': BaseBuilder, 'doc': 'the builder to validate'},
returns="a list of errors found", rtype=list)
returns="A ValidationResult containing the errors and warnings found", rtype=ValidationResult)
def validate(self, **kwargs):
"""Validate a builder against a Spec

Expand All @@ -338,7 +338,8 @@ def validate(self, **kwargs):
msg = "builder must have data type defined with attribute '%s'" % self.__type_key
raise ValueError(msg)
validator = self.get_validator(dt)
return validator.validate(builder)
errors_list = validator.validate(builder)
return ValidationResult(errors=errors_list, warnings=[])


class Validator(metaclass=ABCMeta):
Expand Down
54 changes: 52 additions & 2 deletions tests/unit/validator_tests/test_validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@
from hdmf.testing import TestCase, remove_test_file
from hdmf.validate import ValidatorMap
from hdmf.validate.errors import (DtypeError, MissingError, ExpectedArrayError, MissingDataType,
IncorrectQuantityError, IllegalLinkError, ShapeError, IncorrectDataType)
IncorrectQuantityError, IllegalLinkError, ShapeError, IncorrectDataType,
ValidationWarning, ValidationResult, Error)
from hdmf.backends.hdf5 import HDF5IO
from hdmf.utils import ZARR_INSTALLED, StrDataset
from hdmf.validate.errors import Error

CORE_NAMESPACE = 'test_core'

Expand Down Expand Up @@ -2043,3 +2043,53 @@ def test_isodatetime_no_time_component_fails(self):
# This confirms it fails because it lacks the 'T' and timezone
self.assertEqual(len(result), 1)
self.assertIsInstance(result[0], Error)


class TestValidationResultWrapper(TestCase):
"""Unit tests for the ValidationResult container and ValidationWarning class.

These tests verify that the ValidationResult wrapper correctly isolates
warnings while maintaining perfect backward compatibility by ensuring that
magic methods (__len__, __bool__, __iter__, __getitem__) reflect the errors
list only.
"""

def test_validation_result_basic_behavior(self):
err = Error(name="TestError", reason="Critical issue", location="root")
warn = ValidationWarning(name="TestWarning", reason="Minor issue", location="root")

result = ValidationResult(errors=[err], warnings=[warn])
self.assertEqual(result.errors, [err])
self.assertEqual(result.warnings, [warn])
self.assertEqual(len(result), 1)
self.assertTrue(bool(result))
self.assertEqual(result[0], err)
self.assertEqual(list(result), [err])

def test_validation_result_empty_behavior(self):
empty_result = ValidationResult()
assert len(empty_result) == 0
assert bool(empty_result) is False
assert empty_result.warnings == []

def test_validate_method_returns_empty_warnings(self):
"""Test that the validate method returns a ValidationResult with empty warnings for clean data."""

catalog = SpecCatalog()
catalog.register_spec(GroupSpec('A dummy spec', data_type_def='Dummy'), 'test.yaml')
namespace = SpecNamespace('test ns', 'test_ns', [{'source': 'test.yaml'}], version='0.1.0', catalog=catalog)
vmap = ValidatorMap(namespace)

builder = GroupBuilder('root', attributes={'data_type': 'Dummy'})

result = vmap.validate(builder)

assert isinstance(result, ValidationResult)
assert result.warnings == []

def test_error_and_warning_with_same_attributes_are_not_equal(self):
"""Test that an Error and a ValidationWarning with the same name, reason, and location are not equal. """
err = Error(name="TestIssue", reason="Same issue", location="root")
warn = ValidationWarning(name="TestIssue", reason="Same issue", location="root")

self.assertNotEqual(err, warn)
Loading