Skip to content

Commit

Permalink
Merge pull request #577 from bioimage-io/rich_md_output
Browse files Browse the repository at this point in the history
Rich md output for validation summaries
  • Loading branch information
FynnBe authored Mar 23, 2024
2 parents 9d8ef86 + 30423c9 commit 2435ca9
Show file tree
Hide file tree
Showing 5 changed files with 103 additions and 55 deletions.
48 changes: 40 additions & 8 deletions bioimageio/spec/_build_description.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from typing import Any, Callable, Mapping, Optional, Type, TypeVar, Union
from typing import Any, Callable, List, Mapping, Optional, Type, TypeVar, Union

from ._internal.common_nodes import InvalidDescr, ResourceDescrBase
from ._internal.io import BioimageioYamlContent
from ._internal.types import FormatVersionPlaceholder
from ._internal.validation_context import ValidationContext, validation_context_var
from .generic import GenericDescr
from .summary import ErrorEntry, ValidationDetail

ResourceDescrT = TypeVar("ResourceDescrT", bound=ResourceDescrBase)

Expand Down Expand Up @@ -50,16 +50,48 @@ def build_description_impl(
get_rd_class: Callable[[Any, Any], Type[ResourceDescrT]],
) -> Union[ResourceDescrT, InvalidDescr]:
context = context or validation_context_var.get()
if not isinstance(content, dict):
# "Invalid content of type '{type(content)}'"
rd_class = GenericDescr
errors: List[ErrorEntry] = []
if isinstance(content, dict):
for minimum in ("type", "format_version"):
if minimum not in content:
errors.append(
ErrorEntry(
loc=(minimum,), msg=f"Missing field '{minimum}'", type="error"
)
)
elif not isinstance(content[minimum], str):
errors.append(
ErrorEntry(
loc=(minimum,),
msg=f"Invalid type '{type(content[minimum])}'",
type="error",
)
)
else:
errors.append(
ErrorEntry(
loc=(), msg=f"Invalid content of type '{type(content)}'", type="error"
)
)
content = {}

typ = content.get("type")
rd_class = get_rd_class(typ, content.get("format_version"))
if errors:
ret = InvalidDescr(**content) # pyright: ignore[reportArgumentType]
ret.validation_summary.add_detail(
ValidationDetail(
name="extract fields to chose description class",
status="failed",
errors=errors,
)
)
return ret

typ = content["type"]
rd_class = get_rd_class(typ, content["format_version"])
rd = rd_class.load(content, context=context)
assert rd.validation_summary is not None

if format_version != DISCOVER and not isinstance(rd, InvalidDescr):
# load with requested format_version
discover_details = rd.validation_summary.details
as_rd_class = get_rd_class(typ, format_version)
rd = as_rd_class.load(content, context=context)
Expand Down
90 changes: 50 additions & 40 deletions bioimageio/spec/model/v0_5.py
Original file line number Diff line number Diff line change
Expand Up @@ -2271,49 +2271,59 @@ def get_axis_sizes(
outputs: Dict[Tuple[TensorId, AxisId], Union[int, _DataDepSize]] = {}
input_ids = {d.id for d in self.inputs}
output_ids = {d.id for d in self.outputs}
for t_descr in chain(self.inputs, self.outputs):
for a in t_descr.axes:
if isinstance(a, BatchAxis):
if (t_descr.id, a.id) in ns:
raise ValueError(
f"No size increment factor (n) for batch axis of tensor {t_descr.id} expected."
)
s = batch_size
elif isinstance(a.size, int):
if (t_descr.id, a.id) in ns:
raise ValueError(
f"No size increment factor (n) for fixed size axis {a.id} of tensor {t_descr.id} expected."
)
s = a.size
elif isinstance(a.size, ParameterizedSize):
if (t_descr.id, a.id) not in ns:
raise ValueError(
f"Size increment factor (n) not given for parametrized axis {a.id} of tensor {t_descr.id}."
)
s = a.size.get_size(ns[(t_descr.id, a.id)])
elif isinstance(a.size, SizeReference):
assert not isinstance(a, BatchAxis)
ref_axis = all_axes[a.size.tensor_id][a.size.axis_id]
assert not isinstance(ref_axis, BatchAxis)
if (a.size.tensor_id, a.size.axis_id) not in ns:
raise ValueError(
f"No increment (n) provided for axis {a.id} of tensor {t_descr.id}. Expected reference from tensor {a.size.tensor_id} and axis {a.size.axis_id}."
)
s = a.size.get_size(
axis=a, ref_axis=ref_axis, n=ns[(t_descr.id, a.id)]

def get_axis_size(a: Union[InputAxis, OutputAxis]):
if isinstance(a, BatchAxis):
if (t_descr.id, a.id) in ns:
raise ValueError(
"No size increment factor (n) for batch axis of tensor"
+ f" '{t_descr.id}' expected."
)
elif isinstance(a.size, DataDependentSize):
if (t_descr.id, a.id) in ns:
raise ValueError(
f"No size increment factor (n) for data dependent size axis {a.id} of tensor {t_descr.id} expected."
)
assert t_descr.id in output_ids
outputs[t_descr.id, a.id] = _DataDepSize(a.size.min, a.size.max)
continue
else:
assert_never(a.size)
return batch_size
elif isinstance(a.size, int):
if (t_descr.id, a.id) in ns:
raise ValueError(
"No size increment factor (n) for fixed size axis"
+ f" '{a.id}' of tensor '{t_descr.id}' expected."
)
return a.size
elif isinstance(a.size, ParameterizedSize):
if (t_descr.id, a.id) not in ns:
raise ValueError(
"Size increment factor (n) missing for parametrized axis"
+ f" '{a.id}' of tensor '{t_descr.id}'."
)
return a.size.get_size(ns[(t_descr.id, a.id)])
elif isinstance(a.size, SizeReference):
if (t_descr.id, a.id) in ns:
raise ValueError(
f"No size increment factor (n) for axis '{a.id}' of tensor"
+ f" '{t_descr.id}' with size reference expected."
)
assert not isinstance(a, BatchAxis)
ref_axis = all_axes[a.size.tensor_id][a.size.axis_id]
assert not isinstance(ref_axis, BatchAxis)
return a.size.get_size(
axis=a,
ref_axis=ref_axis,
n=ns.get((a.size.tensor_id, a.size.axis_id), 0),
)
elif isinstance(a.size, DataDependentSize):
if (t_descr.id, a.id) in ns:
raise ValueError(
"No size increment factor (n) for data dependent size axis"
+ f" '{a.id}' of tensor '{t_descr.id}' expected."
)
assert t_descr.id in output_ids
return _DataDepSize(a.size.min, a.size.max)
else:
assert_never(a.size)

for t_descr in chain(self.inputs, self.outputs):
for a in t_descr.axes:
s = get_axis_size(a)
if t_descr.id in input_ids:
assert not isinstance(s, _DataDepSize)
inputs[t_descr.id, a.id] = s
else:
assert t_descr.id in output_ids
Expand Down
13 changes: 9 additions & 4 deletions bioimageio/spec/summary.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
from types import MappingProxyType
from typing import Any, Iterable, List, Literal, Mapping, Tuple, Union, no_type_check

import rich.console
import rich.markdown
from pydantic import BaseModel, Field, model_validator
from pydantic_core.core_schema import ErrorType
from typing_extensions import TypedDict, assert_never
Expand Down Expand Up @@ -279,12 +281,15 @@ def display(self) -> None:
from IPython.core.getipython import get_ipython
from IPython.display import Markdown, display
except ImportError:
print(formatted)
pass
else:
if get_ipython() is None:
print(formatted)
else:
if get_ipython() is not None:
_ = display(Markdown(formatted))
return

rich_markdown = rich.markdown.Markdown(formatted)
console = rich.console.Console()
console.print(rich_markdown)

def add_detail(self, detail: ValidationDetail):
if detail.status == "failed":
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"pydantic>=2.6.3",
"python-dateutil",
"python-dotenv",
"rich",
"ruyaml",
"tqdm",
"typing-extensions",
Expand Down
6 changes: 3 additions & 3 deletions tests/test_model/test_v0_5.py
Original file line number Diff line number Diff line change
Expand Up @@ -379,15 +379,15 @@ def test_get_axis_sizes_raises_with_surplus_n(model_data: Dict[str, Any]):


def test_get_axis_sizes_raises_with_missing_n(model_data: Dict[str, Any]):
model_data["outputs"][0]["axes"][2] = {
model_data["inputs"][0]["axes"][2] = {
"type": "space",
"id": "x",
"size": {"tensor_id": "input_1", "axis_id": "x"},
"halo": 0,
"size": {"min": 10, "step": 5},
}

with ValidationContext(perform_io_checks=False):
model = ModelDescr(**model_data)

with pytest.raises(ValueError):
_ = model.get_axis_sizes(ns={}, batch_size=1)

Expand Down

0 comments on commit 2435ca9

Please sign in to comment.