Skip to content
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
13 changes: 8 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,18 @@

## Enhancements

Comment thread
VisLab marked this conversation as resolved.
### Prerelease library schemas can now partner with prerelease standard schemas
### Prerelease schemas are now always included in version lookups

Loading a library schema with `withStandard` pointing to a prerelease version of the standard schema (e.g. `withStandard="8.5.0"`) would fail with a `BAD_WITH_STANDARD` error because the `withStandard` partner lookup was always restricted to released schemas, with no way to opt in to prerelease partner resolution.
Previously, loading a schema whose version existed only in the prerelease cache required passing `check_prerelease=True` through every layer of the loading API. This flag has been **removed** from all public functions (`load_schema_version`, `load_schema`, `from_string`, `from_dataframes`), all schema loader classes, and all internal helpers. Prerelease schemas are now found automatically whenever they are present in the cache.

**Changes:**

- `load_schema()` and `from_string()` in `hed_schema_io.py` now accept a `check_prerelease=False` parameter. When `True`, the `withStandard` partner schema is also searched in the prerelease cache.
- `SchemaLoader` (base class) and all subclasses (`SchemaLoaderXML`, `SchemaLoaderWiki`, `SchemaLoaderJSON`, `SchemaLoaderDF`) accept and forward `check_prerelease`.
- `check_schema_loading.py` (`hed_check_schema_loading` script and `run_loading_check()`) now automatically passes `check_prerelease=True` when loading schemas from a prerelease directory, so `test_all_prerelease_schemas` in `spec_tests` works correctly for library prereleases partnered with a prerelease standard.
- Removed the `check_prerelease` parameter from `load_schema_version()`, `load_schema()`, `from_string()`, and `from_dataframes()` in `hed_schema_io.py`.
- Removed the parameter from `SchemaLoader` (base class) and all subclasses (`SchemaLoaderXML`, `SchemaLoaderWiki`, `SchemaLoaderJSON`, `SchemaLoaderDF`).
- `get_hed_version_path()` in `hed_cache.py` now always searches both regular and prerelease directories (regular first).
- `get_hed_versions()` in `hed_cache.py` now defaults to `check_prerelease=True`. **This is a silent API change**: external callers that omitted `check_prerelease` will now receive prerelease versions. Internal callers that need released-only versions (compliance checker, `deprecatedFrom` validation, hedId comparison) explicitly pass `check_prerelease=False`.
- Default schema version is now resolved dynamically from the cache (highest released version) instead of being hardcoded, so new schema releases no longer require a code change.
- `check_schema_loading.py` simplified — removed `_is_prerelease_partner()` helper.
- `run_loading_check()` now raises `ValueError` immediately for mutually exclusive flag combinations (`prerelease_only` + `exclude_prereleases`, or `library_filter` + `standard_only`), consistent with the existing CLI-level validation.

# Release 0.9.0 January 22, 2026
Expand Down
27 changes: 12 additions & 15 deletions hed/schema/hed_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,15 +79,16 @@ def get_cache_directory(cache_folder=None) -> str:
return HED_CACHE_DIRECTORY

Comment thread
VisLab marked this conversation as resolved.

def get_hed_versions(local_hed_directory=None, library_name=None, check_prerelease=False) -> Union[list, dict]:
def get_hed_versions(local_hed_directory=None, library_name=None, check_prerelease=True) -> Union[list, dict]:
Comment thread
VisLab marked this conversation as resolved.
Comment thread
VisLab marked this conversation as resolved.
"""Get the HED versions in the HED directory.

Parameters:
Comment thread
VisLab marked this conversation as resolved.
local_hed_directory (str): Directory to check for versions which defaults to hed_cache.
library_name (str or None): An optional schema library name.
None retrieves the standard schema only.
Pass "all" to retrieve all standard and library schemas as a dict.
check_prerelease (bool): If True, results can include prerelease schemas
check_prerelease (bool): If True, results can include prerelease schemas.
Pass False to get only released versions (used by compliance checks).

Returns:
Union[list, dict]: List of version numbers or dictionary {library_name: [versions]}.
Expand All @@ -101,7 +102,7 @@ def get_hed_versions(local_hed_directory=None, library_name=None, check_prerelea

all_hed_versions = {}
local_directories = [local_hed_directory]
if check_prerelease and not local_hed_directory.endswith(prerelease_suffix):
if check_prerelease and Path(local_hed_directory).name != "prerelease":
local_directories.append(os.path.join(local_hed_directory, "prerelease"))

hed_files = []
Expand Down Expand Up @@ -134,25 +135,22 @@ def get_hed_versions(local_hed_directory=None, library_name=None, check_prerelea
return all_hed_versions


def get_hed_version_path(
xml_version, library_name=None, local_hed_directory=None, check_prerelease=False
) -> Union[str, None]:
def get_hed_version_path(xml_version, library_name=None, local_hed_directory=None) -> Union[str, None]:
"""Get HED XML file path in a directory. Only returns filenames that exist.

Parameters:
library_name (str or None): Optional the schema library name.
xml_version (str): Returns this version if it exists
library_name (str or None): Optional the schema library name.
local_hed_directory (str): Path to local HED directory. Defaults to HED_CACHE_DIRECTORY
check_prerelease (bool): Also check for prerelease schemas

Returns:
Union[str, None]: The path to the latest HED version the HED directory.
Union[str, None]: The path to the requested HED version the HED directory.

"""
if not local_hed_directory:
local_hed_directory = HED_CACHE_DIRECTORY

hed_versions = get_hed_versions(local_hed_directory, library_name, check_prerelease)
hed_versions = get_hed_versions(local_hed_directory, library_name)
if not hed_versions or not xml_version:
return None
if xml_version in hed_versions:
Expand All @@ -161,11 +159,10 @@ def get_hed_version_path(
if os.path.exists(regular_path):
return regular_path

# If check_prerelease is True, also check prerelease directory
if check_prerelease:
prerelease_path = _create_xml_filename(xml_version, library_name, local_hed_directory, True)
if os.path.exists(prerelease_path):
return prerelease_path
# Also check prerelease directory
prerelease_path = _create_xml_filename(xml_version, library_name, local_hed_directory, True)
if os.path.exists(prerelease_path):
return prerelease_path
return None


Expand Down
79 changes: 30 additions & 49 deletions hed/schema/hed_schema_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,7 @@
MAX_MEMORY_CACHE = 40


Comment thread
VisLab marked this conversation as resolved.
def load_schema_version(
xml_version=None, xml_folder=None, check_prerelease=False
) -> Union["HedSchema", "HedSchemaGroup"]:
def load_schema_version(xml_version=None, xml_folder=None) -> Union["HedSchema", "HedSchemaGroup"]:
"""Return a HedSchema or HedSchemaGroup extracted from xml_version

Parameters:
Expand All @@ -33,7 +31,6 @@ def load_schema_version(
based on the output of HedSchema.get_formatted_version
Basic format: `[schema_namespace:][library_name_]X.Y.Z`.
xml_folder (str): Path to a folder containing schema.
check_prerelease (bool): If True, check the prerelease directory for schemas.

Returns:
Union[HedSchema, HedSchemaGroup]: The schema or schema group extracted.
Expand All @@ -53,19 +50,18 @@ def load_schema_version(
if xml_version and isinstance(xml_version, list):
xml_versions = parse_version_list(xml_version)
schemas = [
_load_schema_version(xml_version=version, xml_folder=xml_folder, check_prerelease=check_prerelease)
for version in xml_versions.values()
_load_schema_version(xml_version=version, xml_folder=xml_folder) for version in xml_versions.values()
]
if len(schemas) == 1:
return schemas[0]

name = ",".join([schema.version for schema in schemas])
return HedSchemaGroup(schemas, name=name)
else:
return _load_schema_version(xml_version=xml_version, xml_folder=xml_folder, check_prerelease=check_prerelease)
return _load_schema_version(xml_version=xml_version, xml_folder=xml_folder)


def load_schema(hed_path, schema_namespace=None, schema=None, name=None, check_prerelease=False) -> "HedSchema":
def load_schema(hed_path, schema_namespace=None, schema=None, name=None) -> "HedSchema":
"""Load a schema from the given file or URL path.

Parameters:
Expand All @@ -77,7 +73,6 @@ def load_schema(hed_path, schema_namespace=None, schema=None, name=None, check_p
schema (HedSchema or None): A HED schema to merge this new file into
It must be a with-standard schema with the same value.
name (str or None): User supplied identifier for this schema
check_prerelease (bool): If True, allow the partnered standard schema (withStandard) to be a prerelease version.

Returns:
HedSchema: The loaded schema.
Expand All @@ -103,22 +98,21 @@ def load_schema(hed_path, schema_namespace=None, schema=None, name=None, check_p
file_as_string,
schema_format=os.path.splitext(hed_path.lower())[1],
name=name,
check_prerelease=check_prerelease,
)
elif hed_path.lower().endswith(".xml"):
hed_schema = SchemaLoaderXML.load(hed_path, schema=schema, name=name, check_prerelease=check_prerelease)
hed_schema = SchemaLoaderXML.load(hed_path, schema=schema, name=name)
elif hed_path.lower().endswith(".mediawiki"):
hed_schema = SchemaLoaderWiki.load(hed_path, schema=schema, name=name, check_prerelease=check_prerelease)
hed_schema = SchemaLoaderWiki.load(hed_path, schema=schema, name=name)
elif hed_path.lower().endswith(".json"):
hed_schema = SchemaLoaderJSON.load(hed_path, schema=schema, name=name, check_prerelease=check_prerelease)
hed_schema = SchemaLoaderJSON.load(hed_path, schema=schema, name=name)
elif hed_path.lower().endswith(".tsv") or os.path.isdir(hed_path):
if schema is not None:
raise HedFileError(
HedExceptions.INVALID_HED_FORMAT,
"Cannot pass a schema to merge into spreadsheet loading currently.",
filename=name,
)
hed_schema = SchemaLoaderDF.load_spreadsheet(filenames=hed_path, name=name, check_prerelease=check_prerelease)
hed_schema = SchemaLoaderDF.load_spreadsheet(filenames=hed_path, name=name)
else:
raise HedFileError(HedExceptions.INVALID_EXTENSION, "Unknown schema extension", filename=hed_path)

Expand All @@ -128,9 +122,7 @@ def load_schema(hed_path, schema_namespace=None, schema=None, name=None, check_p
return hed_schema


def from_string(
schema_string, schema_format=".xml", schema_namespace=None, schema=None, name=None, check_prerelease=False
) -> "HedSchema":
def from_string(schema_string, schema_format=".xml", schema_namespace=None, schema=None, name=None) -> "HedSchema":
"""Create a schema from the given string.

Parameters:
Expand All @@ -141,7 +133,6 @@ def from_string(
schema (HedSchema or None): A HED schema to merge this new file into
It must be a with-standard schema with the same value.
name (str or None): User supplied identifier for this schema
check_prerelease (bool): If True, allow the partnered standard schema (withStandard) to be a prerelease version.

Returns:
HedSchema: The loaded schema.
Expand All @@ -162,17 +153,11 @@ def from_string(
schema_string = schema_string.replace("\r\n", "\n")

if schema_format.endswith(".xml"):
hed_schema = SchemaLoaderXML.load(
schema_as_string=schema_string, schema=schema, name=name, check_prerelease=check_prerelease
)
hed_schema = SchemaLoaderXML.load(schema_as_string=schema_string, schema=schema, name=name)
elif schema_format.endswith(".mediawiki"):
hed_schema = SchemaLoaderWiki.load(
schema_as_string=schema_string, schema=schema, name=name, check_prerelease=check_prerelease
)
hed_schema = SchemaLoaderWiki.load(schema_as_string=schema_string, schema=schema, name=name)
elif schema_format.endswith(".json"):
hed_schema = SchemaLoaderJSON.load(
schema_as_string=schema_string, schema=schema, name=name, check_prerelease=check_prerelease
)
hed_schema = SchemaLoaderJSON.load(schema_as_string=schema_string, schema=schema, name=name)
else:
raise HedFileError(HedExceptions.INVALID_EXTENSION, f"Unknown schema extension {schema_format}", filename=name)

Expand All @@ -181,15 +166,14 @@ def from_string(
return hed_schema


def from_dataframes(schema_data, schema_namespace=None, name=None, check_prerelease=False) -> "HedSchema":
def from_dataframes(schema_data, schema_namespace=None, name=None) -> "HedSchema":
"""Create a schema from the given string.

Parameters:
schema_data (dict of str or None): A dict of DF_SUFFIXES:file_as_string_or_df
Should have an entry for all values of DF_SUFFIXES.
schema_namespace (str, None): The name_prefix all tags in this schema will accept.
name (str or None): User supplied identifier for this schema
check_prerelease (bool): If True, allow the partnered standard schema (withStandard) to be a prerelease version.

Returns:
HedSchema: The loaded schema.
Expand All @@ -207,9 +191,7 @@ def from_dataframes(schema_data, schema_namespace=None, name=None, check_prerele
HedExceptions.BAD_PARAMETERS, "Empty or non dict value passed to HedSchema.from_dataframes", filename=name
)

hed_schema = SchemaLoaderDF.load_spreadsheet(
schema_as_strings_or_df=schema_data, name=name, check_prerelease=check_prerelease
)
hed_schema = SchemaLoaderDF.load_spreadsheet(schema_as_strings_or_df=schema_data, name=name)

if schema_namespace:
hed_schema.set_schema_prefix(schema_namespace=schema_namespace)
Expand Down Expand Up @@ -274,7 +256,7 @@ def parse_version_list(xml_version_list) -> dict:


@functools.lru_cache(maxsize=MAX_MEMORY_CACHE)
def _load_schema_version(xml_version=None, xml_folder=None, check_prerelease=False):
def _load_schema_version(xml_version=None, xml_folder=None):
"""Return specified version

Parameters:
Expand All @@ -284,7 +266,6 @@ def _load_schema_version(xml_version=None, xml_folder=None, check_prerelease=Fal
The schema namespace must be the same and not repeated if loading multiple merged schemas.

xml_folder (str): Path to a folder containing schema.
check_prerelease (bool): If True, check the prerelease directory for schemas.

Returns:
Union[HedSchema, HedSchemaGroup]: The requested HedSchema object.
Expand All @@ -308,9 +289,7 @@ def _load_schema_version(xml_version=None, xml_folder=None, check_prerelease=Fal
else:
xml_versions = [""]

first_schema = _load_schema_version_sub(
xml_versions[0], schema_namespace, xml_folder=xml_folder, check_prerelease=check_prerelease, name=name
)
first_schema = _load_schema_version_sub(xml_versions[0], schema_namespace, xml_folder=xml_folder, name=name)
filenames = [os.path.basename(first_schema.filename)]

# Collect all duplicate issues for proper error reporting
Expand All @@ -321,7 +300,6 @@ def _load_schema_version(xml_version=None, xml_folder=None, check_prerelease=Fal
version,
schema_namespace,
xml_folder=xml_folder,
check_prerelease=check_prerelease,
schema=first_schema,
name=name,
)
Expand Down Expand Up @@ -357,16 +335,14 @@ def _load_schema_version(xml_version=None, xml_folder=None, check_prerelease=Fal
return first_schema


def _load_schema_version_sub(
xml_version, schema_namespace="", xml_folder=None, check_prerelease=False, schema=None, name=""
):
"""Return specified version(single version only for this one)
def _load_schema_version_sub(xml_version, schema_namespace="", xml_folder=None, schema=None, name=""):
"""Return specified version (single version only for this one).

Parameters:
xml_version (str): HED version format string. Expected format: '[library_name_]X.Y.Z'
xml_version (str): HED version format string. Expected format: '[library_name_]X.Y.Z'.
If empty, the latest released standard schema version from the cache is used.
schema_namespace (str): The prefix this will have
xml_folder (str): Path to a folder containing schema
check_prerelease (bool): If True, check the prerelease directory for schemas
schema (HedSchema or None): A HED schema to merge this new file into.
name (str): User supplied identifier for this schema

Expand All @@ -381,7 +357,15 @@ def _load_schema_version_sub(
- The prefix is invalid
"""
if not xml_version:
xml_version = "8.4.0"
versions = hed_cache.get_hed_versions(xml_folder, check_prerelease=False)
if isinstance(versions, list) and versions:
xml_version = versions[0]
else:
raise HedFileError(
HedExceptions.FILE_NOT_FOUND,
Comment thread
VisLab marked this conversation as resolved.
"No HED standard schema versions found in cache. Ensure schemas are installed or cached.",
"",
)

# Parse library name from version string before validation
library_name = ""
Expand All @@ -402,16 +386,13 @@ def _load_schema_version_sub(
version_to_validate,
library_name=library_name,
local_hed_directory=xml_folder,
check_prerelease=check_prerelease,
)

if hed_file_path:
hed_schema = load_schema(hed_file_path, schema_namespace=schema_namespace, schema=schema, name=name)
else:
library_string = f"for library '{library_name}'" if library_name else ""
known_versions = hed_cache.get_hed_versions(
xml_folder, library_name=library_name if library_name else "all", check_prerelease=check_prerelease
)
known_versions = hed_cache.get_hed_versions(xml_folder, library_name=library_name if library_name else "all")
raise HedFileError(
HedExceptions.FILE_NOT_FOUND,
f"HED version {library_string}: '{version_to_validate}' not found. Check {hed_cache.get_cache_directory(xml_folder)} for cache or https://github.com/hed-standard/hed-schemas/tree/main/library_schemas. "
Expand Down
Loading
Loading