Coverage for bioimageio/spec/_internal/field_warning.py: 95%
57 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-02 14:21 +0000
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-02 14:21 +0000
1import dataclasses
2import sys
3from typing import TYPE_CHECKING, Any, Dict, Literal, Optional, Union, get_args
5import pydantic.functional_validators
6from annotated_types import BaseMetadata, GroupedMetadata
7from loguru import logger
8from pydantic import TypeAdapter
9from pydantic._internal._decorators import inspect_validator
10from pydantic_core import PydanticCustomError
11from pydantic_core.core_schema import (
12 NoInfoValidatorFunction,
13 ValidationInfo,
14 WithInfoValidatorFunction,
15)
16from typing_extensions import Annotated, LiteralString
18from .validation_context import get_validation_context
19from .warning_levels import WARNING, WarningSeverity
21if TYPE_CHECKING:
22 from pydantic.functional_validators import _V2Validator # type: ignore
24if sys.version_info < (3, 10):
25 SLOTS: Dict[str, Any] = {}
26else:
27 SLOTS = {"slots": True}
30ValidatorFunction = Union[NoInfoValidatorFunction, WithInfoValidatorFunction]
32AnnotationMetaData = Union[BaseMetadata, GroupedMetadata]
35def warn(
36 typ: Union[AnnotationMetaData, Any],
37 msg: LiteralString, # warning message, e.g. "'{value}' incompatible with {typ}
38 severity: WarningSeverity = WARNING,
39):
40 """treat a type or its annotation metadata as a warning condition"""
41 if isinstance(typ, get_args(AnnotationMetaData)):
42 typ = Annotated[Any, typ]
44 validator: TypeAdapter[Any] = TypeAdapter(typ)
46 return AfterWarner(
47 validator.validate_python, severity=severity, msg=msg, context={"typ": typ}
48 )
51def call_validator_func(
52 func: "_V2Validator",
53 mode: Literal["after", "before", "plain", "wrap"],
54 value: Any,
55 info: ValidationInfo,
56) -> Any:
57 info_arg = inspect_validator(func, mode)
58 if info_arg:
59 return func(value, info) # type: ignore
60 else:
61 return func(value) # type: ignore
64def as_warning(
65 func: "_V2Validator",
66 *,
67 mode: Literal["after", "before", "plain", "wrap"] = "after",
68 severity: WarningSeverity = WARNING,
69 msg: Optional[LiteralString] = None,
70 msg_context: Optional[Dict[str, Any]] = None,
71) -> ValidatorFunction:
72 """turn validation function into a no-op, based on warning level"""
74 def wrapper(value: Any, info: ValidationInfo) -> Any:
75 try:
76 call_validator_func(func, mode, value, info)
77 except (AssertionError, ValueError) as e:
78 issue_warning(
79 msg or ",".join(e.args),
80 value=value,
81 severity=severity,
82 msg_context=msg_context,
83 )
85 return value
87 return wrapper
90@dataclasses.dataclass(frozen=True, **SLOTS)
91class AfterWarner(pydantic.functional_validators.AfterValidator):
92 """Like AfterValidator, but wraps validation `func` `as_warning`"""
94 severity: WarningSeverity = WARNING
95 msg: Optional[LiteralString] = None
96 context: Optional[Dict[str, Any]] = None
98 def __post_init__(self):
99 object.__setattr__(
100 self,
101 "func",
102 as_warning(
103 self.func,
104 mode="after",
105 severity=self.severity,
106 msg=self.msg,
107 msg_context=self.context,
108 ),
109 )
112@dataclasses.dataclass(frozen=True, **SLOTS)
113class BeforeWarner(pydantic.functional_validators.BeforeValidator):
114 """Like BeforeValidator, but wraps validation `func` `as_warning`"""
116 severity: WarningSeverity = WARNING
117 msg: Optional[LiteralString] = None
118 context: Optional[Dict[str, Any]] = None
120 def __post_init__(self):
121 object.__setattr__(
122 self,
123 "func",
124 as_warning(
125 self.func,
126 mode="before",
127 severity=self.severity,
128 msg=self.msg,
129 msg_context=self.context,
130 ),
131 )
134# TODO: add `loc: Loc` to `issue_warning()`
135# and use a loguru handler to format warnings accordingly
136def issue_warning(
137 msg: LiteralString,
138 *,
139 value: Any,
140 severity: WarningSeverity = WARNING,
141 msg_context: Optional[Dict[str, Any]] = None,
142 field: Optional[str] = None,
143):
144 msg_context = {"value": value, "severity": severity, **(msg_context or {})}
145 if severity >= (ctxt := get_validation_context()).warning_level:
146 raise PydanticCustomError("warning", msg, msg_context)
147 elif ctxt.log_warnings:
148 log_msg = (field + ": " if field else "") + (msg.format(**msg_context))
149 logger.opt(depth=1).log(severity, log_msg)