Coverage for src/bioimageio/spec/_internal/utils.py: 65%
112 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-18 09:17 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-18 09:17 +0000
1from __future__ import annotations
3import dataclasses
4import re
5import sys
6from dataclasses import dataclass
7from functools import wraps
8from inspect import isfunction, signature
9from typing import (
10 Any,
11 Callable,
12 Dict,
13 Iterable,
14 TypeVar,
15)
17import pydantic
18from exceptiongroup import ExceptionGroup
19from ruyaml import Optional
20from typing_extensions import ParamSpec
22if sys.version_info < (3, 10): # pragma: no cover
23 SLOTS: dict[str, bool] = {}
24else:
25 SLOTS = {"slots": True}
28K = TypeVar("K")
29V = TypeVar("V")
30NestedDict = Dict[K, "NestedDict[K, V] | V"]
32if sys.version_info < (3, 9): # pragma: no cover
33 from functools import lru_cache as cache
34 from pathlib import Path
36 def files(package_name: str):
37 assert package_name == "bioimageio.spec", package_name
38 return Path(__file__).parent.parent
40else:
41 from functools import cache as cache
42 from importlib.resources import files as files
45def get_format_version_tuple(format_version: Any) -> Optional[tuple[int, int, int]]:
46 if (
47 not isinstance(format_version, str)
48 or format_version.count(".") != 2
49 or any(not v.isdigit() for v in format_version.split("."))
50 ):
51 return None
53 parsed = tuple(map(int, format_version.split(".")))
54 assert len(parsed) == 3
55 return parsed
58def nest_dict(flat_dict: dict[tuple[K, ...], V]) -> NestedDict[K, V]:
59 res: NestedDict[K, V] = {}
60 for k, v in flat_dict.items():
61 node: dict[K, NestedDict[K, V] | V] | NestedDict[K, V] = res
62 for kk in k[:-1]:
63 if not isinstance(node, dict):
64 raise ValueError(f"nesting level collision for flat key {k} at {kk}")
65 d: NestedDict[K, V] = {}
66 node = node.setdefault(kk, d) # type: ignore
68 if not isinstance(node, dict):
69 raise ValueError(f"nesting level collision for flat key {k}")
71 node[k[-1]] = v
73 return res
76FirstK = TypeVar("FirstK")
79def nest_dict_with_narrow_first_key(
80 flat_dict: dict[tuple[K, ...], V], first_k: type[FirstK]
81) -> dict[FirstK, NestedDict[K, V] | V]:
82 """convenience function to annotate a special version of a NestedDict.
83 Root level keys are of a narrower type than the nested keys. If not a ValueError is raisd.
84 """
85 nested = nest_dict(flat_dict)
86 invalid_first_keys = [k for k in nested if not isinstance(k, first_k)]
87 if invalid_first_keys:
88 raise ValueError(f"Invalid root level keys: {invalid_first_keys}")
90 return nested # type: ignore
93def unindent(text: str, ignore_first_line: bool = False):
94 """remove minimum count of spaces at beginning of each line.
96 Args:
97 text: indented text
98 ignore_first_line: allows to correctly unindent doc strings
99 """
100 first = int(ignore_first_line)
101 lines = text.split("\n")
102 filled_lines = [line for line in lines[first:] if line]
103 if len(filled_lines) < 2:
104 return "\n".join(line.strip() for line in lines)
106 indent = min(len(line) - len(line.lstrip(" ")) for line in filled_lines)
107 return "\n".join(lines[:first] + [line[indent:] for line in lines[first:]])
110T = TypeVar("T")
111P = ParamSpec("P")
114def assert_all_params_set_explicitly(fn: Callable[P, T]) -> Callable[P, T]:
115 @wraps(fn)
116 def wrapper(*args: P.args, **kwargs: P.kwargs):
117 n_args = len(args)
118 missing: set[str] = set()
120 for p in signature(fn).parameters.values():
121 if p.kind == p.POSITIONAL_ONLY:
122 if n_args == 0:
123 missing.add(p.name)
124 else:
125 n_args -= 1 # 'use' positional arg
126 elif p.kind == p.POSITIONAL_OR_KEYWORD:
127 if n_args == 0:
128 if p.name not in kwargs:
129 missing.add(p.name)
130 else:
131 n_args -= 1 # 'use' positional arg
132 elif p.kind in (p.VAR_POSITIONAL, p.VAR_KEYWORD):
133 pass
134 elif p.kind == p.KEYWORD_ONLY and p.name not in kwargs:
135 missing.add(p.name)
137 assert not missing, f"parameters {missing} of {fn} are not set explicitly"
139 return fn(*args, **kwargs)
141 return wrapper
144def get_os_friendly_file_name(name: str) -> str:
145 return re.sub(r"\W+|^(?=\d)", "_", name)
148@dataclass
149class _PrettyDataClassReprMixin:
150 """A mixin that provides a pretty __repr__ for dataclasses
152 - leaving out fields that are None
153 - leaving out memory locations of functions
154 """
156 def __repr__(self):
157 field_values = {
158 f.name: v
159 for f in dataclasses.fields(self)
160 if (v := getattr(self, f.name)) is not None
161 }
162 field_str = ", ".join(
163 f"{k}=" + (f"<function {v.__name__}>" if isfunction(v) else repr(v))
164 for k, v in field_values.items()
165 )
166 return f"{self.__class__.__name__}({field_str})"
169class PrettyPlainSerializer(pydantic.PlainSerializer, _PrettyDataClassReprMixin):
170 pass
173class PrettyWrapSerializer(pydantic.WrapSerializer, _PrettyDataClassReprMixin):
174 pass
177def try_all(
178 funcs: Iterable[Callable[P, T]],
179 *args: P.args,
180 **kwargs: P.kwargs,
181) -> T:
182 ret, errors = _try_all(funcs, False, *args, **kwargs)
183 if errors:
184 raise ExceptionGroup("All functions raised", errors)
186 assert not isinstance(ret, _AllFailedSentinel)
187 return ret
190def try_all_raise_last(
191 funcs: Iterable[Callable[P, T]],
192 *args: P.args,
193 **kwargs: P.kwargs,
194) -> T:
195 ret, errors = _try_all(funcs, True, *args, **kwargs)
196 if errors:
197 raise errors[-1]
199 assert not isinstance(ret, _AllFailedSentinel)
200 return ret
203class _AllFailedSentinel:
204 pass
207def _try_all(
208 funcs: Iterable[Callable[P, T]],
209 raise_last_only: bool,
210 *args: P.args,
211 **kwargs: P.kwargs,
212) -> tuple[_AllFailedSentinel | T, list[Exception]]:
213 """Try to call each of the functions `funcs` with the given arguments.
215 If all raise, raise an exception group (or the last).
217 Returns:
218 Result of the first successful call.
219 """
220 errors: list[Exception] = []
221 for c in funcs:
222 try:
223 return c(*args, **kwargs), []
224 except Exception as e:
225 errors.append(e)
227 if errors:
228 errors.append(RuntimeError("No functions provided to try."))
230 return _AllFailedSentinel(), errors