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.
- xarray_annotated/__init__.py +50 -0
- xarray_annotated/_annotations.py +119 -0
- xarray_annotated/_config.py +101 -0
- xarray_annotated/_reader.py +110 -0
- xarray_annotated/_writer.py +99 -0
- xarray_annotated/py.typed +0 -0
- xarray_annotated/schema/__init__.py +62 -0
- xarray_annotated/schema/_annotations.py +254 -0
- xarray_annotated/schema/_check.py +232 -0
- xarray_annotated/schema/_config.py +147 -0
- xarray_annotated/schema/_decorator.py +124 -0
- xarray_annotated/temporal/__init__.py +60 -0
- xarray_annotated/temporal/_annotations.py +154 -0
- xarray_annotated/temporal/_check.py +326 -0
- xarray_annotated/temporal/_config.py +182 -0
- xarray_annotated/temporal/_decorator.py +133 -0
- xarray_annotated/units/__init__.py +53 -0
- xarray_annotated/units/_annotations.py +169 -0
- xarray_annotated/units/_check.py +592 -0
- xarray_annotated/units/_config.py +237 -0
- xarray_annotated/units/_decorator.py +203 -0
- xarray_annotated/units/_registry.py +94 -0
- xarray_annotated-0.5.2.dist-info/METADATA +156 -0
- xarray_annotated-0.5.2.dist-info/RECORD +26 -0
- xarray_annotated-0.5.2.dist-info/WHEEL +4 -0
- xarray_annotated-0.5.2.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""xarray-annotated: validate xarray DataArray properties declared via ``typing.Annotated``.
|
|
2
|
+
|
|
3
|
+
Annotation is the unifying technology: a declared property is read off an
|
|
4
|
+
``Annotated[DataArray, ...]`` hint and validated at runtime. Each kind of
|
|
5
|
+
property is a domain subpackage, imported explicitly:
|
|
6
|
+
|
|
7
|
+
* ``xarray_annotated.units`` — physical units (pint / CF), the mature domain.
|
|
8
|
+
* ``xarray_annotated.schema`` — structural properties (dims, coords, dtype);
|
|
9
|
+
validate-only (never mutates).
|
|
10
|
+
* ``xarray_annotated.temporal`` — the frequency (and phase) of a time axis;
|
|
11
|
+
validate-only.
|
|
12
|
+
|
|
13
|
+
The top level is deliberately thin — nothing domain-specific is re-exported
|
|
14
|
+
here, so the domains never collide in a shared namespace::
|
|
15
|
+
|
|
16
|
+
from xarray_annotated import schema, temporal, units
|
|
17
|
+
from xarray_annotated.units import declare_units, check_units
|
|
18
|
+
from xarray_annotated.schema import declare_schema, Dims, Dtype
|
|
19
|
+
from xarray_annotated.temporal import declare_freq, Freq
|
|
20
|
+
|
|
21
|
+
The only names surfaced at the top level are domain-*agnostic* helpers that
|
|
22
|
+
belong to no single domain: the shared declaration *writer* ``annotate`` and its
|
|
23
|
+
inverse *reader* ``declarations_from_signature`` (which reads every facet declared
|
|
24
|
+
on a signature into one uniform ``Declared`` value), plus the ``Annotated``
|
|
25
|
+
introspection kernel (``unwrap_annotated``, ``walk_signature``).
|
|
26
|
+
``walk_signature`` is the shared driver behind every domain's reader, so a
|
|
27
|
+
third-party facet author can use it to build their own ``*_from_signature``
|
|
28
|
+
reader. No domain-specific name is re-exported here.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from importlib.metadata import version as _version
|
|
32
|
+
|
|
33
|
+
from . import schema, temporal, units
|
|
34
|
+
from ._annotations import unwrap_annotated, walk_signature
|
|
35
|
+
from ._reader import Declared, declarations_from_signature
|
|
36
|
+
from ._writer import annotate
|
|
37
|
+
|
|
38
|
+
__version__ = _version("xarray-annotated")
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
"Declared",
|
|
42
|
+
"__version__",
|
|
43
|
+
"annotate",
|
|
44
|
+
"declarations_from_signature",
|
|
45
|
+
"schema",
|
|
46
|
+
"temporal",
|
|
47
|
+
"units",
|
|
48
|
+
"unwrap_annotated",
|
|
49
|
+
"walk_signature",
|
|
50
|
+
]
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Shared `typing.Annotated` introspection for xarray-annotated.
|
|
2
|
+
|
|
3
|
+
Pure `typing` machinery with no dependency on any domain's registry or config —
|
|
4
|
+
the "declaration side" common to every domain. A domain supplies an *extractor*
|
|
5
|
+
that turns one `Annotated[...]` hint into its own declaration payload (a unit
|
|
6
|
+
string, a list of schema markers, …); `walk_signature` drives that extractor over
|
|
7
|
+
a whole function signature, handling the `TypedDict` / dataclass / single-value
|
|
8
|
+
return shapes once, for everyone.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import dataclasses
|
|
12
|
+
import types
|
|
13
|
+
from collections.abc import Callable
|
|
14
|
+
from typing import (
|
|
15
|
+
Annotated,
|
|
16
|
+
Any,
|
|
17
|
+
TypeVar,
|
|
18
|
+
Union,
|
|
19
|
+
get_args,
|
|
20
|
+
get_origin,
|
|
21
|
+
get_type_hints,
|
|
22
|
+
is_typeddict,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
import xarray as xr
|
|
26
|
+
|
|
27
|
+
_T = TypeVar("_T")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def unwrap_annotated(hint: Any) -> Any:
|
|
31
|
+
"""Return the underlying type of an `Annotated` hint, else the hint itself.
|
|
32
|
+
|
|
33
|
+
`Annotated[DataArray, ...]` → `DataArray`; a non-`Annotated` hint is returned
|
|
34
|
+
unchanged. Lets type comparisons (e.g. `t is DataArray`) see through the
|
|
35
|
+
metadata that declarations attach to parameters.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
hint: A type hint, possibly `Annotated`.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
The base type if `hint` is `Annotated`; otherwise `hint` unchanged.
|
|
42
|
+
"""
|
|
43
|
+
return get_args(hint)[0] if get_origin(hint) is Annotated else hint
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _is_dataarray_type(tp: Any) -> bool:
|
|
47
|
+
"""Return whether `tp` is `DataArray` (possibly wrapped in a Union).
|
|
48
|
+
|
|
49
|
+
Accepts a bare `DataArray` as well as `DataArray | None` /
|
|
50
|
+
`Optional[DataArray]` (and any other union that includes `DataArray`), so an
|
|
51
|
+
optional DataArray parameter can still carry a declaration. Anything else
|
|
52
|
+
(scalars, `str`, `bool`, `Dataset`, …) is not a declaration-bearing type.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
tp: A type annotation to inspect.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
`True` if `tp` is or contains `DataArray`.
|
|
59
|
+
"""
|
|
60
|
+
if tp is xr.DataArray:
|
|
61
|
+
return True
|
|
62
|
+
if get_origin(tp) in (Union, types.UnionType):
|
|
63
|
+
return any(_is_dataarray_type(arg) for arg in get_args(tp))
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def walk_signature(
|
|
68
|
+
func: object,
|
|
69
|
+
extract: Callable[[Any], _T | None],
|
|
70
|
+
) -> tuple[dict[str, _T], dict[str, _T] | _T | None]:
|
|
71
|
+
"""Extract per-parameter and return declarations from a function's hints.
|
|
72
|
+
|
|
73
|
+
Reads `get_type_hints(func, include_extras=True)` and maps each hint through
|
|
74
|
+
`extract`, which returns a domain's declaration payload for that hint or
|
|
75
|
+
`None` when the hint carries no declaration. The `is not None` filter is the
|
|
76
|
+
single "no declaration" rule shared by every domain.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
func: A callable whose parameters/return may carry `Annotated` metadata.
|
|
80
|
+
extract: Turns one hint into a declaration payload, or `None`. For units
|
|
81
|
+
this is `annotated_unit` (payload `str`); for schema `annotated_schema`
|
|
82
|
+
(payload `list[marker]`).
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
An `(inputs, output)` pair:
|
|
86
|
+
|
|
87
|
+
* `inputs` — `dict[str, T]` mapping each declared parameter to its payload.
|
|
88
|
+
* `output` — a `dict[str, T]` (per-field) if the return hint is a
|
|
89
|
+
`TypedDict` or dataclass; a single `T` if the return is one declared
|
|
90
|
+
`Annotated[DataArray, ...]`; `None` otherwise.
|
|
91
|
+
"""
|
|
92
|
+
hints = get_type_hints(func, include_extras=True)
|
|
93
|
+
ret = hints.pop("return", None)
|
|
94
|
+
|
|
95
|
+
inputs = {
|
|
96
|
+
name: payload
|
|
97
|
+
for name, hint in hints.items()
|
|
98
|
+
if (payload := extract(hint)) is not None
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
output: dict[str, _T] | _T | None
|
|
102
|
+
if is_typeddict(ret):
|
|
103
|
+
ret_hints = get_type_hints(ret, include_extras=True)
|
|
104
|
+
output = {
|
|
105
|
+
name: payload
|
|
106
|
+
for name, hint in ret_hints.items()
|
|
107
|
+
if (payload := extract(hint)) is not None
|
|
108
|
+
}
|
|
109
|
+
elif dataclasses.is_dataclass(ret):
|
|
110
|
+
ret_hints = get_type_hints(ret, include_extras=True)
|
|
111
|
+
output = {
|
|
112
|
+
f.name: payload
|
|
113
|
+
for f in dataclasses.fields(ret)
|
|
114
|
+
if (payload := extract(ret_hints.get(f.name))) is not None
|
|
115
|
+
}
|
|
116
|
+
else:
|
|
117
|
+
output = extract(ret)
|
|
118
|
+
|
|
119
|
+
return inputs, output
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Shared configuration kernel for xarray-annotated.
|
|
2
|
+
|
|
3
|
+
Machinery common to every annotation-validation domain (currently `units` and
|
|
4
|
+
`schema`), so the domains never drift on how a policy axis resolves:
|
|
5
|
+
|
|
6
|
+
- `_resolve` — the shared precedence for one axis: environment variable (parsed),
|
|
7
|
+
then a process override, then a module default.
|
|
8
|
+
- the **`enabled`** master switch — package-wide (it gates *every* domain, not just
|
|
9
|
+
one), so it lives here rather than in any single domain. Each domain's `Policy`
|
|
10
|
+
carries an `enabled` field but resolves it through `resolve_enabled` / the
|
|
11
|
+
override accessors below, so toggling it in one domain toggles it everywhere.
|
|
12
|
+
- `_Unset` / `_UNSET` — the sentinel that lets a `set_policy` keyword distinguish
|
|
13
|
+
"argument omitted" from "explicitly set to `None`" (which *clears* an override).
|
|
14
|
+
|
|
15
|
+
Domain-specific axes (units' `on_missing` / `on_inexact`, schema's `on_mismatch`)
|
|
16
|
+
stay in the respective domain's `_config.py`; only the genuinely shared pieces
|
|
17
|
+
live here.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import os
|
|
21
|
+
from collections.abc import Callable
|
|
22
|
+
from typing import TypeVar
|
|
23
|
+
|
|
24
|
+
#: Master switch default. Validation is on unless explicitly disabled.
|
|
25
|
+
DEFAULT_ENABLED: bool = True
|
|
26
|
+
|
|
27
|
+
#: Master switch is package-wide (gates every annotation-validation domain), so it
|
|
28
|
+
#: is deliberately un-namespaced; each domain's behavioural knobs are namespaced.
|
|
29
|
+
ENABLED_ENV_VAR = "XARRAY_ANNOTATED_ENABLED"
|
|
30
|
+
|
|
31
|
+
#: String values (lower-cased) accepted for the boolean `enabled` env var.
|
|
32
|
+
_TRUTHY: frozenset[str] = frozenset({"1", "true", "yes", "on"})
|
|
33
|
+
_FALSEY: frozenset[str] = frozenset({"0", "false", "no", "off"})
|
|
34
|
+
|
|
35
|
+
#: Process-wide override for the master switch, shared across all domains.
|
|
36
|
+
_process_enabled: bool | None = None
|
|
37
|
+
|
|
38
|
+
_T = TypeVar("_T")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class _Unset:
|
|
42
|
+
"""Sentinel distinguishing 'argument omitted' from 'explicitly set to None'."""
|
|
43
|
+
|
|
44
|
+
def __repr__(self) -> str:
|
|
45
|
+
return "<unset>"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
_UNSET = _Unset()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _resolve(
|
|
52
|
+
env_var: str,
|
|
53
|
+
process_value: _T | None,
|
|
54
|
+
default: _T,
|
|
55
|
+
parse: Callable[[str], _T],
|
|
56
|
+
) -> _T:
|
|
57
|
+
"""Resolve one axis: env var (parsed) → process override → default."""
|
|
58
|
+
env = os.environ.get(env_var)
|
|
59
|
+
if env:
|
|
60
|
+
return parse(env)
|
|
61
|
+
if process_value is not None:
|
|
62
|
+
return process_value
|
|
63
|
+
return default
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _parse_enabled_env(value: str) -> bool:
|
|
67
|
+
lowered = value.lower()
|
|
68
|
+
if lowered in _TRUTHY:
|
|
69
|
+
return True
|
|
70
|
+
if lowered in _FALSEY:
|
|
71
|
+
return False
|
|
72
|
+
raise ValueError(
|
|
73
|
+
f"Invalid {ENABLED_ENV_VAR} value {value!r}. "
|
|
74
|
+
f"Use one of {sorted(_TRUTHY | _FALSEY)}."
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def resolve_enabled() -> bool:
|
|
79
|
+
"""Resolve the package-wide master switch (env → process override → default)."""
|
|
80
|
+
return _resolve(
|
|
81
|
+
ENABLED_ENV_VAR, _process_enabled, DEFAULT_ENABLED, _parse_enabled_env
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def get_enabled_override() -> bool | None:
|
|
86
|
+
"""Return the current process override for the master switch (`None` if unset).
|
|
87
|
+
|
|
88
|
+
Used by each domain's `policy` context manager to save/restore the shared
|
|
89
|
+
switch around a temporary override.
|
|
90
|
+
"""
|
|
91
|
+
return _process_enabled
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def set_enabled_override(value: bool | None) -> None:
|
|
95
|
+
"""Set (or, with `None`, clear) the process override for the master switch.
|
|
96
|
+
|
|
97
|
+
Shared by every domain's `set_policy`, so disabling validation in one domain
|
|
98
|
+
disables it package-wide.
|
|
99
|
+
"""
|
|
100
|
+
global _process_enabled
|
|
101
|
+
_process_enabled = None if value is None else bool(value)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Reading *all* declared facets off a signature — the unified reader.
|
|
2
|
+
|
|
3
|
+
Every domain ships its own reader (`units_from_signature`, `schema_from_signature`,
|
|
4
|
+
`freq_from_signature`) returning a domain-shaped payload; this is the one
|
|
5
|
+
cross-domain reader that collapses them into a single uniform value. Where the
|
|
6
|
+
writer `annotate` turns facet values into an `Annotated` hint,
|
|
7
|
+
`declarations_from_signature` turns a whole signature back into a facet value per
|
|
8
|
+
declared DataArray — it is the read-side counterpart to `annotate`, and their
|
|
9
|
+
round-trip is exact.
|
|
10
|
+
|
|
11
|
+
The unified value `Declared` is deliberately *homogeneous*: every slot holds a
|
|
12
|
+
marker (`Unit` / `Dims` / `Dtype` / `Coords` / `Freq`) or `None`. Markers are the
|
|
13
|
+
package's public currency — the schema and temporal markers are load-bearing (they
|
|
14
|
+
carry strictness that has no bare-string form) — so a facet-generic consumer wants
|
|
15
|
+
one marker per slot rather than a mix of strings and objects. A bare-string unit
|
|
16
|
+
shorthand is normalised to a `Unit`, so the shape does not depend on how the unit
|
|
17
|
+
was spelled.
|
|
18
|
+
|
|
19
|
+
Like `annotate`, this is cross-domain — it assembles every domain's markers into one
|
|
20
|
+
value — so it lives at the package root rather than in any one domain.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from dataclasses import dataclass
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
from ._annotations import walk_signature
|
|
27
|
+
from .schema import Coords, Dims, Dtype
|
|
28
|
+
from .schema._annotations import annotated_schema
|
|
29
|
+
from .temporal import Freq
|
|
30
|
+
from .temporal._annotations import annotated_freq
|
|
31
|
+
from .units import Unit
|
|
32
|
+
from .units._annotations import annotated_unit
|
|
33
|
+
|
|
34
|
+
__all__ = ["Declared", "declarations_from_signature"]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True, slots=True)
|
|
38
|
+
class Declared:
|
|
39
|
+
"""Every facet declared on one DataArray hint, one marker (or `None`) per slot.
|
|
40
|
+
|
|
41
|
+
The uniform value produced by `declarations_from_signature`: a homogeneous
|
|
42
|
+
bag of markers, so facet-generic code can treat each slot identically. Each
|
|
43
|
+
slot holds the corresponding marker or `None` when that facet was not
|
|
44
|
+
declared; the unit is always a `Unit` (its bare-string shorthand is
|
|
45
|
+
normalised on read), so `.unit.unit` recovers the string.
|
|
46
|
+
|
|
47
|
+
The field order (`unit`, `dims`, `dtype`, `coords`, `freq`) matches `annotate`'s
|
|
48
|
+
keyword arguments, so `annotate(unit=d.unit, dims=d.dims, dtype=d.dtype,
|
|
49
|
+
coords=d.coords, freq=d.freq)` rebuilds the hint this was read from.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
unit: Unit | None = None
|
|
53
|
+
dims: Dims | None = None
|
|
54
|
+
dtype: Dtype | None = None
|
|
55
|
+
coords: Coords | None = None
|
|
56
|
+
freq: Freq | None = None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _annotated_declaration(hint: Any) -> Declared | None:
|
|
60
|
+
"""Combine the per-domain extractors into one `Declared`, or `None`.
|
|
61
|
+
|
|
62
|
+
Delegates to the existing `annotated_unit`, `annotated_schema` and
|
|
63
|
+
`annotated_freq` so the "is this an `Annotated[DataArray, ...]` hint?" logic is
|
|
64
|
+
not duplicated, then folds their payloads into a single homogeneous value.
|
|
65
|
+
Returns `None` when the hint declares no facet, which is `walk_signature`'s "no
|
|
66
|
+
declaration" signal.
|
|
67
|
+
"""
|
|
68
|
+
unit = annotated_unit(hint)
|
|
69
|
+
markers = annotated_schema(hint) or ()
|
|
70
|
+
dims = next((m for m in markers if isinstance(m, Dims)), None)
|
|
71
|
+
dtype = next((m for m in markers if isinstance(m, Dtype)), None)
|
|
72
|
+
coords = next((m for m in markers if isinstance(m, Coords)), None)
|
|
73
|
+
freq = annotated_freq(hint)
|
|
74
|
+
if all(facet is None for facet in (unit, dims, dtype, coords, freq)):
|
|
75
|
+
return None
|
|
76
|
+
return Declared(
|
|
77
|
+
unit=Unit(unit) if unit is not None else None,
|
|
78
|
+
dims=dims,
|
|
79
|
+
dtype=dtype,
|
|
80
|
+
coords=coords,
|
|
81
|
+
freq=freq,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def declarations_from_signature(
|
|
86
|
+
func: object,
|
|
87
|
+
) -> tuple[dict[str, Declared], dict[str, Declared] | Declared | None]:
|
|
88
|
+
"""Read every declared facet off a function's signature in one uniform shape.
|
|
89
|
+
|
|
90
|
+
The all-facets counterpart to `units_from_signature` / `schema_from_signature`:
|
|
91
|
+
a single reader whose payload is a `Declared` carrying the unit, dims, dtype,
|
|
92
|
+
and coords declared on a DataArray hint at once. A consumer that would
|
|
93
|
+
otherwise call both per-domain readers and merge their differently-shaped
|
|
94
|
+
results can call this once and route a uniform value instead.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
func: A callable whose parameters/return may carry
|
|
98
|
+
`Annotated[DataArray, ...]` metadata.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
An `(inputs, output)` pair, mirroring `walk_signature` with a `Declared`
|
|
102
|
+
payload:
|
|
103
|
+
|
|
104
|
+
* `inputs` — `dict[str, Declared]` mapping each declared parameter to its
|
|
105
|
+
facets. Parameters carrying no declaration are omitted.
|
|
106
|
+
* `output` — a `dict[str, Declared]` (per-field) for a `TypedDict` or
|
|
107
|
+
dataclass return, a single `Declared` for one declared DataArray
|
|
108
|
+
return, or `None` when the return carries no declaration.
|
|
109
|
+
"""
|
|
110
|
+
return walk_signature(func, _annotated_declaration)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Writing declarations *onto* an annotation — the inverse of the readers.
|
|
2
|
+
|
|
3
|
+
Every domain ships a reader (`Annotated → declaration`: `units_from_signature`,
|
|
4
|
+
`schema_from_signature`, `freq_from_signature`); this is the one shared *writer*
|
|
5
|
+
(`declaration → Annotated`). A tool that builds functions dynamically (codegen,
|
|
6
|
+
graph nodes) needs to stamp a declared contract onto a generated signature;
|
|
7
|
+
`annotate` returns a real `Annotated` object it can assign to
|
|
8
|
+
`fn.__annotations__["return"]`, so the ordinary `declare_units` / `declare_schema` /
|
|
9
|
+
`declare_freq` decorators then read it straight back.
|
|
10
|
+
|
|
11
|
+
It is deliberately cross-domain — it assembles the units `Unit` marker, the schema
|
|
12
|
+
`Dims` / `Dtype` / `Coords` markers, and the temporal `Freq` marker — so it lives at
|
|
13
|
+
the package root rather than in any one domain, next to the domain-agnostic
|
|
14
|
+
annotation helpers.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from collections.abc import Iterable
|
|
18
|
+
from typing import Annotated, Any
|
|
19
|
+
|
|
20
|
+
import xarray as xr
|
|
21
|
+
|
|
22
|
+
from .schema import Coords, Dims, Dtype
|
|
23
|
+
from .temporal import Freq
|
|
24
|
+
from .units import Unit
|
|
25
|
+
|
|
26
|
+
__all__ = ["annotate"]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def annotate(
|
|
30
|
+
base: Any = xr.DataArray,
|
|
31
|
+
*,
|
|
32
|
+
unit: str | Unit | None = None,
|
|
33
|
+
dims: Iterable[str] | Dims | None = None,
|
|
34
|
+
dtype: str | Dtype | None = None,
|
|
35
|
+
coords: Iterable[str] | Coords | None = None,
|
|
36
|
+
freq: str | Freq | None = None,
|
|
37
|
+
) -> Any:
|
|
38
|
+
"""Build an `Annotated[base, <markers>]` hint from declared facet values.
|
|
39
|
+
|
|
40
|
+
The inverse of the `*_from_signature` readers: given facet values it returns a
|
|
41
|
+
real `Annotated` object carrying the corresponding markers, in a fixed order
|
|
42
|
+
(unit, dims, dtype, coords, freq). Assign it to a function's return/parameter
|
|
43
|
+
annotation and the `declare_units` / `declare_schema` / `declare_freq` decorators
|
|
44
|
+
read it back exactly as they would a hand-written one.
|
|
45
|
+
|
|
46
|
+
Each facet accepts either a raw value or an already-built marker, so a caller
|
|
47
|
+
holding a mix (e.g. a `Unit` object but bare dim-name tuples) can pass both
|
|
48
|
+
without unwrapping:
|
|
49
|
+
|
|
50
|
+
* `unit` — a unit string (`"Pa"`) or a `Unit`.
|
|
51
|
+
* `dims` — an iterable of dim names (`("time", "x")`) or a `Dims`.
|
|
52
|
+
* `dtype` — a dtype string (`"float64"`) or a `Dtype`.
|
|
53
|
+
* `coords` — an iterable of coord names or a `Coords`.
|
|
54
|
+
* `freq` — an offset string (`"7D"`) or a `Freq`.
|
|
55
|
+
|
|
56
|
+
A facet left as `None` contributes no marker. When no facet is given, `base`
|
|
57
|
+
is returned unchanged (no `Annotated` wrapper), so `annotate()` is a safe
|
|
58
|
+
no-op default.
|
|
59
|
+
|
|
60
|
+
Note the `freq` string is a *writer* convenience only: there is no bare-string
|
|
61
|
+
shorthand on the read side, so a frequency must be spelled as a `Freq` marker in
|
|
62
|
+
a hand-written annotation.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
base: The base type to annotate (default `xarray.DataArray`).
|
|
66
|
+
unit: Declared unit, or `None`.
|
|
67
|
+
dims: Declared dimensions, or `None`.
|
|
68
|
+
dtype: Declared dtype, or `None`.
|
|
69
|
+
coords: Declared coordinates, or `None`.
|
|
70
|
+
freq: Declared temporal frequency, or `None`.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
`Annotated[base, <markers>]` if any facet was given; otherwise `base`.
|
|
74
|
+
|
|
75
|
+
Examples:
|
|
76
|
+
>>> from typing import Annotated, get_args, get_origin
|
|
77
|
+
>>> import xarray as xr
|
|
78
|
+
>>> from xarray_annotated import annotate
|
|
79
|
+
>>> from xarray_annotated.units import Unit
|
|
80
|
+
>>> hint = annotate(unit="Pa", dims=("time", "x"), dtype="float64")
|
|
81
|
+
>>> get_origin(hint) is Annotated
|
|
82
|
+
True
|
|
83
|
+
>>> Unit("Pa") in get_args(hint)
|
|
84
|
+
True
|
|
85
|
+
>>> annotate() is xr.DataArray
|
|
86
|
+
True
|
|
87
|
+
"""
|
|
88
|
+
markers: list[Any] = []
|
|
89
|
+
if unit is not None:
|
|
90
|
+
markers.append(unit if isinstance(unit, Unit) else Unit(unit))
|
|
91
|
+
if dims is not None:
|
|
92
|
+
markers.append(dims if isinstance(dims, Dims) else Dims(*dims))
|
|
93
|
+
if dtype is not None:
|
|
94
|
+
markers.append(dtype if isinstance(dtype, Dtype) else Dtype(dtype))
|
|
95
|
+
if coords is not None:
|
|
96
|
+
markers.append(coords if isinstance(coords, Coords) else Coords(*coords))
|
|
97
|
+
if freq is not None:
|
|
98
|
+
markers.append(freq if isinstance(freq, Freq) else Freq(freq))
|
|
99
|
+
return Annotated[(base, *markers)] if markers else base
|
|
File without changes
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Structural-property domain for xarray-annotated.
|
|
2
|
+
|
|
3
|
+
Declares and validates a DataArray's *structural* properties — the ones every
|
|
4
|
+
DataArray possesses regardless of physical units: its dimensions (``Dims``),
|
|
5
|
+
coordinates (``Coords``), and dtype (``Dtype``). The structural counterpart to
|
|
6
|
+
``xarray_annotated.units``, but simpler: structural validation only *asserts* (it
|
|
7
|
+
never converts or mutates).
|
|
8
|
+
|
|
9
|
+
Declare several at once inside ``Annotated`` and apply them with ``declare_schema``::
|
|
10
|
+
|
|
11
|
+
from typing import Annotated
|
|
12
|
+
import xarray as xr
|
|
13
|
+
from xarray_annotated.schema import declare_schema, Dims, Dtype
|
|
14
|
+
|
|
15
|
+
@declare_schema
|
|
16
|
+
def f(x: Annotated[xr.DataArray, Dims("time", "x"), Dtype("float64")]) -> xr.DataArray:
|
|
17
|
+
...
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from ._annotations import (
|
|
21
|
+
Coords,
|
|
22
|
+
Dims,
|
|
23
|
+
Dtype,
|
|
24
|
+
annotated_schema,
|
|
25
|
+
schema_from_signature,
|
|
26
|
+
)
|
|
27
|
+
from ._check import (
|
|
28
|
+
SchemaError,
|
|
29
|
+
SchemaWarning,
|
|
30
|
+
assert_valid_schema,
|
|
31
|
+
check_schema,
|
|
32
|
+
dims_compatible,
|
|
33
|
+
dtype_compatible,
|
|
34
|
+
)
|
|
35
|
+
from ._config import (
|
|
36
|
+
OnMismatch,
|
|
37
|
+
Policy,
|
|
38
|
+
get_policy,
|
|
39
|
+
policy,
|
|
40
|
+
set_policy,
|
|
41
|
+
)
|
|
42
|
+
from ._decorator import declare_schema
|
|
43
|
+
|
|
44
|
+
__all__ = [
|
|
45
|
+
"Coords",
|
|
46
|
+
"Dims",
|
|
47
|
+
"Dtype",
|
|
48
|
+
"OnMismatch",
|
|
49
|
+
"Policy",
|
|
50
|
+
"SchemaError",
|
|
51
|
+
"SchemaWarning",
|
|
52
|
+
"annotated_schema",
|
|
53
|
+
"assert_valid_schema",
|
|
54
|
+
"check_schema",
|
|
55
|
+
"declare_schema",
|
|
56
|
+
"dims_compatible",
|
|
57
|
+
"dtype_compatible",
|
|
58
|
+
"get_policy",
|
|
59
|
+
"policy",
|
|
60
|
+
"schema_from_signature",
|
|
61
|
+
"set_policy",
|
|
62
|
+
]
|