xarray-annotated 0.5.2__tar.gz → 0.6.0__tar.gz

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.
Files changed (28) hide show
  1. {xarray_annotated-0.5.2 → xarray_annotated-0.6.0}/PKG-INFO +5 -1
  2. {xarray_annotated-0.5.2 → xarray_annotated-0.6.0}/README.md +4 -0
  3. {xarray_annotated-0.5.2 → xarray_annotated-0.6.0}/pyproject.toml +39 -3
  4. xarray_annotated-0.6.0/pyproject.toml.orig +141 -0
  5. {xarray_annotated-0.5.2 → xarray_annotated-0.6.0}/src/xarray_annotated/__init__.py +1 -1
  6. {xarray_annotated-0.5.2 → xarray_annotated-0.6.0}/src/xarray_annotated/_annotations.py +16 -9
  7. {xarray_annotated-0.5.2 → xarray_annotated-0.6.0}/src/xarray_annotated/_config.py +5 -8
  8. {xarray_annotated-0.5.2 → xarray_annotated-0.6.0}/src/xarray_annotated/schema/__init__.py +3 -2
  9. {xarray_annotated-0.5.2 → xarray_annotated-0.6.0}/src/xarray_annotated/schema/_annotations.py +1 -1
  10. {xarray_annotated-0.5.2 → xarray_annotated-0.6.0}/src/xarray_annotated/schema/_config.py +5 -5
  11. {xarray_annotated-0.5.2 → xarray_annotated-0.6.0}/src/xarray_annotated/schema/_decorator.py +4 -3
  12. {xarray_annotated-0.5.2 → xarray_annotated-0.6.0}/src/xarray_annotated/temporal/__init__.py +3 -1
  13. {xarray_annotated-0.5.2 → xarray_annotated-0.6.0}/src/xarray_annotated/temporal/_annotations.py +1 -1
  14. {xarray_annotated-0.5.2 → xarray_annotated-0.6.0}/src/xarray_annotated/temporal/_config.py +7 -7
  15. {xarray_annotated-0.5.2 → xarray_annotated-0.6.0}/src/xarray_annotated/units/_check.py +7 -7
  16. {xarray_annotated-0.5.2 → xarray_annotated-0.6.0}/src/xarray_annotated/units/_config.py +9 -9
  17. {xarray_annotated-0.5.2 → xarray_annotated-0.6.0}/src/xarray_annotated/units/_decorator.py +2 -1
  18. xarray_annotated-0.5.2/pyproject.toml.orig +0 -81
  19. {xarray_annotated-0.5.2 → xarray_annotated-0.6.0}/LICENSE +0 -0
  20. {xarray_annotated-0.5.2 → xarray_annotated-0.6.0}/src/xarray_annotated/_reader.py +0 -0
  21. {xarray_annotated-0.5.2 → xarray_annotated-0.6.0}/src/xarray_annotated/_writer.py +0 -0
  22. {xarray_annotated-0.5.2 → xarray_annotated-0.6.0}/src/xarray_annotated/py.typed +0 -0
  23. {xarray_annotated-0.5.2 → xarray_annotated-0.6.0}/src/xarray_annotated/schema/_check.py +0 -0
  24. {xarray_annotated-0.5.2 → xarray_annotated-0.6.0}/src/xarray_annotated/temporal/_check.py +0 -0
  25. {xarray_annotated-0.5.2 → xarray_annotated-0.6.0}/src/xarray_annotated/temporal/_decorator.py +0 -0
  26. {xarray_annotated-0.5.2 → xarray_annotated-0.6.0}/src/xarray_annotated/units/__init__.py +0 -0
  27. {xarray_annotated-0.5.2 → xarray_annotated-0.6.0}/src/xarray_annotated/units/_annotations.py +0 -0
  28. {xarray_annotated-0.5.2 → xarray_annotated-0.6.0}/src/xarray_annotated/units/_registry.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: xarray-annotated
3
- Version: 0.5.2
3
+ Version: 0.6.0
4
4
  Summary: Run-time validation of xarray DataArray properties (units, dims/coords/dtype) declared via typing.Annotated
5
5
  Keywords: xarray,pint,units,validation,annotations
6
6
  Author: jmarshrossney
@@ -78,10 +78,12 @@ It assumes a unit (pascals) and says so in the docstring but doesn't check or en
78
78
  ```python
79
79
  import xarray as xr
80
80
 
81
+
81
82
  def altitude(p: xr.DataArray) -> xr.DataArray:
82
83
  """Barometric altitude. Assumes p is in Pa."""
83
84
  return 44330.0 * (1.0 - (p / 101325.0) ** 0.1903)
84
85
 
86
+
85
87
  p = xr.DataArray([1013.0, 1000.0], dims=["time"], attrs={"units": "hPa"})
86
88
  altitude(p)
87
89
  ```
@@ -104,6 +106,7 @@ import xarray as xr
104
106
  from xarray_annotated.schema import declare_schema, Dims
105
107
  from xarray_annotated.units import declare_units, Unit
106
108
 
109
+
107
110
  @declare_units
108
111
  @declare_schema
109
112
  def altitude(
@@ -112,6 +115,7 @@ def altitude(
112
115
  """Barometric altitude."""
113
116
  return 44330.0 * (1.0 - (p / 101325.0) ** 0.1903)
114
117
 
118
+
115
119
  p = xr.DataArray([1013.0, 1000.0], dims=["time"], attrs={"units": "hPa"})
116
120
  altitude(p)
117
121
  ```
@@ -51,10 +51,12 @@ It assumes a unit (pascals) and says so in the docstring but doesn't check or en
51
51
  ```python
52
52
  import xarray as xr
53
53
 
54
+
54
55
  def altitude(p: xr.DataArray) -> xr.DataArray:
55
56
  """Barometric altitude. Assumes p is in Pa."""
56
57
  return 44330.0 * (1.0 - (p / 101325.0) ** 0.1903)
57
58
 
59
+
58
60
  p = xr.DataArray([1013.0, 1000.0], dims=["time"], attrs={"units": "hPa"})
59
61
  altitude(p)
60
62
  ```
@@ -77,6 +79,7 @@ import xarray as xr
77
79
  from xarray_annotated.schema import declare_schema, Dims
78
80
  from xarray_annotated.units import declare_units, Unit
79
81
 
82
+
80
83
  @declare_units
81
84
  @declare_schema
82
85
  def altitude(
@@ -85,6 +88,7 @@ def altitude(
85
88
  """Barometric altitude."""
86
89
  return 44330.0 * (1.0 - (p / 101325.0) ** 0.1903)
87
90
 
91
+
88
92
  p = xr.DataArray([1013.0, 1000.0], dims=["time"], attrs={"units": "hPa"})
89
93
  altitude(p)
90
94
  ```
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "xarray-annotated"
7
- version = "0.5.2"
7
+ version = "0.6.0"
8
8
  description = "Run-time validation of xarray DataArray properties (units, dims/coords/dtype) declared via typing.Annotated"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -56,6 +56,11 @@ dev = [
56
56
  "rust-just>=1.46.0",
57
57
  "pre-commit>=4.6.0",
58
58
  { include-group = "docs" },
59
+ { include-group = "integrations" },
60
+ ]
61
+ integrations = [
62
+ "beartype>=0.22.9",
63
+ "jaxtyping>=0.3.11",
59
64
  ]
60
65
  docs = [
61
66
  "marimo>=0.23.11",
@@ -80,12 +85,43 @@ line-length = 88
80
85
  indent-width = 4
81
86
  target-version = "py312"
82
87
 
88
+ [tool.ruff.lint]
89
+ select = [
90
+ "E",
91
+ "D",
92
+ "W",
93
+ "F",
94
+ "UP",
95
+ "B",
96
+ "SIM",
97
+ "RUF",
98
+ "PT",
99
+ "I",
100
+ ]
101
+
83
102
  [tool.ruff.lint.per-file-ignores]
84
- "examples/notebook.py" = [
85
- "F821",
103
+ "tests/**" = [
104
+ "B008",
105
+ "D",
106
+ "E501",
107
+ ]
108
+ "examples/**" = [
109
+ "B018",
110
+ "D100",
111
+ "D103",
112
+ "E501",
86
113
  "F722",
114
+ "F821",
115
+ "F841",
116
+ "RUF001",
117
+ "RUF003",
118
+ "PLR1711",
119
+ "PLC0415",
87
120
  ]
88
121
 
122
+ [tool.ruff.lint.pydocstyle]
123
+ convention = "google"
124
+
89
125
  [tool.uv]
90
126
  exclude-newer = "1 week"
91
127
 
@@ -0,0 +1,141 @@
1
+ [build-system]
2
+ requires = ["uv_build>=0.11.17,<0.12.0"]
3
+ build-backend = "uv_build"
4
+
5
+ [project]
6
+ name = "xarray-annotated"
7
+ version = "0.6.0"
8
+ description = "Run-time validation of xarray DataArray properties (units, dims/coords/dtype) declared via typing.Annotated"
9
+ authors = [
10
+ { name = "jmarshrossney", email = "17361029+jmarshrossney@users.noreply.github.com" }
11
+ ]
12
+ readme = "README.md"
13
+ license = "MIT"
14
+ license-files = ["LICENSE"]
15
+ requires-python = ">=3.12"
16
+ dependencies = [
17
+ "pandas>=2.2",
18
+ "pint>=0.25.3",
19
+ "pint-xarray>=0.6.1",
20
+ "xarray>=2026.2.0",
21
+ ]
22
+ keywords = ["xarray", "pint", "units", "validation", "annotations"]
23
+ classifiers = [
24
+ "Development Status :: 3 - Alpha",
25
+ "Intended Audience :: Science/Research",
26
+ "Programming Language :: Python :: 3.12",
27
+ "Programming Language :: Python :: 3.13",
28
+ "Programming Language :: Python :: 3.14",
29
+ "Topic :: Scientific/Engineering",
30
+ ]
31
+
32
+ [project.optional-dependencies]
33
+ cf = ["cf-xarray>=0.11.3"]
34
+
35
+ [project.urls]
36
+ Homepage = "https://jmarshrossney.github.io/xarray-annotated"
37
+ Repository = "https://github.com/jmarshrossney/xarray-annotated"
38
+ Documentation = "https://jmarshrossney.github.io/xarray-annotated"
39
+
40
+ [dependency-groups]
41
+ dev = [
42
+ "xarray-annotated[cf]",
43
+ "cftime>=1.6",
44
+ "coverage>=7.13.5",
45
+ "pyright>=1.1.409",
46
+ "pytest>=9.0.2",
47
+ "pytest-cov>=7.1.0",
48
+ "ruff>=0.15.4",
49
+ "rust-just>=1.46.0",
50
+ "pre-commit>=4.6.0",
51
+ {include-group = "docs"},
52
+ {include-group = "integrations"},
53
+ ]
54
+ # Third-party libraries xarray-annotated *composes with* but never imports. These are
55
+ # test-only: there is nothing for a user to install to "enable" the integration, so this
56
+ # is deliberately a dependency group and not an optional-dependency extra (contrast
57
+ # `cf`, which is an extra because `units._registry` really does import cf_xarray).
58
+ # `tests/test_jaxtyping.py` skips itself when they are absent.
59
+ integrations = [
60
+ "beartype>=0.22.9",
61
+ "jaxtyping>=0.3.11",
62
+ ]
63
+ docs = [
64
+ "marimo>=0.23.11",
65
+ "marimo-md-export>=0.7.0",
66
+ "mkdocstrings-python>=2.0.5",
67
+ "zensical>=0.0.46",
68
+ ]
69
+
70
+ [tool.pytest]
71
+ testpaths = ["tests"]
72
+
73
+ [tool.pyright]
74
+ venvPath = "."
75
+ venv = ".venv"
76
+ include = ["src/", "tests/"]
77
+
78
+ [tool.ruff]
79
+ line-length = 88
80
+ indent-width = 4
81
+ target-version = "py312"
82
+
83
+ [tool.ruff.lint]
84
+ # Ruff broadened its default rule set in 0.16, so always select explicitly:
85
+ # that way a version bump cannot change which rules apply.
86
+ select = [
87
+ "E", # pycodestyle errors
88
+ "D", # pydocstyle
89
+ "W", # pycodestyle warnings
90
+ "F", # pyflakes
91
+ "UP", # pyupgrade
92
+ "B", # flake8-bugbear
93
+ "SIM", # flake8-simplify
94
+ "RUF", # ruff-specific rules
95
+ "PT", # flake8-pytest-style
96
+ "I", # isort
97
+ ]
98
+
99
+ [tool.ruff.lint.per-file-ignores]
100
+ # Exempt tests from docstring requirements. B008 (function call in an argument
101
+ # default) is exempted because several tests exist precisely to check that a
102
+ # parameter left at its default is still converted/validated -- the call in the
103
+ # default is the fixture under test, not an oversight.
104
+ "tests/**" = ["B008", "D", "E501"]
105
+ # marimo threads `Annotated` in as a cell argument, so ruff can't resolve it to
106
+ # `typing.Annotated` and misreads the unit strings in the metadata as forward
107
+ # references (F821 "undefined name" / F722 "syntax error in forward annotation").
108
+ # The rest are generic to marimo notebooks, which are generated dataflow
109
+ # descriptions rather than hand-written modules:
110
+ # B018 a bare trailing expression IS the cell's output mechanism.
111
+ # E501 long prose/URLs inside mo.md() cannot be rewrapped by ruff format,
112
+ # and a `# noqa` after the closing quotes flip-flops forever
113
+ # (marimo strips the two spaces, ruff format restores them).
114
+ # D100 notebooks are not modules and have no module docstring.
115
+ # D103 cell functions are machine-generated and undocumented.
116
+ # F841 exploratory cells leave unused variables around.
117
+ # RUF001 prose contains en dashes; RUF003 contains Greek letters.
118
+ "examples/**" = [
119
+ "B018",
120
+ "D100",
121
+ "D103",
122
+ "E501",
123
+ "F722",
124
+ "F821",
125
+ "F841",
126
+ "RUF001",
127
+ "RUF003",
128
+ # Pre-emptive, and only active if "PL" is ever added to select above:
129
+ # PLR1711 ruff deletes the bare `return` that ends every cell and
130
+ # `marimo check --fix` re-adds it -- an infinite flip-flop.
131
+ # PLC0415 notebook imports live inside cells by design.
132
+ "PLR1711",
133
+ "PLC0415",
134
+ ]
135
+
136
+ [tool.ruff.lint.pydocstyle]
137
+ convention = "google"
138
+
139
+ [tool.uv]
140
+ exclude-newer = "1 week"
141
+ exclude-newer-package = { marimo-md-export = false }
@@ -1,4 +1,4 @@
1
- """xarray-annotated: validate xarray DataArray properties declared via ``typing.Annotated``.
1
+ """xarray-annotated: validate DataArray properties declared via ``typing.Annotated``.
2
2
 
3
3
  Annotation is the unifying technology: a declared property is read off an
4
4
  ``Annotated[DataArray, ...]`` hint and validated at runtime. Each kind of
@@ -14,7 +14,6 @@ from collections.abc import Callable
14
14
  from typing import (
15
15
  Annotated,
16
16
  Any,
17
- TypeVar,
18
17
  Union,
19
18
  get_args,
20
19
  get_origin,
@@ -24,8 +23,6 @@ from typing import (
24
23
 
25
24
  import xarray as xr
26
25
 
27
- _T = TypeVar("_T")
28
-
29
26
 
30
27
  def unwrap_annotated(hint: Any) -> Any:
31
28
  """Return the underlying type of an `Annotated` hint, else the hint itself.
@@ -44,30 +41,40 @@ def unwrap_annotated(hint: Any) -> Any:
44
41
 
45
42
 
46
43
  def _is_dataarray_type(tp: Any) -> bool:
47
- """Return whether `tp` is `DataArray` (possibly wrapped in a Union).
44
+ """Return whether `tp` is `DataArray` (possibly wrapped or in a Union).
48
45
 
49
46
  Accepts a bare `DataArray` as well as `DataArray | None` /
50
47
  `Optional[DataArray]` (and any other union that includes `DataArray`), so an
51
48
  optional DataArray parameter can still carry a declaration. Anything else
52
49
  (scalars, `str`, `bool`, `Dataset`, …) is not a declaration-bearing type.
53
50
 
51
+ Also accepts any *array-type wrapper* that exposes the underlying array class
52
+ as an `array_type` attribute — notably jaxtyping's `Float[DataArray, "t x"]`,
53
+ which occupies the base-type slot of an `Annotated` hint rather than competing
54
+ with it. This is a duck-typed check: jaxtyping is never imported, and any
55
+ other library following the same convention composes for free. See
56
+ `docs/guides/jaxtyping.md`.
57
+
54
58
  Args:
55
59
  tp: A type annotation to inspect.
56
60
 
57
61
  Returns:
58
- `True` if `tp` is or contains `DataArray`.
62
+ `True` if `tp` is, wraps, or contains `DataArray`.
59
63
  """
60
64
  if tp is xr.DataArray:
61
65
  return True
66
+ inner = getattr(tp, "array_type", None)
67
+ if isinstance(inner, type) and issubclass(inner, xr.DataArray):
68
+ return True
62
69
  if get_origin(tp) in (Union, types.UnionType):
63
70
  return any(_is_dataarray_type(arg) for arg in get_args(tp))
64
71
  return False
65
72
 
66
73
 
67
- def walk_signature(
74
+ def walk_signature[T](
68
75
  func: object,
69
- extract: Callable[[Any], _T | None],
70
- ) -> tuple[dict[str, _T], dict[str, _T] | _T | None]:
76
+ extract: Callable[[Any], T | None],
77
+ ) -> tuple[dict[str, T], dict[str, T] | T | None]:
71
78
  """Extract per-parameter and return declarations from a function's hints.
72
79
 
73
80
  Reads `get_type_hints(func, include_extras=True)` and maps each hint through
@@ -98,7 +105,7 @@ def walk_signature(
98
105
  if (payload := extract(hint)) is not None
99
106
  }
100
107
 
101
- output: dict[str, _T] | _T | None
108
+ output: dict[str, T] | T | None
102
109
  if is_typeddict(ret):
103
110
  ret_hints = get_type_hints(ret, include_extras=True)
104
111
  output = {
@@ -19,7 +19,6 @@ live here.
19
19
 
20
20
  import os
21
21
  from collections.abc import Callable
22
- from typing import TypeVar
23
22
 
24
23
  #: Master switch default. Validation is on unless explicitly disabled.
25
24
  DEFAULT_ENABLED: bool = True
@@ -35,8 +34,6 @@ _FALSEY: frozenset[str] = frozenset({"0", "false", "no", "off"})
35
34
  #: Process-wide override for the master switch, shared across all domains.
36
35
  _process_enabled: bool | None = None
37
36
 
38
- _T = TypeVar("_T")
39
-
40
37
 
41
38
  class _Unset:
42
39
  """Sentinel distinguishing 'argument omitted' from 'explicitly set to None'."""
@@ -48,12 +45,12 @@ class _Unset:
48
45
  _UNSET = _Unset()
49
46
 
50
47
 
51
- def _resolve(
48
+ def _resolve[T](
52
49
  env_var: str,
53
- process_value: _T | None,
54
- default: _T,
55
- parse: Callable[[str], _T],
56
- ) -> _T:
50
+ process_value: T | None,
51
+ default: T,
52
+ parse: Callable[[str], T],
53
+ ) -> T:
57
54
  """Resolve one axis: env var (parsed) → process override → default."""
58
55
  env = os.environ.get(env_var)
59
56
  if env:
@@ -13,8 +13,9 @@ Declare several at once inside ``Annotated`` and apply them with ``declare_schem
13
13
  from xarray_annotated.schema import declare_schema, Dims, Dtype
14
14
 
15
15
  @declare_schema
16
- def f(x: Annotated[xr.DataArray, Dims("time", "x"), Dtype("float64")]) -> xr.DataArray:
17
- ...
16
+ def f(
17
+ x: Annotated[xr.DataArray, Dims("time", "x"), Dtype("float64")],
18
+ ) -> xr.DataArray: ...
18
19
  """
19
20
 
20
21
  from ._annotations import (
@@ -48,7 +48,7 @@ class Dims:
48
48
  _names: tuple[str, ...]
49
49
  _ordered: bool
50
50
  _on_mismatch: OnMismatch | None
51
- __slots__ = ("_names", "_ordered", "_on_mismatch")
51
+ __slots__ = ("_names", "_on_mismatch", "_ordered")
52
52
 
53
53
  def __init__(
54
54
  self,
@@ -20,11 +20,11 @@ from dataclasses import dataclass
20
20
  from typing import Literal
21
21
 
22
22
  from .._config import (
23
+ _UNSET,
23
24
  DEFAULT_ENABLED,
24
25
  ENABLED_ENV_VAR,
25
26
  _resolve,
26
27
  _Unset,
27
- _UNSET,
28
28
  get_enabled_override,
29
29
  resolve_enabled,
30
30
  set_enabled_override,
@@ -100,8 +100,8 @@ def get_policy() -> Policy:
100
100
 
101
101
  def set_policy(
102
102
  *,
103
- enabled: bool | None | _Unset = _UNSET,
104
- on_mismatch: OnMismatch | None | _Unset = _UNSET,
103
+ enabled: bool | _Unset | None = _UNSET,
104
+ on_mismatch: OnMismatch | _Unset | None = _UNSET,
105
105
  ) -> None:
106
106
  """Set process-wide schema-policy overrides for one or more axes.
107
107
 
@@ -125,8 +125,8 @@ def set_policy(
125
125
  @contextmanager
126
126
  def policy(
127
127
  *,
128
- enabled: bool | None | _Unset = _UNSET,
129
- on_mismatch: OnMismatch | None | _Unset = _UNSET,
128
+ enabled: bool | _Unset | None = _UNSET,
129
+ on_mismatch: OnMismatch | _Unset | None = _UNSET,
130
130
  ):
131
131
  """Temporarily override schema-policy axes, restoring them on exit.
132
132
 
@@ -44,8 +44,8 @@ def declare_schema(
44
44
  default). When the policy is **disabled** the wrapper is a total no-op.
45
45
 
46
46
  Usable bare (`@declare_schema`) or called (`@declare_schema(on_mismatch=...)`).
47
- Every marker declaration is validated **at decoration time** (`assert_valid_schema`),
48
- so a malformed declaration fails fast at import.
47
+ Every marker declaration is validated **at decoration time**
48
+ (`assert_valid_schema`), so a malformed declaration fails fast at import.
49
49
 
50
50
  Args:
51
51
  func: The function to decorate (when used bare); `None` when parametrised.
@@ -91,7 +91,8 @@ def declare_schema(
91
91
  if not pol.enabled:
92
92
  # Master switch off: a total no-op (no validation at all).
93
93
  return fn(*args, **kwargs)
94
- # Per-call default severity (a marker's own override still wins in check_schema).
94
+ # Per-call default severity (a marker's own override still wins
95
+ # inside check_schema).
95
96
  eff = on_mismatch if on_mismatch is not None else pol.on_mismatch
96
97
  if input_markers:
97
98
  bound = sig.bind_partial(*args, **kwargs)
@@ -16,7 +16,9 @@ the classic resample footgun of a series landing on Wednesdays::
16
16
  from xarray_annotated.temporal import declare_freq, Freq
17
17
 
18
18
  @declare_freq
19
- def weekly_mean(x: Annotated[xr.DataArray, Freq("D")]) -> Annotated[xr.DataArray, Freq("W-SUN")]:
19
+ def weekly_mean(
20
+ x: Annotated[xr.DataArray, Freq("D")],
21
+ ) -> Annotated[xr.DataArray, Freq("W-SUN")]:
20
22
  return x.resample(time="W-SUN").mean()
21
23
 
22
24
  ``freq_compatible`` is the same comparison with no array in hand, for a build-time
@@ -55,7 +55,7 @@ class Freq:
55
55
  _dim: str | None
56
56
  _anchored: bool | None
57
57
  _on_mismatch: OnMismatch | None
58
- __slots__ = ("_freq", "_dim", "_anchored", "_on_mismatch")
58
+ __slots__ = ("_anchored", "_dim", "_freq", "_on_mismatch")
59
59
 
60
60
  def __init__(
61
61
  self,
@@ -24,11 +24,11 @@ from dataclasses import dataclass
24
24
  from typing import Literal
25
25
 
26
26
  from .._config import (
27
+ _UNSET,
27
28
  DEFAULT_ENABLED,
28
29
  ENABLED_ENV_VAR,
29
30
  _resolve,
30
31
  _Unset,
31
- _UNSET,
32
32
  get_enabled_override,
33
33
  resolve_enabled,
34
34
  set_enabled_override,
@@ -127,9 +127,9 @@ def get_policy() -> Policy:
127
127
 
128
128
  def set_policy(
129
129
  *,
130
- enabled: bool | None | _Unset = _UNSET,
131
- on_mismatch: OnMismatch | None | _Unset = _UNSET,
132
- on_uninferable: OnUninferable | None | _Unset = _UNSET,
130
+ enabled: bool | _Unset | None = _UNSET,
131
+ on_mismatch: OnMismatch | _Unset | None = _UNSET,
132
+ on_uninferable: OnUninferable | _Unset | None = _UNSET,
133
133
  ) -> None:
134
134
  """Set process-wide temporal-policy overrides for one or more axes.
135
135
 
@@ -158,9 +158,9 @@ def set_policy(
158
158
  @contextmanager
159
159
  def policy(
160
160
  *,
161
- enabled: bool | None | _Unset = _UNSET,
162
- on_mismatch: OnMismatch | None | _Unset = _UNSET,
163
- on_uninferable: OnUninferable | None | _Unset = _UNSET,
161
+ enabled: bool | _Unset | None = _UNSET,
162
+ on_mismatch: OnMismatch | _Unset | None = _UNSET,
163
+ on_uninferable: OnUninferable | _Unset | None = _UNSET,
164
164
  ):
165
165
  """Temporarily override temporal-policy axes, restoring them on exit.
166
166
 
@@ -425,13 +425,13 @@ def _apply_output_quantified(
425
425
  return da
426
426
  if equal:
427
427
  return da
428
- if on_output == "strict":
429
- if units_compatible(label, declared):
430
- raise ValueError(
431
- f"{prefix}output {name!r}: unit {label!r} differs from declared "
432
- f"{declared!r} and on_output='strict' requires them to match"
433
- )
434
- # Fall through: the conversion below raises DimensionalityError for us.
428
+ # Only *compatible* units raise here; an incompatible label falls through to the
429
+ # conversion below, which raises DimensionalityError for us.
430
+ if on_output == "strict" and units_compatible(label, declared):
431
+ raise ValueError(
432
+ f"{prefix}output {name!r}: unit {label!r} differs from declared "
433
+ f"{declared!r} and on_output='strict' requires them to match"
434
+ )
435
435
  try:
436
436
  return da.pint.to(declared)
437
437
  except PintExceptionGroup as group:
@@ -31,11 +31,11 @@ from dataclasses import dataclass
31
31
  from typing import Literal
32
32
 
33
33
  from .._config import (
34
+ _UNSET,
34
35
  DEFAULT_ENABLED,
35
36
  ENABLED_ENV_VAR,
36
37
  _resolve,
37
38
  _Unset,
38
- _UNSET,
39
39
  get_enabled_override,
40
40
  resolve_enabled,
41
41
  set_enabled_override,
@@ -152,10 +152,10 @@ def get_policy() -> Policy:
152
152
 
153
153
  def set_policy(
154
154
  *,
155
- enabled: bool | None | _Unset = _UNSET,
156
- on_missing: OnMissing | None | _Unset = _UNSET,
157
- on_inexact: OnInexact | None | _Unset = _UNSET,
158
- on_output: OnOutput | None | _Unset = _UNSET,
155
+ enabled: bool | _Unset | None = _UNSET,
156
+ on_missing: OnMissing | _Unset | None = _UNSET,
157
+ on_inexact: OnInexact | _Unset | None = _UNSET,
158
+ on_output: OnOutput | _Unset | None = _UNSET,
159
159
  ) -> None:
160
160
  """Set process-wide policy overrides for one or more axes.
161
161
 
@@ -189,10 +189,10 @@ def set_policy(
189
189
  @contextmanager
190
190
  def policy(
191
191
  *,
192
- enabled: bool | None | _Unset = _UNSET,
193
- on_missing: OnMissing | None | _Unset = _UNSET,
194
- on_inexact: OnInexact | None | _Unset = _UNSET,
195
- on_output: OnOutput | None | _Unset = _UNSET,
192
+ enabled: bool | _Unset | None = _UNSET,
193
+ on_missing: OnMissing | _Unset | None = _UNSET,
194
+ on_inexact: OnInexact | _Unset | None = _UNSET,
195
+ on_output: OnOutput | _Unset | None = _UNSET,
196
196
  ):
197
197
  """Temporarily override policy axes, restoring all of them on exit.
198
198
 
@@ -199,5 +199,6 @@ def declare_units(
199
199
  return wrapper
200
200
 
201
201
  # @declare_units → func is the decorated function;
202
- # @declare_units(on_missing=...) → func is None, return the parametrised decorator.
202
+ # @declare_units(on_missing=...) → func is None, return the parametrised
203
+ # decorator.
203
204
  return decorate if func is None else decorate(func)
@@ -1,81 +0,0 @@
1
- [build-system]
2
- requires = ["uv_build>=0.11.17,<0.12.0"]
3
- build-backend = "uv_build"
4
-
5
- [project]
6
- name = "xarray-annotated"
7
- version = "0.5.2"
8
- description = "Run-time validation of xarray DataArray properties (units, dims/coords/dtype) declared via typing.Annotated"
9
- authors = [
10
- { name = "jmarshrossney", email = "17361029+jmarshrossney@users.noreply.github.com" }
11
- ]
12
- readme = "README.md"
13
- license = "MIT"
14
- license-files = ["LICENSE"]
15
- requires-python = ">=3.12"
16
- dependencies = [
17
- "pandas>=2.2",
18
- "pint>=0.25.3",
19
- "pint-xarray>=0.6.1",
20
- "xarray>=2026.2.0",
21
- ]
22
- keywords = ["xarray", "pint", "units", "validation", "annotations"]
23
- classifiers = [
24
- "Development Status :: 3 - Alpha",
25
- "Intended Audience :: Science/Research",
26
- "Programming Language :: Python :: 3.12",
27
- "Programming Language :: Python :: 3.13",
28
- "Programming Language :: Python :: 3.14",
29
- "Topic :: Scientific/Engineering",
30
- ]
31
-
32
- [project.optional-dependencies]
33
- cf = ["cf-xarray>=0.11.3"]
34
-
35
- [project.urls]
36
- Homepage = "https://jmarshrossney.github.io/xarray-annotated"
37
- Repository = "https://github.com/jmarshrossney/xarray-annotated"
38
- Documentation = "https://jmarshrossney.github.io/xarray-annotated"
39
-
40
- [dependency-groups]
41
- dev = [
42
- "xarray-annotated[cf]",
43
- "cftime>=1.6",
44
- "coverage>=7.13.5",
45
- "pyright>=1.1.409",
46
- "pytest>=9.0.2",
47
- "pytest-cov>=7.1.0",
48
- "ruff>=0.15.4",
49
- "rust-just>=1.46.0",
50
- "pre-commit>=4.6.0",
51
- {include-group = "docs"},
52
- ]
53
- docs = [
54
- "marimo>=0.23.11",
55
- "marimo-md-export>=0.7.0",
56
- "mkdocstrings-python>=2.0.5",
57
- "zensical>=0.0.46",
58
- ]
59
-
60
- [tool.pytest]
61
- testpaths = ["tests"]
62
-
63
- [tool.pyright]
64
- venvPath = "."
65
- venv = ".venv"
66
- include = ["src/", "tests/"]
67
-
68
- [tool.ruff]
69
- line-length = 88
70
- indent-width = 4
71
- target-version = "py312"
72
-
73
- [tool.ruff.lint.per-file-ignores]
74
- # marimo threads `Annotated` in as a cell argument, so ruff can't resolve it to
75
- # `typing.Annotated` and misreads the unit strings in the metadata as forward
76
- # references (F821 "undefined name" / F722 "syntax error in forward annotation").
77
- "examples/notebook.py" = ["F821", "F722"]
78
-
79
- [tool.uv]
80
- exclude-newer = "1 week"
81
- exclude-newer-package = { marimo-md-export = false }