Reading and writing declarations (advanced)¶
You probably don’t need this page
@declare_schema, @declare_units and @declare_freq are the recommended entry
point, and most users need nothing else. Everything below is for building tooling on
top of the declarations — skip it unless you are.
The primitives the decorators are built from are public too, for code that needs to validate an array by hand, or to inspect declarations without calling anything — build-time graph checks, documentation generation, code generation, custom consumers.
This is the API that makes a declaration useful to something other than the function it sits on.
Validating by hand¶
Use these where a decorator doesn’t fit: inside a loop, at a boundary you don’t own, or on an array that arrived from somewhere other than a parameter.
check_schema¶
Validates a single array against a marker or list of markers and returns it
unchanged (or raises SchemaError):
from xarray_annotated.schema import check_schema, Dims
check_schema(da, Dims("time", "x"), name="da", on_mismatch=None, qualname=None)
on_mismatch defaults to the active policy when None;
name labels the array in messages; it is a total no-op when the policy is disabled.
check_units¶
Validates and converts a single array:
from xarray_annotated.units import check_units
check_units(da, declared, name, on_missing=None, on_inexact=None, qualname=None)
Given an input da, check_units:
- reads
da.attrs["units"]; - if present and parseable, converts
datodeclaredand re-stampsattrs["units"] = declaredon the result; - if missing or unparseable, follows the
on_missingaxis; - if present but dimensionally incompatible with
declared(e.g."kg"where"Pa"is declared), raisespint.DimensionalityErrornaming the offending variable — always, regardless of policy.
on_missing and on_inexact may be passed per call; each defaults to the active policy
when None.
apply_output_units¶
The output counterpart, and deliberately not symmetric — it stamps rather than converts:
from xarray_annotated.units import apply_output_units
apply_output_units(da, declared, name, on_output=None, qualname=None)
Sets attrs["units"] = declared on da and returns it. Under
on_output="strict" a label that is present, parseable, and
different from declared raises instead. An absent, over-long, or unparseable label is
always stamped — absence is not evidence of a mismatch.
Reach for this when you are applying a declaration you read off a signature yourself.
Always use the return value. For an attrs-labelled array it stamps in place and hands
back the same object, but a pint-quantified one is
converted rather than stamped, so what comes back may be a new array.
check_freq¶
Validates a single array’s time axis and returns it unchanged (or raises
FreqError), taking the same shape of arguments:
from xarray_annotated.temporal import check_freq, Freq
check_freq(
da, Freq("7D"), name="da", on_mismatch=None, on_uninferable=None, qualname=None
)
Checking a declaration itself¶
assert_valid_unit(unit, context) / assert_valid_schema(marker, context) /
assert_valid_freq(marker, context) provide the same fail-fast declaration checks the
decorators run at import — useful if you are building markers dynamically and want the
typo caught where it was made.
Reading declarations off a signature¶
These extract a function’s declared properties without calling it — the single source that both the decorators and any static checker consume, so a declaration is never written twice.
units_from_signature¶
from typing import Annotated, TypedDict
import xarray as xr
from xarray_annotated.units import units_from_signature
class Output(TypedDict):
gpp: Annotated[xr.DataArray, "g m-2 d-1"]
lue: Annotated[xr.DataArray, "g MJ-1"]
def node(
temp: Annotated[xr.DataArray, "degC"],
plain: xr.DataArray,
) -> Output: ...
inputs, outputs = units_from_signature(node)
# inputs == {"temp": "degC"}
# outputs == {"gpp": "g m-2 d-1", "lue": "g MJ-1"}
Only parameters — or fields of a TypedDict/dataclass return type — with a
unit-annotated DataArray contribute; a plain xr.DataArray hint with no unit is
ignored. A bare Annotated[DataArray, unit] return annotation yields a single unit
string rather than a dict.
schema_from_signature¶
Mirrors it, returning the list of markers on each parameter/field (since a hint may declare several):
from typing import Annotated
import xarray as xr
from xarray_annotated.schema import schema_from_signature, Dims, Dtype
def node(
x: Annotated[xr.DataArray, Dims("time", "x"), Dtype("float64")],
plain: xr.DataArray,
) -> Annotated[xr.DataArray, Dims("time", "x")]: ...
inputs, output = schema_from_signature(node)
# inputs == {"x": [Dims("time", "x"), Dtype("float64")]}
# output == [Dims("time", "x")]
TypedDict/dataclass returns are read per-field, exactly as for units.
freq_from_signature does the same for the Freq marker (one marker, or None, per
parameter).
Reading a single hint¶
Each domain also exposes the per-annotation reader underneath its signature reader, for when you already hold one hint and don’t want to reach into a private module:
from typing import Annotated
import xarray as xr
from xarray_annotated.units import Unit, annotated_unit
from xarray_annotated.schema import Dims, annotated_schema
from xarray_annotated.temporal import Freq, annotated_freq
hint = Annotated[xr.DataArray, Unit("Pa"), Dims("time"), Freq("D")]
annotated_unit(hint) # "Pa" (normalises the bare-string shorthand too)
annotated_schema(hint) # [Dims("time")]
annotated_freq(hint) # Freq("D")
Each returns None when the hint declares nothing for that domain, isn’t Annotated, or
annotates a non-DataArray base type. A wrapper exposing the wrapped class as
array_type — such as a jaxtyping array type — counts as a DataArray base type; see
Using with jaxtyping. These are the building blocks the *_from_signature
readers are driven from, alongside walk_signature and unwrap_annotated at the package
root — the combination a third-party facet author needs to write their own reader.
Cross-domain reader: declarations_from_signature¶
declarations_from_signature (from the package root) reads all declared facets — unit, dims,
dtype, coords, and freq — into a single uniform Declared value per parameter. This is the
read-side counterpart to annotate (below), and their round-trip is exact:
from typing import Annotated
import xarray as xr
from xarray_annotated import declarations_from_signature
from xarray_annotated.schema import Dims, Dtype
from xarray_annotated.units import Unit
def node(
x: Annotated[xr.DataArray, Dims("time", "x"), Dtype("float64"), Unit("degC")],
) -> Annotated[xr.DataArray, Dims("time", "x")]: ...
inputs, output = declarations_from_signature(node)
# inputs == {"x": Declared(dims=Dims("time", "x"), dtype=Dtype("float64"), unit=Unit("degC"))}
# output == Declared(dims=Dims("time", "x"))
A bare-string unit shorthand is normalised to a Unit marker on read, so .unit.unit always
recovers the string. Parameters with no declared facet are omitted entirely.
This is the reader to use when checking a graph rather than a call: read each node’s
declared inputs and outputs, compare them pairwise, and fail at assembly time rather than
at run time. freq_compatible (see
What “the same frequency” means) performs
the frequency half of that comparison with no array in hand.
Writing annotations programmatically: annotate¶
annotate (from the package root) is the inverse of the readers: given facet values it returns a
real Annotated hint — useful for code generation or tools that build function signatures
dynamically:
from typing import Annotated, get_args, get_origin
import xarray as xr
from xarray_annotated import annotate
hint = annotate(unit="Pa", dims=("time", "x"), dtype="float64", freq="7D")
# Annotated[xr.DataArray, Unit("Pa"), Dims("time", "x"), Dtype("float64"), Freq("7D")]
annotate() is xr.DataArray # no-op when no facets given
Each facet accepts either a raw value or an already-built marker, so a caller holding a mix can pass both without unwrapping:
Assign the result to a function’s __annotations__ and the @declare_units /
@declare_schema / @declare_freq decorators read it back exactly as if it were
hand-written.
The first positional argument is the base type, which defaults to xr.DataArray. Pass a
jaxtyping array type there to keep dtype/shape checking on the generated hint —
annotate(Float[xr.DataArray, "time x"], unit="Pa") round-trips unchanged.