xarray-annotated 0.5.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,254 @@
1
+ """Typed markers declaring a DataArray's structural properties, and the reader.
2
+
3
+ The structural counterpart to ``units._annotations``: the properties every
4
+ ``DataArray`` possesses regardless of physical units — its dimensions
5
+ (``Dims``), coordinates (``Coords``), and dtype (``Dtype``). Used inside
6
+ ``Annotated`` exactly as ``Unit`` is, and — unlike units — **several at once**::
7
+
8
+ Annotated[xr.DataArray, Dims("time", "lat", "lon")]
9
+ Annotated[xr.DataArray, Dims("time", "x"), Dtype("float64")]
10
+
11
+ Each marker carries the *strictness* of its own check (dims order, exact dtype)
12
+ and an optional per-marker ``on_mismatch`` severity override, because the three
13
+ properties have different natural defaults and a wrong dtype is often less severe
14
+ than a wrong set of dims. The markers are immutable (``__slots__``) and hashable
15
+ so an annotation carrying them stays hashable/cacheable; ``eq``/``repr`` cover all
16
+ fields, so ``eval(repr(m)) == m``.
17
+
18
+ There is deliberately **no bare-string shorthand** (a string in the metadata is a
19
+ description, never a declaration): only these typed markers are read.
20
+ """
21
+
22
+ from typing import (
23
+ Annotated,
24
+ Any,
25
+ get_args,
26
+ get_origin,
27
+ )
28
+
29
+ from .._annotations import _is_dataarray_type, walk_signature
30
+ from ._config import OnMismatch, _validate_on_mismatch
31
+
32
+
33
+ def _check_on_mismatch(value: OnMismatch | None) -> OnMismatch | None:
34
+ """Validate a marker's optional ``on_mismatch`` override at construction."""
35
+ return None if value is None else _validate_on_mismatch(value)
36
+
37
+
38
+ class Dims:
39
+ """Declare the expected dimension names of a DataArray.
40
+
41
+ ``Dims("time", "lat", "lon")`` declares an array over exactly those dims.
42
+ By default the *set* of dims must match (extra or missing dims fail) but their
43
+ order is free — xarray operations are order-independent until you drop to
44
+ numpy. Pass ``ordered=True`` to also pin the order (e.g. before ``.values``,
45
+ ``.stack`` or ``apply_ufunc``).
46
+ """
47
+
48
+ _names: tuple[str, ...]
49
+ _ordered: bool
50
+ _on_mismatch: OnMismatch | None
51
+ __slots__ = ("_names", "_ordered", "_on_mismatch")
52
+
53
+ def __init__(
54
+ self,
55
+ *names: str,
56
+ ordered: bool = False,
57
+ on_mismatch: OnMismatch | None = None,
58
+ ) -> None:
59
+ self._names = tuple(names)
60
+ self._ordered = ordered
61
+ self._on_mismatch = _check_on_mismatch(on_mismatch)
62
+
63
+ @property
64
+ def names(self) -> tuple[str, ...]:
65
+ """The declared dimension names, in the order given."""
66
+ return self._names
67
+
68
+ @property
69
+ def ordered(self) -> bool:
70
+ """Whether dim order (not just the set) must match."""
71
+ return self._ordered
72
+
73
+ @property
74
+ def on_mismatch(self) -> OnMismatch | None:
75
+ """Per-marker severity override, or `None` to use the policy default."""
76
+ return self._on_mismatch
77
+
78
+ def __repr__(self) -> str:
79
+ parts = [repr(n) for n in self._names]
80
+ if self._ordered:
81
+ parts.append("ordered=True")
82
+ if self._on_mismatch is not None:
83
+ parts.append(f"on_mismatch={self._on_mismatch!r}")
84
+ return f"Dims({', '.join(parts)})"
85
+
86
+ def __eq__(self, other: object) -> bool:
87
+ if isinstance(other, Dims):
88
+ return (self._names, self._ordered, self._on_mismatch) == (
89
+ other._names,
90
+ other._ordered,
91
+ other._on_mismatch,
92
+ )
93
+ return NotImplemented
94
+
95
+ def __hash__(self) -> int:
96
+ return hash((self._names, self._ordered, self._on_mismatch))
97
+
98
+
99
+ class Coords:
100
+ """Declare coordinate variables a DataArray must carry.
101
+
102
+ ``Coords("time", "lat")`` declares that those coordinates are present (as
103
+ *labels*, not merely dims — a dim can exist without coordinate values).
104
+ Extra coordinates are allowed; only the declared ones must be present.
105
+ """
106
+
107
+ _names: tuple[str, ...]
108
+ _on_mismatch: OnMismatch | None
109
+ __slots__ = ("_names", "_on_mismatch")
110
+
111
+ def __init__(
112
+ self,
113
+ *names: str,
114
+ on_mismatch: OnMismatch | None = None,
115
+ ) -> None:
116
+ self._names = tuple(names)
117
+ self._on_mismatch = _check_on_mismatch(on_mismatch)
118
+
119
+ @property
120
+ def names(self) -> tuple[str, ...]:
121
+ """The declared coordinate names."""
122
+ return self._names
123
+
124
+ @property
125
+ def on_mismatch(self) -> OnMismatch | None:
126
+ """Per-marker severity override, or `None` to use the policy default."""
127
+ return self._on_mismatch
128
+
129
+ def __repr__(self) -> str:
130
+ parts = [repr(n) for n in self._names]
131
+ if self._on_mismatch is not None:
132
+ parts.append(f"on_mismatch={self._on_mismatch!r}")
133
+ return f"Coords({', '.join(parts)})"
134
+
135
+ def __eq__(self, other: object) -> bool:
136
+ if isinstance(other, Coords):
137
+ return (self._names, self._on_mismatch) == (
138
+ other._names,
139
+ other._on_mismatch,
140
+ )
141
+ return NotImplemented
142
+
143
+ def __hash__(self) -> int:
144
+ return hash((self._names, self._on_mismatch))
145
+
146
+
147
+ class Dtype:
148
+ """Declare the expected dtype of a DataArray, e.g. ``Dtype("float64")``.
149
+
150
+ By default the check is by *kind*: any float satisfies ``Dtype("float64")``,
151
+ any integer satisfies ``Dtype("int32")`` — enough to catch an int/float or
152
+ bool/float mix-up without firing on ``f8`` vs ``f4``. Pass ``exact=True`` to
153
+ require the precise dtype (e.g. to pin memory footprint or a typed sink).
154
+ """
155
+
156
+ _dtype: str
157
+ _exact: bool
158
+ _on_mismatch: OnMismatch | None
159
+ __slots__ = ("_dtype", "_exact", "_on_mismatch")
160
+
161
+ def __init__(
162
+ self,
163
+ dtype: str,
164
+ *,
165
+ exact: bool = False,
166
+ on_mismatch: OnMismatch | None = None,
167
+ ) -> None:
168
+ self._dtype = dtype
169
+ self._exact = exact
170
+ self._on_mismatch = _check_on_mismatch(on_mismatch)
171
+
172
+ @property
173
+ def dtype(self) -> str:
174
+ """The declared dtype string."""
175
+ return self._dtype
176
+
177
+ @property
178
+ def exact(self) -> bool:
179
+ """Whether the exact dtype (not just its kind) must match."""
180
+ return self._exact
181
+
182
+ @property
183
+ def on_mismatch(self) -> OnMismatch | None:
184
+ """Per-marker severity override, or `None` to use the policy default."""
185
+ return self._on_mismatch
186
+
187
+ def __repr__(self) -> str:
188
+ parts = [repr(self._dtype)]
189
+ if self._exact:
190
+ parts.append("exact=True")
191
+ if self._on_mismatch is not None:
192
+ parts.append(f"on_mismatch={self._on_mismatch!r}")
193
+ return f"Dtype({', '.join(parts)})"
194
+
195
+ def __eq__(self, other: object) -> bool:
196
+ if isinstance(other, Dtype):
197
+ return (self._dtype, self._exact, self._on_mismatch) == (
198
+ other._dtype,
199
+ other._exact,
200
+ other._on_mismatch,
201
+ )
202
+ return NotImplemented
203
+
204
+ def __hash__(self) -> int:
205
+ return hash((self._dtype, self._exact, self._on_mismatch))
206
+
207
+
208
+ #: The marker types the reader recognises in `Annotated` metadata.
209
+ SchemaMarker = Dims | Coords | Dtype
210
+ _SCHEMA_MARKERS = (Dims, Coords, Dtype)
211
+
212
+
213
+ def annotated_schema(hint: Any) -> list[SchemaMarker] | None:
214
+ """Return every schema marker carried by an `Annotated` hint, or `None`.
215
+
216
+ Collects *all* `Dims` / `Coords` / `Dtype` markers in the metadata (a hint may
217
+ declare several structural properties at once), but only when the annotated
218
+ base type is a `DataArray` (incl. `DataArray | None`). Non-`Annotated` hints,
219
+ hints on non-DataArray types, and hints with no schema marker return `None`
220
+ (so `walk_signature`'s "no declaration" filter drops them).
221
+
222
+ Args:
223
+ hint: A type hint, typically from `get_type_hints(..., include_extras=True)`.
224
+
225
+ Returns:
226
+ A non-empty list of markers in annotation order, or `None`.
227
+ """
228
+ if get_origin(hint) is not Annotated:
229
+ return None
230
+ args = get_args(hint)
231
+ if not _is_dataarray_type(args[0]):
232
+ return None
233
+ markers = [m for m in args[1:] if isinstance(m, _SCHEMA_MARKERS)]
234
+ return markers or None
235
+
236
+
237
+ def schema_from_signature(
238
+ func: object,
239
+ ) -> tuple[
240
+ dict[str, list[SchemaMarker]],
241
+ dict[str, list[SchemaMarker]] | list[SchemaMarker] | None,
242
+ ]:
243
+ """Extract declared schema markers from a function's type annotations.
244
+
245
+ Args:
246
+ func: A callable whose hints carry `Annotated[DataArray, <markers>]`.
247
+
248
+ Returns:
249
+ An `(inputs, output)` pair, where each declaration is the list of markers
250
+ on that parameter/field; `output` is a per-field dict for a `TypedDict` or
251
+ dataclass return, a single list for one declared `DataArray` return, or
252
+ `None`.
253
+ """
254
+ return walk_signature(func, annotated_schema)
@@ -0,0 +1,232 @@
1
+ """Runtime structural validation of a DataArray against declared markers.
2
+
3
+ The "usage side" of the schema domain: given a `DataArray` and the `Dims` /
4
+ `Coords` / `Dtype` markers declared for it, check each one under the active policy.
5
+ Unlike the units domain this **never mutates** — it asserts and (per `on_mismatch`)
6
+ raises, warns, or ignores, then returns the array unchanged, so a decorator can
7
+ drop it into an argument-validation pass symmetrically with `check_units`.
8
+
9
+ The per-property checks are independent functions dispatched by marker type, so a
10
+ new structural property is added by writing one checker and one dispatch entry —
11
+ nothing else here changes.
12
+ """
13
+
14
+ import warnings
15
+ from collections.abc import Callable
16
+ from typing import Any
17
+
18
+ import numpy as np
19
+ import xarray as xr
20
+
21
+ from ._annotations import Coords, Dims, Dtype, SchemaMarker
22
+ from ._config import OnMismatch, _validate_on_mismatch, get_policy
23
+
24
+
25
+ class SchemaWarning(UserWarning):
26
+ """Warning issued for a structural mismatch under ``on_mismatch="warn"``."""
27
+
28
+
29
+ class SchemaError(Exception):
30
+ """Raised for a structural mismatch under ``on_mismatch="error"``.
31
+
32
+ Deliberately **not** a ``ValueError``: a structural mismatch (the *data* fails
33
+ the declaration) is a distinct event from a malformed *declaration* (an
34
+ unparseable dtype, duplicate dim names), which raises ``ValueError`` from
35
+ ``assert_valid_schema``. Keeping them on separate hierarchies means catching
36
+ one never silently swallows the other.
37
+ """
38
+
39
+
40
+ def assert_valid_schema(marker: SchemaMarker, context: str) -> None:
41
+ """Validate a marker *declaration* itself, independent of any DataArray.
42
+
43
+ Used at decoration time to fail fast on a malformed declaration (e.g. an
44
+ unparseable dtype string or duplicate dim names) rather than only when the
45
+ decorated function first runs.
46
+
47
+ Args:
48
+ marker: The declared `Dims`, `Coords`, or `Dtype` marker.
49
+ context: A label for the error message (e.g. ``"f input 'x'"``).
50
+
51
+ Raises:
52
+ ValueError: If the declaration is malformed.
53
+ """
54
+ kind = type(marker).__name__
55
+ if isinstance(marker, Dtype):
56
+ try:
57
+ np.dtype(marker.dtype)
58
+ except TypeError as exc:
59
+ raise ValueError(f"{context}: invalid dtype {marker.dtype!r}.") from exc
60
+ else: # Dims or Coords: a tuple of names
61
+ for name in marker.names:
62
+ if not isinstance(name, str) or not name:
63
+ raise ValueError(
64
+ f"{context}: {kind} names must be non-empty strings, "
65
+ f"got {marker.names!r}."
66
+ )
67
+ if len(set(marker.names)) != len(marker.names):
68
+ raise ValueError(f"{context}: {kind} has duplicate names {marker.names!r}.")
69
+
70
+
71
+ def _check_dims(da: xr.DataArray, marker: Dims) -> tuple[bool, str]:
72
+ """Check a DataArray's dims against a `Dims` marker. Returns (ok, detail)."""
73
+ actual = tuple(da.dims)
74
+ if marker.ordered:
75
+ ok = actual == marker.names
76
+ detail = f"dims order mismatch: expected {marker.names}, got {actual}"
77
+ else:
78
+ ok = set(actual) == set(marker.names)
79
+ detail = f"dims mismatch: expected {marker.names} in any order, got {actual}"
80
+ return ok, detail
81
+
82
+
83
+ def _check_coords(da: xr.DataArray, marker: Coords) -> tuple[bool, str]:
84
+ """Check a DataArray carries the declared coords. Returns (ok, detail)."""
85
+ actual = set(map(str, da.coords))
86
+ missing = [n for n in marker.names if n not in actual]
87
+ detail = f"missing coords {missing}; present: {sorted(actual)}"
88
+ return not missing, detail
89
+
90
+
91
+ def _check_dtype(da: xr.DataArray, marker: Dtype) -> tuple[bool, str]:
92
+ """Check a DataArray's dtype against a `Dtype` marker. Returns (ok, detail)."""
93
+ declared = np.dtype(marker.dtype)
94
+ actual = da.dtype
95
+ if marker.exact:
96
+ ok = actual == declared
97
+ detail = f"dtype mismatch: expected exactly {declared}, got {actual}"
98
+ else:
99
+ ok = actual.kind == declared.kind
100
+ detail = (
101
+ f"dtype kind mismatch: expected kind {declared.kind!r} "
102
+ f"(like {declared}), got {actual}"
103
+ )
104
+ return ok, detail
105
+
106
+
107
+ def dims_compatible(a: Dims, b: Dims) -> bool:
108
+ """Return whether two `Dims` declarations can describe the same array.
109
+
110
+ The marker-vs-marker counterpart to validating an array against a `Dims`
111
+ declaration (no array in hand), for a static / build-time check of a
112
+ producer/consumer edge: two declarations are
113
+ *provably inconsistent* only if their dim **sets** differ, or if both pin the
114
+ order (`ordered=True`) and the orders disagree. A loose declaration on either
115
+ side can always be satisfied, so it never conflicts.
116
+
117
+ Args:
118
+ a: A `Dims` marker.
119
+ b: Another `Dims` marker.
120
+
121
+ Returns:
122
+ `True` unless the two are provably inconsistent.
123
+
124
+ Examples:
125
+ >>> from xarray_annotated.schema import Dims, dims_compatible
126
+ >>> dims_compatible(Dims("x", "y"), Dims("y", "x"))
127
+ True
128
+ >>> dims_compatible(Dims("x"), Dims("x", "y"))
129
+ False
130
+ >>> dims_compatible(Dims("x", "y", ordered=True), Dims("y", "x", ordered=True))
131
+ False
132
+ """
133
+ if set(a.names) != set(b.names):
134
+ return False
135
+ return not (a.ordered and b.ordered and a.names != b.names)
136
+
137
+
138
+ def dtype_compatible(a: Dtype, b: Dtype) -> bool:
139
+ """Return whether two `Dtype` declarations can describe the same array.
140
+
141
+ The marker-vs-marker counterpart to validating an array against a `Dtype`
142
+ declaration (no array in hand): two
143
+ declarations are *provably inconsistent* only if their numpy **kinds** differ
144
+ (float vs int), or if both require the exact dtype (`exact=True`) and those
145
+ dtypes differ (`f8` vs `f4`). A kind-only declaration matches any width, so
146
+ it never conflicts on width alone.
147
+
148
+ Args:
149
+ a: A `Dtype` marker.
150
+ b: Another `Dtype` marker.
151
+
152
+ Returns:
153
+ `True` unless the two are provably inconsistent.
154
+
155
+ Examples:
156
+ >>> from xarray_annotated.schema import Dtype, dtype_compatible
157
+ >>> dtype_compatible(Dtype("float64"), Dtype("float32"))
158
+ True
159
+ >>> dtype_compatible(Dtype("float64"), Dtype("int64"))
160
+ False
161
+ >>> dtype_compatible(Dtype("float64", exact=True), Dtype("float32", exact=True))
162
+ False
163
+ """
164
+ if np.dtype(a.dtype).kind != np.dtype(b.dtype).kind:
165
+ return False
166
+ return not (a.exact and b.exact and np.dtype(a.dtype) != np.dtype(b.dtype))
167
+
168
+
169
+ # There is deliberately no `coords_compatible`: two `Coords` declarations are
170
+ # lower bounds (each merely requires its names be present, extras allowed), so
171
+ # they can never be *proven* inconsistent — a build-time edge check skips coords.
172
+
173
+
174
+ _CHECKERS: dict[type, Callable[[xr.DataArray, Any], tuple[bool, str]]] = {
175
+ Dims: _check_dims,
176
+ Coords: _check_coords,
177
+ Dtype: _check_dtype,
178
+ }
179
+
180
+
181
+ def _report(ok: bool, severity: OnMismatch, message: str) -> None:
182
+ """Act on a check result under the resolved severity."""
183
+ if ok or severity == "ignore":
184
+ return
185
+ if severity == "error":
186
+ raise SchemaError(message)
187
+ # stacklevel=3 targets the declare_schema call path (user -> wrapper ->
188
+ # check_schema -> _report); one frame too far for a direct check_schema call.
189
+ warnings.warn(message, SchemaWarning, stacklevel=3)
190
+
191
+
192
+ def check_schema(
193
+ da: xr.DataArray,
194
+ declared: SchemaMarker | list[SchemaMarker],
195
+ name: str,
196
+ on_mismatch: OnMismatch | None = None,
197
+ qualname: str | None = None,
198
+ ) -> xr.DataArray:
199
+ """Validate `da` against its declared schema marker(s); return it unchanged.
200
+
201
+ Runs each marker's checker under the effective severity, resolved per marker
202
+ as: the marker's own `on_mismatch` override, else the `on_mismatch` argument,
203
+ else the policy default (`get_policy().on_mismatch`). When the master switch
204
+ is off (`enabled=False`) this is a total no-op.
205
+
206
+ Args:
207
+ da: The DataArray to validate.
208
+ declared: A single marker or a list of markers declared for `da`.
209
+ name: The parameter/field name, for error messages.
210
+ on_mismatch: Per-call default severity, overriding the policy default but
211
+ overridden by a marker's own `on_mismatch`.
212
+ qualname: Optional qualified function name, prefixed to messages.
213
+
214
+ Returns:
215
+ `da`, unchanged (validation never mutates).
216
+
217
+ Raises:
218
+ SchemaError: On a mismatch when the effective severity is ``"error"``.
219
+ """
220
+ pol = get_policy()
221
+ if not pol.enabled:
222
+ return da
223
+ if on_mismatch is not None:
224
+ on_mismatch = _validate_on_mismatch(on_mismatch)
225
+
226
+ markers = [declared] if isinstance(declared, (Dims, Coords, Dtype)) else declared
227
+ prefix = f"[{qualname}] {name!r}" if qualname else f"{name!r}"
228
+ for marker in markers:
229
+ ok, detail = _CHECKERS[type(marker)](da, marker)
230
+ severity = marker.on_mismatch or on_mismatch or pol.on_mismatch
231
+ _report(ok, severity, f"{prefix} {detail}")
232
+ return da
@@ -0,0 +1,147 @@
1
+ """Process-wide validation policy for the structural (schema) domain.
2
+
3
+ Simpler than the units policy because structural validation never converts — it
4
+ only asserts. Two axes:
5
+
6
+ - `enabled` — the **package-wide** master switch (shared with every domain via
7
+ `xarray_annotated._config`); `False` makes all validation a no-op.
8
+ - `on_mismatch` — what to do when a DataArray's structure does not match a
9
+ declaration (`"error"` / `"warn"` / `"ignore"`). This is the *default*; an
10
+ individual marker may override it (e.g. error on wrong dims but only warn on
11
+ wrong dtype).
12
+
13
+ Each axis resolves independently: environment variable, then a process override
14
+ set via `set_policy` / the `policy` context manager, then the module default. The
15
+ shared `_resolve` helper encodes that precedence.
16
+ """
17
+
18
+ from contextlib import contextmanager
19
+ from dataclasses import dataclass
20
+ from typing import Literal
21
+
22
+ from .._config import (
23
+ DEFAULT_ENABLED,
24
+ ENABLED_ENV_VAR,
25
+ _resolve,
26
+ _Unset,
27
+ _UNSET,
28
+ get_enabled_override,
29
+ resolve_enabled,
30
+ set_enabled_override,
31
+ )
32
+
33
+ # `ENABLED_ENV_VAR` is re-exported (the master switch is shared but callers still
34
+ # reach it as `schema._config.ENABLED_ENV_VAR`).
35
+ __all__ = [
36
+ "ENABLED_ENV_VAR",
37
+ "OnMismatch",
38
+ "Policy",
39
+ "get_policy",
40
+ "policy",
41
+ "set_policy",
42
+ ]
43
+
44
+ OnMismatch = Literal["error", "warn", "ignore"]
45
+
46
+ _VALID_ON_MISMATCH: frozenset[str] = frozenset({"error", "warn", "ignore"})
47
+
48
+ #: A structural mismatch is usually a genuine bug (wrong array wired in), so the
49
+ #: default is to raise. Soften it per-marker (`Dims(..., on_mismatch="warn")`) or
50
+ #: package-wide via the policy.
51
+ DEFAULT_ON_MISMATCH: OnMismatch = "error"
52
+
53
+ ON_MISMATCH_ENV_VAR = "XARRAY_ANNOTATED_SCHEMA_ON_MISMATCH"
54
+
55
+ _process_on_mismatch: OnMismatch | None = None
56
+
57
+
58
+ @dataclass(frozen=True, slots=True)
59
+ class Policy:
60
+ """The resolved schema-validation policy — the two axes as a single value.
61
+
62
+ Returned by `get_policy`. Build overrides via `set_policy` or the `policy`
63
+ context manager rather than constructing this directly.
64
+
65
+ Attributes:
66
+ enabled: Master switch; `False` makes all validation a no-op.
67
+ on_mismatch: Default behaviour on a structural mismatch
68
+ (`"error"`, `"warn"`, or `"ignore"`); a marker may override it.
69
+ """
70
+
71
+ enabled: bool = DEFAULT_ENABLED
72
+ on_mismatch: OnMismatch = DEFAULT_ON_MISMATCH
73
+
74
+
75
+ def _validate_on_mismatch(value: str) -> OnMismatch:
76
+ if value not in _VALID_ON_MISMATCH:
77
+ raise ValueError(
78
+ f"Invalid on_mismatch {value!r}. "
79
+ f"Choose one of {sorted(_VALID_ON_MISMATCH)}."
80
+ )
81
+ return value # type: ignore[return-value]
82
+
83
+
84
+ def get_policy() -> Policy:
85
+ """Resolve the active schema policy (env → process → default, per axis).
86
+
87
+ Returns:
88
+ The resolved `Policy`.
89
+ """
90
+ return Policy(
91
+ enabled=resolve_enabled(),
92
+ on_mismatch=_resolve(
93
+ ON_MISMATCH_ENV_VAR,
94
+ _process_on_mismatch,
95
+ DEFAULT_ON_MISMATCH,
96
+ lambda v: _validate_on_mismatch(v.lower()),
97
+ ),
98
+ )
99
+
100
+
101
+ def set_policy(
102
+ *,
103
+ enabled: bool | None | _Unset = _UNSET,
104
+ on_mismatch: OnMismatch | None | _Unset = _UNSET,
105
+ ) -> None:
106
+ """Set process-wide schema-policy overrides for one or more axes.
107
+
108
+ Only the axes you pass are changed. Pass a value to set it, `None` to
109
+ *clear* that axis's override (so its env var / default applies again), or
110
+ omit it to leave it untouched.
111
+
112
+ Args:
113
+ enabled: Override the (package-wide) master switch, or `None` to clear.
114
+ on_mismatch: Override the on-mismatch axis, or `None` to clear.
115
+ """
116
+ global _process_on_mismatch
117
+ if not isinstance(enabled, _Unset):
118
+ set_enabled_override(enabled)
119
+ if not isinstance(on_mismatch, _Unset):
120
+ _process_on_mismatch = (
121
+ None if on_mismatch is None else _validate_on_mismatch(on_mismatch)
122
+ )
123
+
124
+
125
+ @contextmanager
126
+ def policy(
127
+ *,
128
+ enabled: bool | None | _Unset = _UNSET,
129
+ on_mismatch: OnMismatch | None | _Unset = _UNSET,
130
+ ):
131
+ """Temporarily override schema-policy axes, restoring them on exit.
132
+
133
+ Sets every axis you pass in one go, and restores the previous process
134
+ overrides afterwards even if an exception is raised.
135
+
136
+ Args:
137
+ enabled: Override the (package-wide) master switch, or `None` to clear.
138
+ on_mismatch: Override the on-mismatch axis, or `None` to clear.
139
+ """
140
+ global _process_on_mismatch
141
+ saved = (get_enabled_override(), _process_on_mismatch)
142
+ set_policy(enabled=enabled, on_mismatch=on_mismatch)
143
+ try:
144
+ yield
145
+ finally:
146
+ enabled_saved, _process_on_mismatch = saved
147
+ set_enabled_override(enabled_saved)