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
2 changes: 1 addition & 1 deletion .python-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.10
3.12
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,9 @@ Run `just pre-commit-install` to install the hooks.

CLI functionality is tested using [pytest](https://docs.pytest.org/en/stable/).
Run `just test` to run the tests.

### Python version support

Some development tooling has tighter python version constraints than `oztest` itself.
Linting and CI covers the full range of `oztest` python support specified in `pyproject.toml`,
but developers should use the python version found in `.python-version`.
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ dependencies = [
[dependency-groups]
test = [
"pytest>=9.1.1",
"zarr>=3.3; python_version >= '3.12'",
]
dev = ["mypy>=2.1.0", "prek>=0.4.5", "ruff>=0.15.17", {include-group = "test"}]

Expand All @@ -29,5 +30,8 @@ build-backend = "setuptools.build_meta"
oztest = "src/oztest"
"oztest.cases" = "cases"

[tool.ruff]
target-version = "py310"

[tool.ruff.lint]
extend-select = ["I"]
64 changes: 55 additions & 9 deletions src/oztest/case_filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

logger = logging.getLogger(__name__)

ZARR_META_FILES = ("zarr.json", ".zattrs", ".zgroup", ".zarray")


def case_kinds(path: Path | None = None) -> Iterable[tuple[str, Any]]:
"""Iterate over case kinds and the associated Traversable."""
Expand Down Expand Up @@ -48,16 +50,40 @@ def case_kind_version_profile_validities(
yield (d.name, d)


def _iter_files_recursive(trav, prefix: str = "") -> Iterable[tuple[str, Any]]:
"""Recursively walk a Traversable, yielding (relative_path, Traversable) for every file found."""
def maybe_join(*args: str, sep="/"):
return sep.join(a for a in args if a)


def _case_kind_version_validity_names_inner(
trav, prefix: str = ""
) -> Iterable[tuple[str, Any]]:
"""Recursively walk a Traversable, yielding (relative_path, Traversable) for every test case found.

Test cases are either files or the top of a Zarr hierarchy.
"""
for item in trav.iterdir():
if item.name.startswith("__") or item.name.startswith("."):
continue
rel = f"{prefix}/{item.name}" if prefix else item.name
if item.is_dir():
yield from _iter_files_recursive(item, rel)

if is_case(item):
stub = item.name.split(".")[0]
yield (maybe_join(prefix, stub), item)
else:
yield (rel, item)
yield from _case_kind_version_validity_names_inner(
item, maybe_join(prefix, item.name)
)


def is_case(trav):
"""If the traversible (path) is a file or a directory containing a Zarr metadata file."""
if trav.is_file():
return True

for fname in ZARR_META_FILES:
if trav.joinpath(fname).is_file():
return True

return False


def case_kind_version_validity_names(
Expand All @@ -71,9 +97,7 @@ def case_kind_version_validity_names(
(e.g. `invalid/image/foo.json`) purely for organisational purposes;
that nesting is not itself a filterable attribute.
"""
for rel, item in _iter_files_recursive(case_kind_version_validity_trav):
name, _, ext = rel.rpartition(".")
yield (name if ext else rel, item)
yield from _case_kind_version_validity_names_inner(case_kind_version_validity_trav)


class OzVersion(Version):
Expand Down Expand Up @@ -272,6 +296,28 @@ def __init__(
self.builtin_cases = builtin_cases
self.verbose = verbose

@classmethod
def from_args(
cls,
kinds: list[str] | None = None,
version_spec: str | None = None,
include_profile: list[str] | None = None,
exclude_profile: list[str] | None = None,
include_validity: list[str] | None = None,
exclude_validity: list[str] | None = None,
include_name: list[str] | None = None,
exclude_name: list[str] | None = None,
):
return cls(
None,
True,
make_kind_filter(kinds),
make_version_filter(version_spec),
make_str_filter(include_profile, exclude_profile),
make_validity_filter(include_validity, exclude_validity),
make_str_filter(include_name, exclude_name),
)

def _iter_case_roots(self) -> Iterator[tuple[Case, bool]]:
if self.builtin_cases:
yield from self._iter_kinds()
Expand Down
44 changes: 44 additions & 0 deletions test/test_valid_storage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import json

import pytest

from oztest.case_filter import Case, CaseFilter


def parametrize_cases(filt: CaseFilter):
cases = sorted(c for c, _ in filt)
argnames = ("case",)
argvalues: list[tuple[Case]] = []
ids: list[str] = []
for c in cases:
argvalues.append((c,))
ids.append(c.slug())

def decorator(test_fn):
return pytest.mark.parametrize(argnames, argvalues, ids=ids)(test_fn)

return decorator


@parametrize_cases(CaseFilter.from_args(kinds=["parse_attributes"]))
def test_attributes_are_json(case: Case):
"""Test that the attribute tests are all valid JSON."""
with case.as_path() as p:
assert p.is_file()
text = p.read_text()
json.loads(text)


@parametrize_cases(
CaseFilter.from_args(kinds=["validate_zarr", "transform_coordinates"])
)
def test_zarr_tests_are_zarr(case: Case):
pytest.importorskip("zarr")
import zarr

with case.as_path() as p:
root = zarr.open(p, mode="r")

if isinstance(root, zarr.Group):
for _ in root.members(None):
pass
Loading