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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,5 @@ docs/generated/
*.nxspe
*.tiff
*.tif
!tests/_regression_test_files/*.tiff
*.mtz
16 changes: 16 additions & 0 deletions docs/user-guide/io.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,22 @@
"display(img_stdev_and_mask_resolved_at_once)\n",
"img_stdev_and_mask_resolved_at_once['t', 0]['z', 0].plot(title='Masked Image, T=0, Z=0')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Backward Compatibility\n",
"\n",
"`Scitiff` IO modules should be backward-compatible.<br>\n",
"Any files **written by** `scitiff` can be **read by a newer version** of `scitiff`.\n",
"\n",
"On the other hand, `scitiff` may not be able to read an image <br>\n",
"if the image was **written by a newer version** of `scitiff`.<br>\n",
"i.e. a mandatory field became optional and was not included in the metadata.\n",
"\n",
"Please tell us if you experience any errors in reading a `scitiff` image with a newer version of `scitiff python package`."
]
}
],
"metadata": {
Expand Down
Binary file added tests/_regression_test_files/scitiff-25.12.0.tiff
Binary file not shown.
Binary file added tests/_regression_test_files/scitiff-25.3.0.tiff
Binary file not shown.
Binary file added tests/_regression_test_files/scitiff-25.4.0.tiff
Binary file not shown.
Binary file added tests/_regression_test_files/scitiff-25.5.0.tiff
Binary file not shown.
Binary file added tests/_regression_test_files/scitiff-25.6.0.tiff
Binary file not shown.
Binary file added tests/_regression_test_files/scitiff-25.7.0.tiff
Binary file not shown.
Binary file added tests/_regression_test_files/scitiff-26.1.0.tiff
Binary file not shown.
139 changes: 139 additions & 0 deletions tests/backward_compatibility_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# SPDX-License-Identifier: BSD-3-Clause
# Copyright (c) 2026 Scipp(ESS) contributors (https://github.com/scipp)
import json
import pathlib
from contextlib import contextmanager
from dataclasses import dataclass
from datetime import UTC, datetime

import pydantic
import pytest
import requests
from packaging.version import Version

from scitiff.io import load_scitiff

_LOWER_BOUND_VERSION = Version('25.1.0')
_SCITIFF_TEST_CACHE = pathlib.Path.home() / '.cache' / 'scitiff-test'
_SCITIFF_TEST_CACHE.mkdir(parents=True, exist_ok=True)
_CACHED_PACKAGE_INFO_PATH = _SCITIFF_TEST_CACHE / 'scitiff-package-info.json'


def _utc_now() -> datetime:
return datetime.now(tz=UTC)


class ScitiffPackageInfoCache(pydantic.BaseModel):
last_updated: datetime = pydantic.Field(default_factory=_utc_now)
versions: tuple[str, ...]

@property
def testing_versions(self) -> tuple[str, ...]:
return tuple(
_v.base_version
for _v in sorted(
Version(version)
for version in self.versions
if Version(version) >= _LOWER_BOUND_VERSION
)
)

@classmethod
def from_pypi(cls) -> 'ScitiffPackageInfoCache':
url = "https://pypi.org/pypi/scitiff/json"
response = requests.get(url, timeout=1)
data = response.json()
pacakge_info = ScitiffPackageInfoCache(versions=tuple(data['releases'].keys()))
# Save the info if possible
try:
_CACHED_PACKAGE_INFO_PATH.write_text(
data=pacakge_info.model_dump_json(indent=True)
)
except Exception as err:
import warnings

warnings.warn(
'Could not save scitiff package info into '
f'{_CACHED_PACKAGE_INFO_PATH.as_posix()}.\n'
f'An error raised: {err}\n'
'Skipping saving file... use `from_pypi` instead '
'if you do not need to save the info.',
RuntimeWarning,
stacklevel=3,
)

return pacakge_info

@classmethod
def maybe_from_cache(cls) -> 'ScitiffPackageInfoCache':
if _CACHED_PACKAGE_INFO_PATH.exists():
try:
latest = ScitiffPackageInfoCache(
**json.loads(_CACHED_PACKAGE_INFO_PATH.read_text())
)
except Exception:
...
else:
if (_utc_now() - latest.last_updated).seconds <= 300:
return latest

return cls.from_pypi()


SCITIFF_PACKAGE_INFO = ScitiffPackageInfoCache.maybe_from_cache()


@dataclass
class KnownError:
error_type: type
"""Type of error. i.e. RuntimeError."""
error_match: str
"""Match description of the error for pytest."""


_KNOWN_ERRORS: dict[str, tuple[KnownError]] = {} # No known errors yet


@contextmanager
def known_backward_compatibility_issues(_errors: tuple[KnownError, ...]):
if len(_errors) == 1:
with pytest.raises(_errors[0].error_type, match=_errors[0].error_match):
yield
elif len(_errors) >= 1:
with pytest.raises(_errors[0].error_type, match=_errors[0].error_match):
with known_backward_compatibility_issues(_errors[1:]):
yield
else:
yield


def _get_scitiff_example_file_path(version: str) -> pathlib.Path:
_test_files_dir = pathlib.Path(__file__).parent / '_regression_test_files'
return _test_files_dir / f'scitiff-{version}.tiff'


@pytest.mark.parametrize(
argnames=('scitiff_version'),
argvalues=SCITIFF_PACKAGE_INFO.testing_versions,
)
def test_example_files_for_all_releases_exist(scitiff_version) -> None:
cur_version_file = _get_scitiff_example_file_path(scitiff_version)
if not cur_version_file.exists():
raise RuntimeError(
f"Example file for version {scitiff_version} does not exist. "
"Use `tools/dump_scitiff_example.py` to create a new one "
"with the missing release version.\n"
"Creating an example file for a new release is not automated.\n"
"Therefore a developer will have to create a new scitiff file "
"and push it to the repo manually."
)


@pytest.mark.parametrize(
argnames=('scitiff_version'),
argvalues=SCITIFF_PACKAGE_INFO.testing_versions,
)
def test_loading_old_version_files(scitiff_version) -> None:
_known_erros = _KNOWN_ERRORS.get(scitiff_version, ())
with known_backward_compatibility_issues(_known_erros):
load_scitiff(_get_scitiff_example_file_path(scitiff_version))
107 changes: 107 additions & 0 deletions tools/dump_scitiff_example.py
Copy link
Member Author

Choose a reason for hiding this comment

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

This is a script that I used for dumping all example scitiff images manually.
I could use it for all released versions.

Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# SPDX-License-Identifier: BSD-3-Clause
# Copyright (c) 2026 Scipp(ESS) contributors (https://github.com/scipp)
import logging
import pathlib

import scipp as sc

from scitiff.data import hyperstack_example
from scitiff.io import save_scitiff


def _require_rich() -> None:
try:
import rich # noqa: F401 - just for checking
except ImportError as e:
raise ImportError(
"You need `rich` to run this script.\n"
"Please install `rich` or `scitiff` with `GUI` "
"optional dependencies.\n"
"Recommended command: pip install scitiff[gui]."
) from e


def _get_rich_logger() -> logging.Logger:
_require_rich()
from rich.logging import RichHandler

logger = logging.getLogger(__name__)
if not logger.handlers:
logger.addHandler(RichHandler())
logger.setLevel(logging.INFO)
return logger


def _get_scitiff_version() -> str:
from scitiff import __version__

if 'dev' in __version__ or __version__.startswith('0.'):
raise RuntimeError(
"Only release versions must be used for dumping an example image."
)
else:
return __version__


def _example_image_after_2610() -> sc.DataGroup:
from scitiff._schema import DAQMetadata
from scitiff.data import hyperstack_example_with_variances_and_mask

# Trimmed the example image
example_image = hyperstack_example_with_variances_and_mask()['x', :10]['y', :10]
daq_metadata = DAQMetadata(
facility='scitiff-dev',
instrument='computer',
detector_type='computer',
simulated=True,
)
extra = {
'string-value': 'string-value',
'int-value': 1,
'float-value': 1.2,
'scipp-scalar-number': sc.scalar(1, unit='count'),
'scipp-scalar-datetime': sc.datetime('now'),
}
return sc.DataGroup(image=example_image, daq=daq_metadata, extra=extra)


def _example_image(version: str) -> sc.DataArray | sc.DataGroup:
from packaging.version import Version

cur_version = Version(version)
if cur_version < Version('25.12.0'): # When saving mask and stdev was introduced.
return hyperstack_example()['x', :10]['y', :10]
elif cur_version < Version('26.1.0'): # When saving data group was introduced.
from scitiff.data import hyperstack_example_with_variances_and_mask

return hyperstack_example_with_variances_and_mask()['x', :10]['y', :10]
else:
return _example_image_after_2610()


def dump_example_scitiff():
"""Dump an example scitiff file with all possible metadata fields."""

logger = _get_rich_logger()
version = _get_scitiff_version()
default_dir = pathlib.Path(__file__).parent.parent / pathlib.Path(
'tests/_regression_test_files'
)
prefix = 'scitiff_'
suffix = '.tiff'
new_file_name = ''.join([prefix, version, suffix])
new_file_path = default_dir / new_file_name
logger.info("Dumping new example scitiff at %s", new_file_path.as_posix())
image = _example_image(version=version)
logger.info(image)
logger.info("Dumping image for version %s", version)
save_scitiff(dg=image, file_path=new_file_path)
logger.info(
"Successfully saved image for version %s in %s",
version,
new_file_path.as_posix(),
)


if __name__ == "__main__":
dump_example_scitiff()
Loading