Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,31 @@ def extract_units(obj: object, /) -> dict:
raise TypeError(msg)


def _reject_unknown_unit_names(
units: "Mapping[Hashable, Any]", valid_names: "set[Any]", obj_kind: str
) -> None:
"""Raise if units names something that is not a variable/coordinate.

Without this, a typo in a **unit_kwargs name (e.g.
quantify(temperatrue="K")) is silently dropped by units.get(name),
leaving the data unquantified with no error or warning.
"""
# Sort so the message is deterministic: `units` insertion order follows the
# accessor's ``set(...)`` iteration, which is not stable across processes.
unknown = sorted(repr(k) for k in units if k not in valid_names)
if not unknown:
return
# ``None`` is the DataArray's own-data key; render it explicitly so the
# message stays helpful (and non-empty) when there are no other names.
names = ("None (the data)" if n is None else repr(n) for n in valid_names)
valid = ", ".join(sorted(names)) or "(none)"
msg = (
f"got unit(s) for name(s) not in the {obj_kind}: {', '.join(unknown)}. "
f"Valid names: {valid}."
)
raise ValueError(msg)


@dispatch
def attach_units(
obj: DataArray, units: Mapping[Hashable, u.AbstractUnit | str | None]
Expand Down Expand Up @@ -296,6 +321,8 @@ def attach_units(
{}

"""
_reject_unknown_unit_names(units, {None} | set(obj.coords), "DataArray")

# Handle the data array itself (None key = the DataArray's own data)
data_unit = units.get(None)
new_data = _array_attach_units(obj.data, data_unit)
Expand Down Expand Up @@ -346,6 +373,8 @@ def attach_units(
New Dataset with units attached as Quantities.

"""
_reject_unknown_unit_names(units, set(obj.data_vars) | set(obj.coords), "Dataset")

# Handle all variables in dataset
new_vars = {}
for name, var in obj.data_vars.items():
Expand Down
34 changes: 34 additions & 0 deletions packages/unxts.interop.xarray/tests/test_unknown_unit_keys.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""quantify()/attach_units reject unit specs for names that don't exist.

Regression: a typo in a `**unit_kwargs` name was silently dropped, leaving the
data unquantified with no error.
"""

import pytest
import unxts.interop.xarray # noqa: F401 # registers the .unxt accessor
import xarray as xr

import unxt as u


def test_dataset_typo_key_raises():
ds = xr.Dataset({"temperature": ("x", [1.0, 2.0])})
with pytest.raises(ValueError, match=r"not in the Dataset.*temperatrue"):
ds.unxt.quantify(temperatrue="K")
# the correct name still works
q = ds.unxt.quantify(temperature="K")
assert u.unit_of(q["temperature"].data) == u.unit("K")


def test_dataarray_typo_coord_key_raises():
# ``y`` is a *non-dimension* coordinate on dim ``x``. A dimension coordinate
# would be wrapped in a pandas index by xarray, which drops the unit (a
# separate, known limitation), so a non-dimension coord is used here to
# verify that a valid coord name actually quantifies.
da = xr.DataArray([1.0, 2.0], dims=["x"], coords={"y": ("x", [10.0, 20.0])})
with pytest.raises(ValueError, match=r"not in the DataArray.*'yy'"):
da.unxt.quantify(yy="s")
# valid data + coord names still quantify
q = da.unxt.quantify("m", y="s")
assert u.unit_of(q.data) == u.unit("m")
assert u.unit_of(q.coords["y"].data) == u.unit("s")
Loading