-
Notifications
You must be signed in to change notification settings - Fork 28
Patch boolean logic and better numpy array protocol support #537
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
WalkthroughCentralizes aggregators in Changes
Possibly related PRs
Suggested labels
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
dascore/core/patch.py (1)
148-155: Fix reverse (right‑hand) operators for non‑commutative ops.Mapping rsub/rtruediv/rfloordiv/rmod/rpow to their left‑hand counterparts yields wrong results (e.g., other - patch currently computes patch - other). Implement true right‑hand semantics.
- __radd__ = __add__ - __rsub__ = __sub__ - __rmul__ = __mul__ - __rpow__ = __pow__ - __rtruediv__ = __truediv__ - __rfloordiv__ = __floordiv__ - __rmod__ = __mod__ + __radd__ = __add__ + __rmul__ = __mul__ + def __rsub__(self, other): + return dascore.utils.array.apply_ufunc(np.subtract, other, self) + def __rtruediv__(self, other): + return dascore.utils.array.apply_ufunc(np.divide, other, self) + def __rfloordiv__(self, other): + return dascore.utils.array.apply_ufunc(np.floor_divide, other, self) + def __rmod__(self, other): + return dascore.utils.array.apply_ufunc(np.mod, other, self) + def __rpow__(self, other): + return dascore.utils.array.apply_ufunc(np.power, other, self)
🧹 Nitpick comments (22)
dascore/utils/patch.py (1)
626-631: Tightened error-message formatting — OK.String tweak looks good and keeps the message compact.
While here, there are tiny doc typos nearby you may want to fix later: “nane” → “name” (Line 568) and “retreiving” → “retrieving” (Line 593).
dascore/constants.py (1)
5-5: Centralizing _AGG_FUNCS — good; consider freezing and typing.The mapping and partial(np.take,…) choices are appropriate for axis-aware reductions.
- Add a read-only wrapper to prevent accidental mutation:
- from types import MappingProxyType
- _AGG_FUNCS = MappingProxyType(_AGG_FUNCS)
- Add a type hint for clarity (e.g., dict[str, Callable]): minor but improves IDE help.
Also applies to: 190-199
tests/test_proc/test_basic.py (1)
167-176: test_max uses L1 norm and sum — fix to use max norm.The “max” normalization test should use norm="max" and check max(abs(.)) per axis.
Apply this diff:
- # test along the distance axis - dist_norm = random_patch.normalize("distance", norm="l1") + # test along the distance axis + dist_norm = random_patch.normalize("distance", norm="max") axis = dims.index("distance") - norm = np.abs(np.sum(dist_norm.data, axis=axis)) + norm = np.max(np.abs(dist_norm.data), axis=axis) assert np.allclose(norm, 1) # tests along time axis - time_norm = random_patch.normalize("time", norm="l1") + time_norm = random_patch.normalize("time", norm="max") axis = dims.index("time") - norm = np.abs(np.sum(time_norm.data, axis=axis)) + norm = np.max(np.abs(time_norm.data), axis=axis) assert np.allclose(norm, 1)Also, optional: the class name “TestStandarize” has a typo; “TestStandardize” reads better.
dascore/proc/aggregate.py (1)
85-87: Single aggregation path — OK.Delegating to _apply_aggregator keeps behavior consistent across methods.
For deterministic docs, consider sorted(_AGG_FUNCS) instead of list(_AGG_FUNCS) in the compose_docstring options.
dascore/proc/basic.py (1)
156-165: bool_patch behavior is correct; tighten docstring.Minor nits: “on” → “one”; “value error” → “ValueError”.
- This follows the numpy convention of raising a value error if the patch - has more than on element. Otherwise, it returns the truthy value of the + This follows the NumPy convention of raising a ValueError if the patch + has more than one element. Otherwise, it returns the truthiness of the one element.dascore/core/patch.py (4)
237-241: Fix dtype annotation type.dtype returns a numpy dtype, not tuple[int, ...].
- def dtype(self) -> tuple[int, ...]: + def dtype(self) -> np.dtype:
233-236: Fix size annotation type.size is an int.
- def size(self) -> tuple[int, ...]: + def size(self) -> int:
252-256: Remove unused blanket noqa or scope it.Ruff flagged unused blanket noqa. Either remove it or target the rule explicitly if you’re silencing N802 for the .T alias.
- def T(self): # noqa + def T(self): # noqa: N802
189-189: Remove stray pass.Dead statement; safe to drop.
- passdascore/utils/array.py (5)
137-144: Preserve original exception context for unit errors.Use “raise … from err”.
- except DimensionalityError: + except DimensionalityError as err: msg = f"{operator} failed with units {data_units} and {other.units}" - raise UnitError(msg) + raise UnitError(msg) from err
213-217: Avoid bare except; narrow to expected numeric/ufunc failures.Catching Exception masks bugs. Limit to common NumPy errors.
- except Exception: # Fall back to floats and re-packing. + except (TypeError, ValueError, np.core._exceptions.UFuncTypeError): float_data = dc.to_float(data)
243-251: Silence unused loop var.value isn’t used; rename to underscore.
- for dim, axis, value in dfo: + for dim, axis, _ in dfo:
151-151: Typo in assertion message.DASCOre → DASCore.
- assert len(args) <= 2, "Currently, DASCOre ufuncs are only supported for 2 inputs." + assert len(args) <= 2, "Currently, DASCore ufuncs are only supported for 2 inputs."
335-342: array_function reassembly: add fallback when no ‘axis’ is present.Functions without an axis parameter (e.g., np.square) will return None here. Consider returning patch.new(data=result) if shapes match; otherwise raise a clear error.
- if "axis" in sig.parameters: + if "axis" in sig.parameters: dims, inds = _get_dims_and_inds_from_signature(patch, sig, args, kwargs) # re-expand array. result = result[inds] new_coords = {x: _get_new_coord(patch.get_coord(x)) for x in dims} cm = patch.coords.update(**new_coords) return patch.new(data=result, coords=cm) + # Fallback: if shape unchanged, just wrap result; else fail clearly. + if result.shape == patch.shape: + return patch.new(data=result) + msg = f"Cannot reassemble result of {getattr(func, '__name__', func)} without an axis parameter." + raise ParameterError(msg)tests/test_utils/test_array_utils.py (8)
30-37: Strengthen the “incompatible coords” assertions.Also assert zero-sized data and stable dims for clearer failure signals.
Apply this diff:
out = apply_ufunc(np.multiply, new, random_patch) - assert 0 in set(out.shape) + assert 0 in set(out.shape) + assert out.data.size == 0 + assert out.dims == random_patch.dims
53-56: Normalize dimensionless-unit assertion.Compare via get_quantity and accept explicit “1” or omitted units; drop the redundant ndarray type check.
Apply this diff:
- new = apply_ufunc(np.divide, patch, other) - new_units = get_quantity("m/s") / get_quantity("m/s") - assert new.attrs.data_units is None or new.attrs.data_units == new_units - assert isinstance(new.data, np.ndarray) + new = apply_ufunc(np.divide, patch, other) + # Dimensionless result: units may be omitted; accept None or "1". + q = get_quantity(new.attrs.data_units) + assert q is None or q == get_quantity("1")
73-76: Same dimensionless-unit normalization here.Apply this diff:
- new = apply_ufunc(np.divide, patch, other) - new_units = get_quantity("m/s") / get_quantity("m/s") - assert new.attrs.data_units is None or new.attrs.data_units == new_units - assert np.allclose(new.data, random_patch.data) + new = apply_ufunc(np.divide, patch, other) + q = get_quantity(new.attrs.data_units) + assert q is None or q == get_quantity("1") + assert np.allclose(new.data, random_patch.data)
46-46: Remove redundant ndarray type checks.These don’t add signal; failures will show up in the numeric assertions.
Apply this diff:
- assert isinstance(new.data, np.ndarray)(Repeat for the two other identical occurrences.)
Also applies to: 51-51, 56-56
97-97: Avoid relying on get_quantity(Unit) coercion.Prefer string form to reduce dependence on permissive parsing.
Apply this diff:
- assert get_quantity(out1.attrs.data_units) == get_quantity(furlongs) + assert get_quantity(out1.attrs.data_units) == get_quantity("furlongs")
131-131: Be explicit about comparing coord keys.Small clarity bump; avoids relying on Mapping’s iteration semantics.
Apply this diff:
- assert set(out.coords.coord_map) == set(random_dft_patch.coords.coord_map) + assert set(out.coords.coord_map.keys()) == set(random_dft_patch.coords.coord_map.keys())
24-29: Add reversed-operand protocol tests (NumPy on LHS).Verifies array_ufunc/array_function when Patch is rhs.
Add:
+ def test_reversed_scalar(self, random_patch): + out = 10 + random_patch # np.add with Patch on RHS + assert np.allclose(out.data, random_patch.data + 10) + + def test_reversed_array_like(self, random_patch): + ones = np.ones(random_patch.shape) + out = ones * random_patch + assert np.allclose(out.data, ones * random_patch.data) + + def test_reversed_unit_and_quantity(self, random_patch): + pa = random_patch.set_units("m/s") + out1 = (m / s) + pa # unit on LHS + out2 = (10 * m / s) + pa # quantity on LHS + assert np.allclose(out1.data, random_patch.data + 1) + assert np.allclose(out2.data, random_patch.data + 10)
16-132: Optional: parametrize repeated unit-op cases.Would reduce duplication and produce tighter failure diffs.
Example:
@pytest.mark.parametrize( "op, other, expected_units, expected_data", [ (np.multiply, 10 * m / s, "m**2/s**2", lambda d: d), (np.add, 10 * m / s, "m/s", lambda d: d + 10), (np.divide, 10 * m / s, "1", lambda d: d), ], ) def test_quantity_ops_param(self, random_patch, op, other, expected_units, expected_data): pa = random_patch.set_units("m/s") out = apply_ufunc(op, pa, other) q = get_quantity(out.attrs.data_units) assert (q is None and expected_units == "1") or q == get_quantity(expected_units) assert np.allclose(out.data, expected_data(random_patch.data))
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (10)
dascore/constants.py(2 hunks)dascore/core/patch.py(6 hunks)dascore/proc/aggregate.py(3 hunks)dascore/proc/basic.py(3 hunks)dascore/utils/array.py(1 hunks)dascore/utils/patch.py(1 hunks)tests/test_core/test_patch.py(5 hunks)tests/test_proc/test_aggregate.py(1 hunks)tests/test_proc/test_basic.py(6 hunks)tests/test_utils/test_array_utils.py(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (8)
tests/test_proc/test_aggregate.py (2)
dascore/examples.py (1)
random_patch(29-111)dascore/core/patch.py (1)
data(223-225)
dascore/utils/array.py (8)
dascore/compat.py (2)
array(31-37)is_array(40-43)dascore/exceptions.py (3)
ParameterError(26-27)PatchBroadcastError(62-63)UnitError(106-107)dascore/units.py (1)
get_quantity(70-94)dascore/utils/misc.py (1)
iterate(256-268)dascore/utils/patch.py (4)
_merge_aligned_coords(879-897)_merge_models(900-924)align_patch_coords(731-781)get_dim_axis_value(558-642)dascore/utils/time.py (5)
dtype_time_like(415-425)is_datetime64(405-407)to_datetime64(22-55)to_timedelta64(141-173)to_float(327-333)dascore/core/patch.py (7)
dtype(238-240)coords(218-220)attrs(213-215)data(223-225)shape(228-230)Patch(28-407)dims(198-200)dascore/proc/basic.py (1)
update(167-207)
tests/test_utils/test_array_utils.py (6)
dascore/units.py (1)
get_quantity(70-94)dascore/exceptions.py (1)
UnitError(106-107)dascore/utils/array.py (1)
apply_ufunc(42-171)dascore/core/patch.py (5)
data(223-225)shape(228-230)attrs(213-215)dims(198-200)coords(218-220)dascore/proc/basic.py (1)
update_attrs(79-105)tests/conftest.py (2)
patch(375-377)random_dft_patch(289-291)
tests/test_proc/test_basic.py (2)
dascore/exceptions.py (2)
ParameterError(26-27)PatchBroadcastError(62-63)dascore/proc/basic.py (1)
standardize(332-369)
dascore/proc/aggregate.py (3)
dascore/utils/array.py (1)
_apply_aggregator(237-252)dascore/utils/docs.py (1)
compose_docstring(45-97)dascore/utils/patch.py (1)
patch_function(179-285)
dascore/proc/basic.py (1)
dascore/utils/array.py (1)
apply_ufunc(42-171)
dascore/core/patch.py (3)
dascore/utils/array.py (3)
array_function(344-377)array_ufunc(380-396)apply_ufunc(42-171)dascore/proc/basic.py (1)
bool_patch(156-164)dascore/proc/aggregate.py (2)
any(245-259)all(264-278)
tests/test_core/test_patch.py (5)
dascore/exceptions.py (2)
CoordError(38-39)ParameterError(26-27)dascore/core/patch.py (6)
T(253-256)data(223-225)dtype(238-240)Patch(28-407)dims(198-200)shape(228-230)dascore/proc/coords.py (1)
transpose(553-584)dascore/core/coordmanager.py (4)
transpose(812-839)new(431-449)min(1011-1013)shape(744-750)dascore/core/coords.py (3)
new(592-603)data(606-608)min(438-440)
🪛 Ruff (0.12.2)
dascore/utils/array.py
141-141: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
213-213: Do not catch blind exception: Exception
(BLE001)
243-243: Loop control variable value not used within loop body
Rename unused value to _value
(B007)
dascore/core/patch.py
253-253: Unused blanket noqa directive
Remove unused noqa directive
(RUF100)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (16)
- GitHub Check: test_code (windows-latest, 3.12)
- GitHub Check: test_code (windows-latest, 3.11)
- GitHub Check: test_code (windows-latest, 3.13)
- GitHub Check: test_code (macos-latest, 3.11)
- GitHub Check: test_code (windows-latest, 3.10)
- GitHub Check: test_code (ubuntu-latest, 3.13)
- GitHub Check: test_code (macos-latest, 3.13)
- GitHub Check: test_code (ubuntu-latest, 3.12)
- GitHub Check: test_code (ubuntu-latest, 3.10)
- GitHub Check: test_code (ubuntu-latest, 3.11)
- GitHub Check: test_code_min_deps (macos-latest, 3.12)
- GitHub Check: test_code_min_deps (macos-latest, 3.13)
- GitHub Check: test_code_min_deps (ubuntu-latest, 3.12)
- GitHub Check: test_code_min_deps (windows-latest, 3.13)
- GitHub Check: test_code_min_deps (ubuntu-latest, 3.13)
- GitHub Check: test_code_min_deps (windows-latest, 3.12)
🔇 Additional comments (18)
tests/test_proc/test_aggregate.py (1)
103-104: Compare arrays, not Patch objects — LGTM.Switching to random_patch.data - agg.data makes the assertion unambiguous.
tests/test_proc/test_basic.py (1)
14-15: Comment/import touch-ups — OK.Importing PatchBroadcastError and the comment clarifications are fine.
Also applies to: 139-149, 153-163, 167-177, 205-211, 216-226, 231-243
dascore/proc/aggregate.py (3)
9-13: Import/structure change — OK.Pulling _AGG_FUNCS from constants and consolidating via _apply_aggregator simplifies the surface.
241-241: sum wrapper parity — OK.Matches other numeric reductions and uses np.nansum.
243-279: Aggregation wrappers verified — aggregate.func used; confirm any/all shadow built-ins intentionally.
- All reducer shortcuts (min, max, mean, median, std, first, last, sum, any, all) call aggregate.func in dascore/proc/aggregate.py (aggregate.func occurrences: lines 105, 124, 143, 162, 181, 201, 221, 240, 259, 278).
- Confirm exposing any and all (which shadow Python built-ins) is intentional for the public API and acceptable for wildcard imports.
tests/test_core/test_patch.py (7)
17-17: Good: centralizing exceptions import.Importing ParameterError from dascore.exceptions matches the public surface and keeps tests aligned with the API.
427-429: Good: .T property parity test.Confirms T mirrors transpose(), matching the new alias.
620-624: Good: expanded operator coverage to comparisons.Exercises gt/ge/lt/le on Patch objects.
644-645: Heads‑up: apply_operator now expects operator‑first.This call assumes apply_ufunc/apply_operator signature is (op, patch, other). Ensure the implementation matches (see my utils/array.py comment/fix).
647-652: Good: dtype and boolean result checks for comparisons.Validates boolean dtype and basic semantics.
654-671: Good: numpy‑like bool semantics.Multi‑element raises; single‑element truthiness works via .all().
768-818: Good: NumPy protocol coverage.Covers reducer, accumulator, reduce, non‑reducer, multi‑axis, and ufunc.at error path. This will guard regressions in array_function/array_ufunc.
dascore/proc/basic.py (2)
12-19: Ensure apply_ufunc signature aligns with call sites.You import apply_ufunc and alias apply_operator to it; tests use operator‑first calls. See my fix in utils/array.py.
372-375: Back‑compat alias looks good.apply_operator = apply_ufunc preserves the legacy entry point.
dascore/core/patch.py (3)
110-127: Operator dunders correctly delegate; depends on operator‑first apply_ufunc.These look good once apply_ufunc is fixed to (op, patch, other).
Also applies to: 131-145
160-162: NumPy protocol wiring LGTM.array_ufunc/array_function assignment and priority are appropriate.
368-370: any/all aliases look good.Convenient shortcuts that align with aggregate funcs.
tests/test_utils/test_array_utils.py (1)
16-132: Solid coverage of apply_ufunc; overall LGTM.Good breadth: scalars, array-like, unit-aware ops, broadcasting, coord alignment, non-dim coords. See below for small test robustness tweaks and a couple of helpful additions.
Are comparison/boolean ops (e.g., patch > 2, any/all) covered in other test modules in this PR?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🧹 Nitpick comments (7)
tests/test_proc/test_basic.py (1)
164-177: Duplicate test methodtest_l1_norm.This method duplicates the logic of
test_l1on lines 150-162. Both test the same L1 normalization functionality with identical assertions.This duplication should be removed. Apply this diff to fix:
- def test_l1_norm(self, random_patch): - """Ensure after l1 normalization operation norms are 1.""" - dims = random_patch.dims - # test along the distance axis - dist_norm = random_patch.normalize("distance", norm="l1") - axis = dims.index("distance") - norm = np.abs(np.sum(dist_norm.data, axis=axis)) - assert np.allclose(norm, 1) - # tests along time axis - time_norm = random_patch.normalize("time", norm="l1") - axis = dims.index("time") - norm = np.abs(np.sum(time_norm.data, axis=axis)) - assert np.allclose(norm, 1)dascore/core/patch.py (1)
250-254: Remove unusednoqadirective.The
noqa: N802directive on line 251 is unnecessary and should be removed as indicated by the static analysis tool.- def T(self): # noqa: N802 + def T(self):dascore/utils/array.py (3)
134-154: Consider improving error messages in unit operations.The unit error handling in
_apply_op_unitscould be more informative by including the actual units in the error message.except DimensionalityError as er: - msg = f"{operator} failed with units {data_units} and {other.units}" + units_str = f"{data_units} and {getattr(other, 'units', 'unknown')}" + msg = f"Cannot apply {operator.__name__} to incompatible units: {units_str}" raise UnitError(msg) from er
437-442: Unusedselfparameter in protocol methods.Both
patch_array_ufuncandpatch_array_functionhave an unusedselfparameter. While this is expected for protocol compliance, consider adding a comment to clarify this.def patch_array_ufunc(self, ufunc, method, *inputs, **kwargs): """ Called when a numpy ufunc is applied to a patch (__array_ufunc__). + + The self parameter is required for protocol compliance but not used directly. """ method = ufunc if method == "__call__" else getattr(ufunc, method, ufunc) return apply_ufunc(method, *inputs, **kwargs)def patch_array_function(self, func, types, args, kwargs): """ Intercept NumPy functions for patch operations. Parameters ---------- + self : Patch + The patch instance (required for protocol but not used). func : callable The NumPy function being called.
324-356: Consider simplifying the patch reassembly logic.The
_reassemble_patchfunction has complex fallback logic for handling missing signatures. Consider extracting the signature retrieval into a separate helper function for clarity.+def _get_function_signature(func): + """Get the signature of a function, with fallback for ufunc methods.""" + try: + return inspect.signature(func) + except (ValueError, AttributeError): + ufunc_method_name = {"reduce": _dummy_reduce, "accumulate": _dummy_accumulate} + name = getattr(func, "__name__", None) + if name in ufunc_method_name: + return inspect.signature(ufunc_method_name[name]) + raise + def _reassemble_patch(result, patch, func, args, kwargs): """ Method to put the patch back together. """ # Simples case, data shape hasn't changed. if result.shape == patch.shape: return patch.new(data=result) # Otherwise, we have to do some detective work to figure out what happened # to the array so we can adjust the coords accordingly. - try: - sig = inspect.signature(func) - # For some reason, some ufuncs methods like reduce don't have signatures. - except (ValueError, AttributeError): - ufunc_method_name = {"reduce": _dummy_reduce, "accumulate": _dummy_accumulate} - name = getattr(func, "__name__", None) - assert name in ufunc_method_name - sig = inspect.signature(ufunc_method_name[name]) + sig = _get_function_signature(func)tests/test_utils/test_array_utils.py (2)
21-26: Fix typo in test method name.The test method is named
test_uniary_ufuncsbut should betest_unary_ufuncs.- def test_uniary_ufuncs(self, func, random_patch): + def test_unary_ufuncs(self, func, random_patch):
89-94: Consider tightening the dimensionless unit assertion.The test accepts either
Noneor"1"for dimensionless units, but it would be cleaner to have consistent behavior.# Dimensionless result: units may be omitted; accept None or "1". q = get_quantity(new.attrs.data_units) - assert q is None or q == get_quantity("1") + # Prefer consistent behavior - either always None or always "1" for dimensionless + assert q is None or str(q) == "1" or q == get_quantity("1")
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (9)
dascore/constants.py(2 hunks)dascore/core/patch.py(7 hunks)dascore/proc/aggregate.py(4 hunks)dascore/proc/basic.py(3 hunks)dascore/utils/array.py(1 hunks)dascore/utils/patch.py(3 hunks)tests/test_core/test_patch.py(5 hunks)tests/test_proc/test_basic.py(6 hunks)tests/test_utils/test_array_utils.py(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- dascore/utils/patch.py
🧰 Additional context used
🧬 Code graph analysis (7)
dascore/proc/aggregate.py (3)
dascore/utils/array.py (1)
_apply_aggregator(244-259)dascore/utils/docs.py (1)
compose_docstring(45-97)dascore/utils/patch.py (1)
patch_function(179-285)
tests/test_utils/test_array_utils.py (4)
dascore/units.py (1)
get_quantity(70-94)dascore/exceptions.py (2)
ParameterError(26-27)UnitError(106-107)dascore/utils/array.py (1)
apply_ufunc(389-434)dascore/core/patch.py (2)
Patch(28-405)data(221-223)
tests/test_core/test_patch.py (2)
dascore/exceptions.py (2)
CoordError(38-39)ParameterError(26-27)dascore/core/patch.py (2)
T(251-254)Patch(28-405)
dascore/utils/array.py (8)
dascore/compat.py (2)
array(31-37)is_array(40-43)dascore/exceptions.py (3)
ParameterError(26-27)PatchBroadcastError(62-63)UnitError(106-107)dascore/units.py (1)
get_quantity(70-94)dascore/utils/misc.py (1)
iterate(256-268)dascore/utils/patch.py (4)
_merge_aligned_coords(879-897)_merge_models(900-924)align_patch_coords(731-781)get_dim_axis_value(558-642)dascore/utils/time.py (5)
dtype_time_like(415-425)is_datetime64(405-407)to_datetime64(22-55)to_timedelta64(141-173)to_float(327-333)dascore/core/patch.py (7)
dtype(236-238)data(221-223)coords(216-218)attrs(211-213)shape(226-228)Patch(28-405)dims(196-198)dascore/proc/basic.py (1)
update(167-207)
dascore/core/patch.py (5)
dascore/utils/array.py (3)
patch_array_function(445-462)patch_array_ufunc(437-442)apply_ufunc(389-434)dascore/proc/basic.py (1)
bool_patch(156-164)dascore/core/coords.py (2)
size(473-475)data(606-608)dascore/proc/coords.py (1)
transpose(553-584)dascore/proc/aggregate.py (2)
any(245-259)all(264-278)
tests/test_proc/test_basic.py (5)
dascore/exceptions.py (2)
ParameterError(26-27)PatchBroadcastError(62-63)dascore/examples.py (1)
random_patch(29-111)tests/conftest.py (1)
random_patch(282-284)dascore/core/patch.py (2)
dims(196-198)data(221-223)dascore/proc/basic.py (1)
standardize(332-369)
dascore/proc/basic.py (1)
dascore/utils/array.py (1)
_apply_binary_ufunc(66-180)
🪛 Ruff (0.12.2)
dascore/utils/array.py
437-437: Unused function argument: self
(ARG001)
445-445: Unused function argument: self
(ARG001)
dascore/core/patch.py
251-251: Unused noqa directive (non-enabled: N802)
Remove unused noqa directive
(RUF100)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (16)
- GitHub Check: test_code_min_deps (windows-latest, 3.13)
- GitHub Check: test_code_min_deps (ubuntu-latest, 3.13)
- GitHub Check: test_code_min_deps (windows-latest, 3.12)
- GitHub Check: test_code_min_deps (macos-latest, 3.12)
- GitHub Check: test_code_min_deps (ubuntu-latest, 3.12)
- GitHub Check: test_code_min_deps (macos-latest, 3.13)
- GitHub Check: test_code (ubuntu-latest, 3.11)
- GitHub Check: test_code (ubuntu-latest, 3.12)
- GitHub Check: test_code (windows-latest, 3.10)
- GitHub Check: test_code (macos-latest, 3.12)
- GitHub Check: test_code (macos-latest, 3.11)
- GitHub Check: test_code (windows-latest, 3.12)
- GitHub Check: test_code (windows-latest, 3.11)
- GitHub Check: test_code (ubuntu-latest, 3.13)
- GitHub Check: test_code (windows-latest, 3.13)
- GitHub Check: test_code (ubuntu-latest, 3.10)
🔇 Additional comments (25)
dascore/constants.py (2)
5-9: LGTM! Clean imports for aggregation registry.The new imports properly support the aggregation function registry, using appropriate standard library types for the implementation.
192-203: Well-designed aggregation registry with appropriate immutability.The
_AGG_FUNCSregistry is well-structured, usingMappingProxyTypeto ensure immutability. The choice of nan-safe numpy functions (nanmean,nanmedian, etc.) is appropriate for handling DAS data that may contain missing values. Usingpartialforfirst/lastoperations is a clean functional approach.tests/test_proc/test_basic.py (2)
200-200: Fix typo:TestStandardizeis spelled correctly.The class was renamed from the misspelled
TestStandarizetoTestStandardize, which is the correct spelling.
644-645: Updated signature forapply_operatorreflects the new implementation.The test correctly uses the new signature
apply_operator(operator, patch, array)instead of the old signature, aligning with the refactored implementation indascore.utils.array.tests/test_core/test_patch.py (4)
427-430: Good test coverage for the new.Tproperty.The test properly verifies that the
.Tproperty works as an alias for.transpose(), matching NumPy conventions.
620-624: Excellent expansion of comparison operator coverage.The addition of
ge,le,gt,ltoperators provides complete coverage of comparison operations, ensuring the new NumPy integration path handles all comparison ufuncs correctly.
654-671: Well-designed boolean conversion tests matching NumPy semantics.The tests correctly verify that boolean conversion of multi-dimensional patches raises a
ValueError(matching NumPy's ambiguity error) and that single-value patches return their element's truthiness. This aligns perfectly with the newbool_patchimplementation.
768-818: Comprehensive test coverage for NumPy function integration.The new test class thoroughly verifies that NumPy functions work directly on Patch objects, including:
- Reducer functions returning Patch instances
- Accumulator and reduce methods
- Non-reducing functions preserving shape
- Patch-on-patch operations
- Proper error handling for unsupported operations
- Complete reductions and multi-axis operations
This provides excellent validation of the new NumPy array protocol support.
dascore/proc/aggregate.py (4)
9-12: Clean refactoring to use centralized aggregation infrastructure.The imports correctly reference the centralized
_AGG_FUNCSregistry fromdascore.constantsand the_apply_aggregatorhelper fromdascore.utils.array, promoting code reuse and maintainability.
85-86: Simplified aggregation implementation using shared helper.The refactored implementation cleanly delegates to
_apply_aggregator, reducing code duplication and ensuring consistent behavior across all aggregation operations.
243-260: Good addition of boolean aggregation methods.The new
any()method provides useful boolean reduction functionality that aligns well with NumPy's semantics. The implementation correctly usesnp.anythrough the aggregation framework.
262-278: Complete boolean aggregation pair withall()method.The
all()method properly complementsany(), usingnp.allfor complete boolean aggregation support.dascore/proc/basic.py (2)
156-165: Well-implemented boolean conversion following NumPy conventions.The
bool_patchfunction correctly implements NumPy's boolean conversion semantics by delegating tobool(self.data), which will raiseValueErrorfor multi-element arrays and return the truthiness of single elements.
372-375: Smooth migration path forapply_operator.The compatibility alias ensures backward compatibility while the implementation has been moved to
dascore.utils.array. The comment clearly explains the purpose.dascore/core/patch.py (6)
111-129: LGTM! Clean operator overloading implementation.The refactored operators consistently use
dascore.utils.array.apply_ufuncwith the appropriate NumPy ufuncs, providing clear and maintainable code.
131-142: Excellent addition of comparison operators.The comparison operators (
__gt__,__ge__,__lt__,__le__) are now properly implemented using the NumPy ufunc protocol, enabling intuitive boolean operations likepatch > 2.
143-144: LGTM! Proper boolean conversion following NumPy convention.The
__bool__method correctly delegates todascore.proc.basic.bool_patch, which follows NumPy's convention of raising ValueError for arrays with more than one element.
159-162: Well-designed NumPy protocol integration.The addition of
__array_ufunc__,__array_function__, and__array_priority__properly integrates Patch objects with NumPy's dispatch mechanism, ensuring Patch takes precedence in mixed operations.
231-238: Good NumPy-compatible property additions.The
sizeanddtypeproperties enhance NumPy compatibility by providing expected attributes.
366-367: LGTM! Valuable boolean aggregation methods.The addition of
anyandallaggregation methods completes the boolean operation support, enabling useful reductions on boolean patches.dascore/utils/array.py (2)
41-63: LGTM! Clean unary ufunc implementation.The unary ufunc handler is simple and effective, directly applying the operator to patch data and creating a new patch with the result.
389-434: Well-structured ufunc dispatcher.The
apply_ufuncfunction provides a clean dispatch mechanism based on ufunc signatures, with appropriate error handling for unsupported operations.tests/test_utils/test_array_utils.py (3)
50-64: Good coverage of edge cases with xfail test.The test properly documents a known limitation with reversed unit/quantity operations using xfail. This is a responsible way to handle current implementation constraints.
174-192: Excellent parameterized test coverage.The parameterized test
test_quantity_ops_paramprovides comprehensive coverage of quantity operations with clear expected outcomes.
202-209: Good test for reduction operations.The test properly verifies that reduction operations work through the ufunc protocol and maintain the Patch type.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #537 +/- ##
========================================
Coverage 99.88% 99.88%
========================================
Files 122 123 +1
Lines 10044 10256 +212
========================================
+ Hits 10032 10244 +212
Misses 12 12
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (2)
dascore/utils/array.py (2)
122-133: Consider more helpful error message for broadcasting failures.The error message could be more informative by including the actual shapes.
if (patch_ndims := patch.ndim) < (array_ndims := other.ndim): - msg = f"Cannot broadcast patch/array {patch_ndims=} {array_ndims=}" + msg = ( + f"Cannot broadcast patch (shape={patch.shape}) with array " + f"(shape={other.shape}): patch has fewer dimensions" + ) raise PatchBroadcastError(msg)
339-339: Remove redundant assertion.This assertion is redundant with the one on line 337.
assert len(patches), "No patches found in apply_array_func" first_patch = patches[0] - assert len(patches) > 0 # Call the array function
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
dascore/utils/array.py(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
dascore/utils/array.py (8)
dascore/compat.py (2)
array(31-37)is_array(40-43)dascore/exceptions.py (3)
ParameterError(26-27)PatchBroadcastError(62-63)UnitError(106-107)dascore/units.py (1)
get_quantity(70-94)dascore/utils/misc.py (1)
iterate(256-268)dascore/utils/patch.py (4)
_merge_aligned_coords(879-897)_merge_models(900-924)align_patch_coords(731-781)get_dim_axis_value(558-642)dascore/utils/time.py (5)
dtype_time_like(415-425)is_datetime64(405-407)to_datetime64(22-55)to_timedelta64(141-173)to_float(327-333)dascore/core/patch.py (8)
dtype(236-238)data(221-223)coords(216-218)attrs(211-213)shape(226-228)ndim(201-203)Patch(28-405)dims(196-198)dascore/proc/basic.py (1)
update(167-207)
🪛 Ruff (0.12.2)
dascore/utils/array.py
408-408: Unused function argument: self
(ARG001)
416-416: Unused function argument: self
(ARG001)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (17)
- GitHub Check: test_code_min_deps (windows-latest, 3.13)
- GitHub Check: test_code (windows-latest, 3.13)
- GitHub Check: test_code (windows-latest, 3.12)
- GitHub Check: test_code (macos-latest, 3.11)
- GitHub Check: test_code (windows-latest, 3.11)
- GitHub Check: test_code_min_deps (ubuntu-latest, 3.13)
- GitHub Check: test_code (windows-latest, 3.10)
- GitHub Check: test_code_min_deps (ubuntu-latest, 3.12)
- GitHub Check: test_code_min_deps (macos-latest, 3.12)
- GitHub Check: test_code_min_deps (windows-latest, 3.12)
- GitHub Check: test_code (ubuntu-latest, 3.13)
- GitHub Check: test_code (ubuntu-latest, 3.11)
- GitHub Check: test_code (macos-latest, 3.12)
- GitHub Check: test_code (macos-latest, 3.13)
- GitHub Check: test_code (macos-latest, 3.10)
- GitHub Check: test_code (ubuntu-latest, 3.12)
- GitHub Check: test_code (ubuntu-latest, 3.10)
🔇 Additional comments (9)
dascore/utils/array.py (9)
33-39: LGTM! Clever workaround for introspection limitations.These dummy functions provide signatures for ufunc methods that don't normally have them, enabling the introspection logic in
_reassemble_patchto work correctly.
41-63: LGTM! Clean unary ufunc implementation.The implementation correctly applies unary operations while preserving patch metadata.
140-155: LGTM! Comprehensive unit handling with clear error messages.The function properly handles unit-aware operations, wrapping DimensionalityError with descriptive context about which operation failed with which units.
183-213: LGTM! Robust datetime/timedelta handling in reductions.The function correctly handles time-like coordinates by falling back to float conversion when direct operations fail, then re-packing to the appropriate datetime/timedelta type.
215-231: LGTM! Flexible aggregation with dimension handling.The function properly handles multiple dimensions and correctly manages coordinate updates based on the reduction strategy.
245-293: Excellent axis normalization with deduplication.The
_normalize_axesfunction properly handles all axis specifications (None, int, sequence) and deduplicates while preserving order. The signature binding is correctly done withbind_partialto allow defaulted parameters.
295-327: LGTM! Smart patch reassembly with fallback for ufunc methods.The function correctly handles both regular functions with signatures and ufunc methods (reduce/accumulate) that lack them by using the dummy functions.
408-414: Unusedselfparameter - expected for protocol.The
selfparameter is intentionally unused as this function implements the__array_ufunc__protocol. The static analysis warning can be safely ignored.
416-433: Unusedselfparameter - expected for protocol.The
selfparameter is intentionally unused as this function implements the__array_function__protocol. The static analysis warning can be safely ignored.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
dascore/core/patch.py (1)
148-155: Bug: reverse operator aliases are incorrect for non-commutative ops.
rsub, rtruediv, rfloordiv, rpow, and rmod must reverse operand order; aliasing to forward ops yields wrong results.- __radd__ = __add__ - __rsub__ = __sub__ - __rmul__ = __mul__ - __rpow__ = __pow__ - __rtruediv__ = __truediv__ - __rfloordiv__ = __floordiv__ - __rmod__ = __mod__ + __radd__ = __add__ + __rmul__ = __mul__ + + def __rsub__(self, other): + return dascore.utils.array.apply_ufunc(np.subtract, other, self) + + def __rtruediv__(self, other): + return dascore.utils.array.apply_ufunc(np.divide, other, self) + + def __rfloordiv__(self, other): + return dascore.utils.array.apply_ufunc(np.floor_divide, other, self) + + def __rpow__(self, other): + return dascore.utils.array.apply_ufunc(np.power, other, self) + + def __rmod__(self, other): + return dascore.utils.array.apply_ufunc(np.mod, other, self)
🧹 Nitpick comments (16)
tests/test_utils/test_patch_utils.py (1)
651-700: Solid coverage for dim_to_axis; add one negative test.
The positive cases are covered well (list, None, passthrough, single str). Consider adding a test that passes an unknown dim (e.g., "foo") to assert it raises ValueError from list.index, which clarifies failure semantics.+ def test_dim_to_axis_unknown_dim_raises(self, random_patch): + args = () + kwargs = {"dim": "foo"} + with pytest.raises(ValueError): + dim_to_axis(random_patch, args, kwargs)docs/tutorial/patch.qmd (4)
637-677: Clarify numpy-like operations wording.
“Applied directly to a patch using built-in python operators” is accurate for operators/ufuncs. Consider adding one sentence that non-ufunc numpy functions (routines routed via array_function) require axis, and DASCore keeps reduced dimensions with length 1 by default.
681-693: State the protocol names explicitly.
Since this section introduces the protocol, add “(array_ufunc and array_function)” to make it explicit for users scanning docs.
715-748: Broadcast example: show shape before/after.
Briefly print shapes to reinforce broadcasting behavior for readers.
750-803: PatchUFunc section reads well; add note on dim vs axis.
Mention that PatchUFunc.reduce/accumulate accept dim (mapped internally to axis), while numpy ufuncs accept axis.dascore/utils/array.py (4)
140-153: Unit-error message may crash if other lacks .units.
When DimensionalityError occurs and other is a Unit without .units (or plain scalar), f-string will raise AttributeError. Guard via getattr.- except DimensionalityError as er: - msg = f"{operator} failed with units {data_units} and {other.units}" + except DimensionalityError as er: + other_units = getattr(other, "units", None) + msg = f"{operator} failed with units {data_units} and {other_units}" raise UnitError(msg) from er
121-133: Edge-case: array has more dims than patch.
Raising immediately if array_ndims > patch_ndims is strict; broadcasting can still succeed when leading dims are length-1 after make_broadcastable_to. Consider deferring to make_broadcastable_to or numpy.broadcast_shapes and only raise on failure.- if (patch_ndims := patch.ndim) < (array_ndims := other.ndim): - msg = f"Cannot broadcast patch/array {patch_ndims=} {array_ndims=}" - raise PatchBroadcastError(msg) - patch = patch.make_broadcastable_to(other.shape) + try: + # Let Patch decide how to align; raise if not possible. + patch = patch.make_broadcastable_to(other.shape) + except Exception as er: + raise PatchBroadcastError( + f"Cannot broadcast patch of shape {patch.shape} to {other.shape}" + ) from er
509-515: Out parameter unsupported: fail fast with clear error.
NumPy passes out= arrays to array_ufunc. Since we always construct a new Patch, reject out to avoid silent misuse.def patch_array_ufunc(self, ufunc, method, *inputs, **kwargs): @@ - method = ufunc if method == "__call__" else getattr(ufunc, method, ufunc) + if "out" in kwargs and kwargs["out"] is not None: + raise ParameterError("The 'out' kwarg is not supported for Patch ufuncs.") + method = ufunc if method == "__call__" else getattr(ufunc, method, ufunc) return apply_ufunc(method, *inputs, **kwargs)
517-535: Prefer NotImplemented over assert for array_function.
Asserting a Patch type is involved can crash in unexpected dispatch paths. Return NotImplemented to let NumPy fall back when appropriate.- # Only handle functions involving Patches - assert any(issubclass(t, dc.Patch) for t in types) - return apply_array_func(func, *args, **kwargs) + if any(issubclass(t, dc.Patch) for t in types): + return apply_array_func(func, *args, **kwargs) + return NotImplementeddascore/core/patch.py (1)
250-255: .T property is fine; consider dropping unused noqa.
If the linter isn’t enforcing N802, the noqa can be removed. Otherwise keep as-is.tests/test_utils/test_array_utils.py (6)
21-26: Typo in test name; add dtype check for boolean ufuncs.Rename “uniary” → “unary”. Also assert bool dtype for np.isfinite to harden behavior.
Apply this diff:
- @pytest.mark.parametrize("func", (np.abs, np.tan, np.isfinite, np.exp)) - def test_uniary_ufuncs(self, func, random_patch): + @pytest.mark.parametrize("func", (np.abs, np.tan, np.isfinite, np.exp)) + def test_unary_ufuncs(self, func, random_patch): """Ensure ufuncs that take a single input also work.""" out = func(random_patch) assert isinstance(out, dc.Patch) + if func is np.isfinite: + assert out.data.dtype == np.bool_
50-58: Make xfail strict to catch future XPASS when Pint interop improves.This guards against the test silently “passing” when the upstream behavior changes.
- @pytest.mark.xfail(raises=(UnitError, DimensionalityError)) + @pytest.mark.xfail(raises=(UnitError, DimensionalityError), strict=True) def test_reversed_unit_and_quantity(self, random_patch):
111-114: Normalize dimensionless-units assertion (use get_quantity on both sides).Current check mixes raw attr with a Quantity; align with earlier pattern to avoid false negatives.
- new = apply_ufunc(np.divide, patch, other) - new_units = get_quantity("m/s") / get_quantity("m/s") - assert new.attrs.data_units is None or new.attrs.data_units == new_units - assert np.allclose(new.data, random_patch.data) + new = apply_ufunc(np.divide, patch, other) + # Dimensionless result: units may be omitted; accept None or "1". + q = get_quantity(new.attrs.data_units) + assert q is None or q == get_quantity("1") + assert np.allclose(new.data, random_patch.data)
169-173: Remove redundant assertion.
set(mapping)already yields the keys; the second check duplicates the first.- assert out_coord_keys == input_coord_keys - assert set(out.coords.coord_map) == set(random_dft_patch.coords.coord_map) + assert out_coord_keys == input_coord_keys
466-471: Silence Ruff ARG001 by marking the unused param.The argument is intentionally unused; prefix with underscore (and optionally keep a noqa).
- def shape_changingFunc(data): + def shape_changing_func(_data): # noqa: ARG001 # Return a different shape to trigger the error path return np.array([1, 2, 3]) # Always return same small array
27-37: Add direct tests for boolean comparisons (core PR objective).Missing explicit checks for patch > scalar, scalar > patch, <=, >=, and dtype=bool. Suggest adding below in TestApplyUfunc.
+ def test_boolean_comparisons(self, random_patch): + pa = random_patch + gt = pa > 0 + assert isinstance(gt, dc.Patch) + assert gt.data.dtype == np.bool_ + # reversed + rgt = 0 < pa + assert rgt.equals(gt) + # equality across self should be all True for <= and >= + assert np.all((pa <= pa).data) + assert np.all((pa >= pa).data) + + def test_boolean_comparisons_with_units(self, random_patch): + pa = random_patch.set_units("m/s") + q = 10 * m / s + out = pa > q + assert isinstance(out, dc.Patch) + assert out.data.dtype == np.bool_If you want, I can open a follow-up PR with these tests.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (7)
dascore/core/patch.py(7 hunks)dascore/utils/array.py(1 hunks)dascore/utils/patch.py(4 hunks)docs/tutorial/patch.qmd(4 hunks)tests/test_core/test_patch.py(5 hunks)tests/test_utils/test_array_utils.py(1 hunks)tests/test_utils/test_patch_utils.py(2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- dascore/utils/patch.py
🧰 Additional context used
🧬 Code graph analysis (5)
tests/test_utils/test_patch_utils.py (4)
dascore/utils/patch.py (1)
dim_to_axis(1183-1225)dascore/examples.py (1)
random_patch(29-111)tests/conftest.py (1)
random_patch(282-284)dascore/core/patch.py (1)
dims(196-198)
dascore/utils/array.py (7)
dascore/compat.py (2)
array(31-37)is_array(40-43)dascore/exceptions.py (3)
ParameterError(26-27)PatchBroadcastError(62-63)UnitError(106-107)dascore/units.py (1)
get_quantity(70-94)dascore/utils/misc.py (1)
iterate(256-268)dascore/utils/patch.py (5)
_merge_aligned_coords(879-897)_merge_models(900-924)align_patch_coords(731-781)dim_to_axis(1183-1225)get_dim_axis_value(558-642)dascore/utils/time.py (5)
dtype_time_like(415-425)is_datetime64(405-407)to_datetime64(22-55)to_timedelta64(141-173)to_float(327-333)dascore/proc/basic.py (1)
update(167-207)
tests/test_core/test_patch.py (5)
dascore/exceptions.py (2)
CoordError(38-39)ParameterError(26-27)dascore/core/patch.py (5)
T(251-254)data(221-223)dtype(236-238)Patch(28-422)dims(196-198)dascore/proc/coords.py (1)
transpose(553-584)dascore/core/coordmanager.py (2)
transpose(812-839)new(431-449)dascore/utils/array.py (4)
accumulate(202-206)accumulate(270-274)reduce(196-200)reduce(266-268)
dascore/core/patch.py (2)
dascore/utils/array.py (4)
PatchUFunc(209-274)patch_array_function(517-534)patch_array_ufunc(509-514)apply_ufunc(461-506)dascore/proc/basic.py (1)
bool_patch(156-164)
tests/test_utils/test_array_utils.py (4)
dascore/units.py (1)
get_quantity(70-94)dascore/exceptions.py (2)
ParameterError(26-27)UnitError(106-107)dascore/utils/array.py (8)
PatchUFunc(209-274)apply_array_func(424-449)apply_ufunc(461-506)reduce(196-200)reduce(266-268)accumulate(202-206)accumulate(270-274)_BoundPatchUFunc(183-206)dascore/core/patch.py (3)
Patch(28-422)data(221-223)dtype(236-238)
🪛 Ruff (0.12.2)
dascore/utils/array.py
509-509: Unused function argument: self
(ARG001)
517-517: Unused function argument: self
(ARG001)
dascore/core/patch.py
251-251: Unused noqa directive (non-enabled: N802)
Remove unused noqa directive
(RUF100)
tests/test_utils/test_array_utils.py
467-467: Unused function argument: data
(ARG001)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (18)
- GitHub Check: test_build_docs
- GitHub Check: test_code (windows-latest, 3.10)
- GitHub Check: test_code (windows-latest, 3.12)
- GitHub Check: test_code (ubuntu-latest, 3.13)
- GitHub Check: test_code (macos-latest, 3.13)
- GitHub Check: test_code (macos-latest, 3.12)
- GitHub Check: test_code (ubuntu-latest, 3.12)
- GitHub Check: test_code (windows-latest, 3.13)
- GitHub Check: test_code (windows-latest, 3.11)
- GitHub Check: test_code (ubuntu-latest, 3.11)
- GitHub Check: test_code (macos-latest, 3.11)
- GitHub Check: test_code (ubuntu-latest, 3.10)
- GitHub Check: test_code_min_deps (ubuntu-latest, 3.13)
- GitHub Check: test_code_min_deps (macos-latest, 3.13)
- GitHub Check: test_code_min_deps (windows-latest, 3.13)
- GitHub Check: test_code_min_deps (ubuntu-latest, 3.12)
- GitHub Check: test_code_min_deps (windows-latest, 3.12)
- GitHub Check: test_code_min_deps (macos-latest, 3.12)
🔇 Additional comments (17)
tests/test_utils/test_patch_utils.py (1)
26-26: Import looks good; public API addition is exercised.
No issues with importing dim_to_axis from dascore.utils.patch.tests/test_core/test_patch.py (7)
17-17: Public exceptions import OK.
Importing ParameterError from dascore.exceptions matches public API exposure.
427-430: .T property test is appropriate.
Confirms alias to transpose(); good catch for numpy ergonomics.
620-624: Comparator coverage is a good addition.
Adding ge/le/gt/lt to the ops tuple ensures the boolean ops are actually exercised.
644-645: apply_operator call-site matches updated signature.
np.add first, then (patch, array) aligns with the new API; good.
647-652: Behavioral check for comparison ops is minimal but effective.
Verifies dtype and a non-trivial truth result; sufficient here.
655-671: Bool semantics tests cover the intended numpy parity.
Ambiguity on multi-element and truthiness on single element both validated.
768-835: Excellent numpy protocol coverage.
Reducer, accumulator, reductions over multiple axes, and patch-on-patch all covered. The at() error is also asserted.dascore/utils/array.py (4)
42-64: Unary ufunc path LGTM.
Simple, correct, and keeps coords/attrs intact as expected for shape-preserving ops.
183-207: Descriptor-bound PatchUFunc is clean and idiomatic.
Binding and reduce/accumulate routing read well.
277-307: Coordinate reduction policy is sensible.
empty/squeeze/aggregator handling is clear; datetime/timedelta fallback is a nice touch.
391-422: Reassembly logic is robust.
Signature fallback for ufunc methods and axis-based re-expansion to keep dimensionality is well designed.dascore/core/patch.py (4)
110-141: Forward operators wired correctly to apply_ufunc.
Arithmetic and comparison overloads look good.
160-163: NumPy protocol hookup LGTM.
Class-level array_ufunc/array_function and priority are appropriate.
231-239: dtype/size properties are useful ergonomics.
Returning underlying data dtype and coord-derived size is consistent with numpy semantics.
365-385: PatchUFunc convenience attributes are a nice API.
Exposing add/subtract/etc with reduce/accumulate improves discoverability.tests/test_utils/test_array_utils.py (1)
18-20: Good coverage and operator-first usage of apply_ufunc.Tests exercise unary/binary, reversed ops, reductions, quantities/units, and descriptor binding. Operator-first calls align with the utils API.
|
✅ Documentation built: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (1)
dascore/utils/array.py (1)
180-183: Bug: invalid isinstance union — raises at runtime
isinstance(other, Quantity | Unit)raises TypeError. Use a tuple of types.Apply this diff:
- if isinstance(other, Quantity | Unit): + if isinstance(other, (Quantity, Unit)): new_data, attrs = _apply_op_units(patch, other, operator, attrs, reversed) else: new_data = _apply_op(patch.data, other, operator, reversed)
🧹 Nitpick comments (9)
dascore/utils/patch.py (1)
1183-1234: Handle integer dim, remove inner imports, and tighten typing in swap_kwargs_dim_to_axis
- Passing dim as an int currently fails (TypeError: 'int' object is not iterable). Support ints by forwarding them directly to axis.
- Avoid importing ParameterError inside the loop; it’s already imported at module scope.
- Optional: add type hints for better API clarity and future linting.
Apply this diff:
-def swap_kwargs_dim_to_axis(patch, kwargs): +def swap_kwargs_dim_to_axis(patch: PatchType, kwargs: Mapping[str, Any]) -> dict: @@ - if dim is not None: - if isinstance(dim, str): + if dim is not None: + if isinstance(dim, str): if dim not in patch.dims: - from dascore.exceptions import ParameterError - msg = f"Dimension '{dim}' not found in patch dimensions {patch.dims}" raise ParameterError(msg) axis = patch.dims.index(dim) - else: + elif isinstance(dim, Sequence) and not isinstance(dim, (str, bytes)): # Handle sequence of dimensions axis = [] for d in dim: if d not in patch.dims: - from dascore.exceptions import ParameterError - msg = f"Dimension '{d}' not found in patch dimensions {patch.dims}" raise ParameterError(msg) axis.append(patch.dims.index(d)) + elif isinstance(dim, int): + axis = dim + else: + raise ParameterError(f"Unsupported dim type: {type(dim)!r}") new_kwargs["axis"] = axisAlso, in the example comment, rename
new_kwargtonew_kwargsfor consistency.docs/tutorial/patch.qmd (3)
681-683: Wording: clarify NumPy integration terminology“implements the numpy array protocol” can be misread as array (conversion). This section is about duck-array protocols (array_function, array_ufunc). Consider “Patch integrates with NumPy’s duck-array protocols (array_function, array_ufunc).”
753-756: Rename apply_array_ufunc → apply_ufunc in textThe function is named apply_ufunc; the text says apply_array_ufunc. Update wording to avoid confusion; the link target is already correct.
- DASCore uses [apply_array_ufunc](`dascore.utils.array.apply_ufunc`) and [array_function](`dascore.utils.array.apply_array_func`) + DASCore uses [apply_ufunc](`dascore.utils.array.apply_ufunc`) and [`apply_array_func`](`dascore.utils.array.apply_array_func`)
785-787: Note formattingUse consistent code formatting for names: PatchUFunc.reduce/accumulate are methods; surround with backticks to match surrounding style.
tests/test_core/test_patch.py (1)
792-858: Great NumPy interoperability coverageExercises reducers, accumulators, full reductions, multi-axis, and bound PatchUFunc paths; also tests ufunc.at error. Consider adding a keepdims=True case for reducers.
dascore/utils/array.py (3)
523-531: Silence ARG001 by marking self unused in patch_array_ufuncRename the parameter to
_selfto match its unused role and satisfy linters.-def patch_array_ufunc(self, ufunc, method, *inputs, **kwargs): +def patch_array_ufunc(_self, ufunc, method, *inputs, **kwargs):
532-550: Silence ARG001 by marking self unused in patch_array_functionSame as above; rename to
_self.-def patch_array_function(self, func, types, args, kwargs): +def patch_array_function(_self, func, types, args, kwargs):
439-457: Multiple-patch array functions not supported (FYI)apply_array_func uses the first Patch for reassembly and dim→axis mapping. That’s fine for reducers (single-operand), but document/guard multi-Patch calls via array_function if ever exposed.
tests/test_utils/test_array_utils.py (1)
467-470: Remove unused parameter to fix linter warning.The
dataparameter in theshape_changing_funcis unused, which triggers a static analysis warning.Apply this diff to fix the linter warning:
- def shape_changing_func(data): + def shape_changing_func(_): # Return a different shape to trigger the error path return np.array([1, 2, 3]) # Always return same small array
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
dascore/utils/array.py(1 hunks)dascore/utils/patch.py(4 hunks)docs/tutorial/patch.qmd(4 hunks)tests/test_core/test_patch.py(5 hunks)tests/test_utils/test_array_utils.py(1 hunks)tests/test_utils/test_patch_utils.py(2 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
dascore/utils/array.py (6)
dascore/compat.py (2)
array(31-37)is_array(40-43)dascore/exceptions.py (3)
ParameterError(26-27)PatchBroadcastError(62-63)UnitError(106-107)dascore/units.py (1)
get_quantity(70-94)dascore/utils/misc.py (1)
iterate(256-268)dascore/utils/patch.py (5)
_merge_aligned_coords(879-897)_merge_models(900-924)align_patch_coords(731-781)get_dim_axis_value(558-642)swap_kwargs_dim_to_axis(1183-1234)dascore/utils/time.py (5)
dtype_time_like(415-425)is_datetime64(405-407)to_datetime64(22-55)to_timedelta64(141-173)to_float(327-333)
tests/test_utils/test_patch_utils.py (4)
dascore/utils/patch.py (1)
swap_kwargs_dim_to_axis(1183-1234)tests/conftest.py (1)
random_patch(282-284)dascore/core/patch.py (1)
dims(196-198)dascore/exceptions.py (1)
ParameterError(26-27)
dascore/utils/patch.py (2)
dascore/core/patch.py (1)
dims(196-198)dascore/exceptions.py (1)
ParameterError(26-27)
tests/test_utils/test_array_utils.py (3)
dascore/units.py (1)
get_quantity(70-94)dascore/exceptions.py (2)
ParameterError(26-27)UnitError(106-107)dascore/utils/array.py (8)
PatchUFunc(215-280)apply_array_func(430-456)apply_ufunc(468-513)reduce(202-206)reduce(272-274)accumulate(208-212)accumulate(276-280)_BoundPatchUFunc(189-212)
tests/test_core/test_patch.py (4)
dascore/exceptions.py (2)
CoordError(38-39)ParameterError(26-27)dascore/core/patch.py (4)
T(251-254)data(221-223)Patch(28-422)dims(196-198)dascore/proc/coords.py (1)
transpose(553-584)dascore/core/coordmanager.py (2)
transpose(812-839)new(431-449)
🪛 Ruff (0.12.2)
dascore/utils/array.py
523-523: Unused function argument: self
(ARG001)
532-532: Unused function argument: self
(ARG001)
tests/test_utils/test_array_utils.py
468-468: Unused function argument: data
(ARG001)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (18)
- GitHub Check: test_code (macos-latest, 3.10)
- GitHub Check: test_code (windows-latest, 3.12)
- GitHub Check: test_code (windows-latest, 3.11)
- GitHub Check: test_code (ubuntu-latest, 3.12)
- GitHub Check: test_code (windows-latest, 3.13)
- GitHub Check: test_code (ubuntu-latest, 3.13)
- GitHub Check: test_code (windows-latest, 3.10)
- GitHub Check: test_code (ubuntu-latest, 3.11)
- GitHub Check: test_code (ubuntu-latest, 3.10)
- GitHub Check: test_code (macos-latest, 3.13)
- GitHub Check: test_code (macos-latest, 3.12)
- GitHub Check: test_code_min_deps (windows-latest, 3.12)
- GitHub Check: test_code_min_deps (windows-latest, 3.13)
- GitHub Check: test_code_min_deps (macos-latest, 3.12)
- GitHub Check: test_code_min_deps (ubuntu-latest, 3.12)
- GitHub Check: test_code_min_deps (ubuntu-latest, 3.13)
- GitHub Check: test_code_min_deps (macos-latest, 3.13)
- GitHub Check: test_build_docs
🔇 Additional comments (14)
dascore/utils/patch.py (1)
568-616: Docstring tweaks for get_dim_axis_value look goodClarity/readability improved; no functional changes.
tests/test_utils/test_patch_utils.py (1)
651-711: Good coverage of swap_kwargs_dim_to_axisCovers single, multiple, None, missing, and error paths. Consider adding a case where dim is an int (eg, {"dim": 1}) once function supports it.
tests/test_core/test_patch.py (4)
17-17: Import updates look goodSwitching to public ParameterError import from dascore.exceptions aligns with exported API.
427-430: Nice .T alias testValidates transpose alias behavior.
645-645: Updated apply_operator call-site matches new signatureThe (op, patch, other) order is consistent with the refactor.
647-652: Boolean comparison assertions are solidCovers dtype, truthiness, and self-comparisons.
dascore/utils/array.py (1)
121-133: Patch.make_broadcastable_to exists and delegates to CoordManager; prepending new axes is not supported by the binary-op path.Defined at dascore/proc/coords.py (calls self.coords.make_broadcastable_to) -> dascore/core/coordmanager.py implements the logic: it uses np.broadcast_shapes and only expands a dimension when the coord is partial (coord._partial) or drop_coords=True, otherwise raises PatchBroadcastError. Note dascore/utils/array._ensure_array_compatible raises if patch.ndim < other.ndim, so cases where the other array would require prepending new axes are rejected before make_broadcastable_to is called.
tests/test_utils/test_array_utils.py (7)
216-224: Comprehensive PatchUFunc basic usage test.Good validation of the wrapper functionality with proper type checking and data verification.
225-259: Excellent accumulate and reduce method testing.These tests properly validate the dimension-aware operations with correct axis handling and expected data transformations.
260-268: Good introspection validation.Ensures the wrapper maintains proper function metadata for debugging and inspection purposes.
269-288: Thorough descriptor protocol testing.This test validates both the binding mechanism and equivalence between bound and unbound calls.
464-479: Good error handling test for shape-changing functions.The test properly validates that functions without axis parameters that change shape are rejected with appropriate error messages.
480-496: Good same-shape function validation.This test ensures that shape-preserving functions without axis parameters work correctly.
497-516: Comprehensive element-wise function test.This test validates coordinate and attribute preservation in shape-preserving operations, which is crucial for patch integrity.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 5
♻️ Duplicate comments (1)
dascore/utils/array.py (1)
180-183: Critical: isinstance with union type raises at runtime.
isinstance(other, Quantity | Unit)throwsTypeError. Use a tuple.- if isinstance(other, Quantity | Unit): + if isinstance(other, (Quantity, Unit)): new_data, attrs = _apply_op_units(patch, other, operator, attrs, reversed) else: new_data = _apply_op(patch.data, other, operator, reversed)
🧹 Nitpick comments (3)
dascore/utils/array.py (2)
526-533: Silence unusedselfwarnings without changing behavior.Minor lint-only tweak to satisfy Ruff ARG001.
def patch_array_ufunc(self, ufunc, method, *inputs, **kwargs): """ Called when a numpy ufunc is applied to a patch (__array_ufunc__). """ - _raise_on_out(kwargs) + # `self` is unused by design in this dispatcher. + del self + _raise_on_out(kwargs)
535-553: Same minor lint fix for__array_function__.def patch_array_function(self, func, types, args, kwargs): """ Intercept NumPy functions for patch operations. """ - # Only handle functions involving Patches - _raise_on_out(kwargs) + # Only handle functions involving Patches + del self + _raise_on_out(kwargs)tests/test_utils/test_array_utils.py (1)
460-465: Add tests to lock in unitless results for boolean ops.To prevent regressions, assert that boolean outputs clear
data_unitsfor both unary and binary cases.@@ class TestApplyUfunc: @@ def test_out_parameter_raises(self, random_patch): """Since patches are immutable, we cant support out. Raise if provided.""" match = "cannot be used" with pytest.raises(ParameterError, match=match): apply_ufunc(np.add, random_patch, random_patch, out=random_patch) + + def test_binary_comparison_drops_units(self, random_patch): + """Boolean results must be unitless.""" + pa = random_patch.set_units("m") + out = apply_ufunc(np.greater, pa, 2.0) + assert out.data.dtype == np.bool_ + assert out.attrs.data_units is None + + def test_unary_boolean_drops_units(self, random_patch): + """Unary boolean ufuncs must be unitless.""" + pa = random_patch.set_units("s") + out = np.isfinite(pa) + assert out.data.dtype == np.bool_ + assert out.attrs.data_units is None
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
dascore/utils/array.py(1 hunks)tests/test_utils/test_array_utils.py(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
dascore/utils/array.py (7)
dascore/compat.py (2)
array(31-37)is_array(40-43)dascore/exceptions.py (3)
ParameterError(26-27)PatchBroadcastError(62-63)UnitError(106-107)dascore/units.py (1)
get_quantity(70-94)dascore/utils/misc.py (1)
iterate(256-268)dascore/utils/patch.py (5)
_merge_aligned_coords(879-897)_merge_models(900-924)align_patch_coords(731-781)get_dim_axis_value(558-642)swap_kwargs_dim_to_axis(1183-1234)dascore/utils/time.py (5)
dtype_time_like(415-425)is_datetime64(405-407)to_datetime64(22-55)to_timedelta64(141-173)to_float(327-333)dascore/core/patch.py (7)
dtype(236-238)data(221-223)coords(216-218)attrs(211-213)shape(226-228)Patch(28-422)dims(196-198)
tests/test_utils/test_array_utils.py (5)
dascore/units.py (1)
get_quantity(70-94)dascore/exceptions.py (2)
ParameterError(26-27)UnitError(106-107)dascore/compat.py (1)
array(31-37)dascore/utils/array.py (8)
PatchUFunc(215-280)apply_array_func(430-456)apply_ufunc(468-513)reduce(202-206)reduce(272-274)accumulate(208-212)accumulate(276-280)_BoundPatchUFunc(189-212)dascore/core/patch.py (3)
Patch(28-422)data(221-223)dtype(236-238)
🪛 Ruff (0.12.2)
dascore/utils/array.py
526-526: Unused function argument: self
(ARG001)
535-535: Unused function argument: self
(ARG001)
tests/test_utils/test_array_utils.py
474-474: Unused function argument: data
(ARG001)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (18)
- GitHub Check: test_code (windows-latest, 3.10)
- GitHub Check: test_code (windows-latest, 3.11)
- GitHub Check: test_code (windows-latest, 3.12)
- GitHub Check: test_code (macos-latest, 3.10)
- GitHub Check: test_code (ubuntu-latest, 3.12)
- GitHub Check: test_code (ubuntu-latest, 3.13)
- GitHub Check: test_code (windows-latest, 3.13)
- GitHub Check: test_code (macos-latest, 3.12)
- GitHub Check: test_code (ubuntu-latest, 3.11)
- GitHub Check: test_code (ubuntu-latest, 3.10)
- GitHub Check: test_code (macos-latest, 3.11)
- GitHub Check: test_code_min_deps (windows-latest, 3.13)
- GitHub Check: test_code_min_deps (windows-latest, 3.12)
- GitHub Check: test_code_min_deps (macos-latest, 3.13)
- GitHub Check: test_code_min_deps (ubuntu-latest, 3.12)
- GitHub Check: test_code_min_deps (macos-latest, 3.12)
- GitHub Check: test_code_min_deps (ubuntu-latest, 3.13)
- GitHub Check: test_build_docs
🔇 Additional comments (2)
tests/test_utils/test_array_utils.py (2)
21-33: Good coverage on unary ufuncs and dtype checks.
144-150: Nice incompatible-units guard.This ensures Pint errors surface as
UnitError. Good.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (4)
dascore/utils/array.py (4)
180-183: Fix invalid isinstance union check (runtime TypeError).
isinstance(other, Quantity | Unit)raises at runtime; use a tuple.- if isinstance(other, Quantity | Unit): + if isinstance(other, (Quantity, Unit)): new_data, attrs = _apply_op_units(patch, other, operator, attrs, reversed) else: new_data = _apply_op(patch.data, other, operator, reversed)
359-362: Fix invalidisinstance(axis, str | bytes)(runtime TypeError).Use a tuple for the second arg to
isinstance.- if isinstance(axis, Iterable) and not isinstance(axis, str | bytes): + if isinstance(axis, Iterable) and not isinstance(axis, (str, bytes)): axes = tuple(int(a) for a in axis) else: axes = (int(axis),)
62-64: Boolean unary ufuncs must clear units.Unary ops like
np.isfinite,np.isnanshould return unitless patches.- out = operator(patch.data, *args, **kwargs) - return patch.new(data=out) + out = operator(patch.data, *args, **kwargs) + attrs = patch.attrs + if hasattr(out, "dtype") and np.issubdtype(np.asarray(out).dtype, np.bool_): + attrs = attrs.update(data_units=None) + return patch.new(data=out, attrs=attrs)
179-186: Clear units for boolean results in non‑unit binary path.Comparisons should yield unitless patches.
- else: - new_data = _apply_op(patch.data, other, operator, reversed) + else: + new_data = _apply_op(patch.data, other, operator, reversed) + if hasattr(new_data, "dtype") and np.issubdtype( + np.asarray(new_data).dtype, np.bool_ + ): + attrs = attrs.update(data_units=None) new = patch.new(data=new_data, coords=coords, attrs=attrs)Want a quick test asserting
(patch > 0).attrs.data_units is Noneand same for unaryisfinite?
🧹 Nitpick comments (1)
dascore/utils/array.py (1)
528-533: Silence Ruff ARG001 by prefixing unusedselfparameters.These are required by NumPy’s protocol but unused here.
-def patch_array_ufunc(self, ufunc, method, *inputs, **kwargs): +def patch_array_ufunc(_patch, ufunc, method, *inputs, **kwargs): @@ -def patch_array_function(self, func, types, args, kwargs): +def patch_array_function(_patch, func, types, args, kwargs):Also applies to: 536-541
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
dascore/utils/array.py(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
dascore/utils/array.py (7)
dascore/compat.py (2)
array(31-37)is_array(40-43)dascore/exceptions.py (3)
ParameterError(26-27)PatchBroadcastError(62-63)UnitError(106-107)dascore/units.py (1)
get_quantity(70-94)dascore/utils/misc.py (1)
iterate(256-268)dascore/utils/patch.py (5)
_merge_aligned_coords(879-897)_merge_models(900-924)align_patch_coords(731-781)get_dim_axis_value(558-642)swap_kwargs_dim_to_axis(1183-1234)dascore/utils/time.py (5)
dtype_time_like(415-425)is_datetime64(405-407)to_datetime64(22-55)to_timedelta64(141-173)to_float(327-333)dascore/core/patch.py (7)
dtype(236-238)data(221-223)coords(216-218)attrs(211-213)shape(226-228)Patch(28-422)dims(196-198)
🪛 Ruff (0.12.2)
dascore/utils/array.py
528-528: Unused function argument: self
(ARG001)
536-536: Unused function argument: self
(ARG001)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (16)
- GitHub Check: test_code (windows-latest, 3.11)
- GitHub Check: test_code (ubuntu-latest, 3.13)
- GitHub Check: test_code (windows-latest, 3.10)
- GitHub Check: test_code (macos-latest, 3.10)
- GitHub Check: test_code (windows-latest, 3.12)
- GitHub Check: test_code (windows-latest, 3.13)
- GitHub Check: test_code (macos-latest, 3.13)
- GitHub Check: test_code (macos-latest, 3.11)
- GitHub Check: test_code (ubuntu-latest, 3.10)
- GitHub Check: test_code_min_deps (windows-latest, 3.12)
- GitHub Check: test_code_min_deps (ubuntu-latest, 3.12)
- GitHub Check: test_build_docs
- GitHub Check: test_code_min_deps (windows-latest, 3.13)
- GitHub Check: test_code_min_deps (ubuntu-latest, 3.13)
- GitHub Check: test_code_min_deps (macos-latest, 3.13)
- GitHub Check: test_code_min_deps (macos-latest, 3.12)
🔇 Additional comments (3)
dascore/utils/array.py (3)
430-456: Good: immutability enforced for array functions (out=rejected).Early guard via
_raise_on_outis correct.
498-516: Good: immutability enforced for ufuncs (out=rejected).Consistent with Patch immutability and expected errors.
291-295: dc.to_float is exported and supports ndarrays — no change required.to_float is defined in dascore/utils/time.py (signature accepts np.ndarray) and re-exported in dascore/init.py, so using dc.to_float(data) here is safe.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
♻️ Duplicate comments (2)
dascore/utils/array.py (2)
187-203: Fix union-type isinstance (runtime TypeError) and avoid shadowing built-in ‘reversed’.
isinstance(other, Quantity | Unit)raises TypeError; use a tuple. Also, don’t shadow built-in reversed().- reversed = False + is_reversed = False if other_is_patch and not patch_is_patch: patch, other = other, patch - reversed = True + is_reversed = True @@ - if isinstance(other, Quantity | Unit): - new_data, attrs = _apply_op_units(patch, other, operator, attrs, reversed) + if isinstance(other, (Quantity, Unit)): + new_data, attrs = _apply_op_units( + patch, other, operator, attrs, reversed=is_reversed + ) else: - new_data = _apply_op(patch.data, other, operator, reversed) + new_data = _apply_op(patch.data, other, operator, reversed=is_reversed)
376-379: Fix invalid isinstance check usingstr | bytes.PEP 604 union cannot be used in isinstance. Use a tuple.
- if isinstance(axis, Iterable) and not isinstance(axis, str | bytes): + if isinstance(axis, Iterable) and not isinstance(axis, (str, bytes)): axes = tuple(int(a) for a in axis) else: axes = (int(axis),)
🧹 Nitpick comments (2)
dascore/utils/array.py (2)
536-541: Silence linter: unusedself(keeps protocol intact).Rename to
_selfto satisfy Ruff without behavior change.-def patch_array_ufunc(self, ufunc, method, *inputs, **kwargs): +def patch_array_ufunc(_self, ufunc, method, *inputs, **kwargs):
544-561: Silence linter: unusedself(keeps protocol intact).Same as above for array_function helper.
-def patch_array_function(self, func, types, args, kwargs): +def patch_array_function(_self, func, types, args, kwargs):
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
dascore/utils/array.py(1 hunks)tests/test_core/test_patch.py(5 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- tests/test_core/test_patch.py
🧰 Additional context used
🧬 Code graph analysis (1)
dascore/utils/array.py (7)
dascore/compat.py (2)
array(31-37)is_array(40-43)dascore/exceptions.py (3)
ParameterError(26-27)PatchBroadcastError(62-63)UnitError(106-107)dascore/units.py (1)
get_quantity(70-94)dascore/utils/misc.py (1)
iterate(256-268)dascore/utils/patch.py (5)
_merge_aligned_coords(879-897)_merge_models(900-924)align_patch_coords(731-781)get_dim_axis_value(558-642)swap_kwargs_dim_to_axis(1183-1234)dascore/utils/time.py (5)
dtype_time_like(415-425)is_datetime64(405-407)to_datetime64(22-55)to_timedelta64(141-173)to_float(327-333)dascore/core/patch.py (7)
dtype(236-238)data(221-223)coords(216-218)attrs(211-213)shape(226-228)Patch(28-422)dims(196-198)
🪛 Ruff (0.12.2)
dascore/utils/array.py
536-536: Unused function argument: self
(ARG001)
544-544: Unused function argument: self
(ARG001)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (18)
- GitHub Check: test_build_docs
- GitHub Check: test_code (windows-latest, 3.13)
- GitHub Check: test_code (windows-latest, 3.12)
- GitHub Check: test_code (macos-latest, 3.12)
- GitHub Check: test_code (ubuntu-latest, 3.10)
- GitHub Check: test_code (macos-latest, 3.13)
- GitHub Check: test_code (ubuntu-latest, 3.13)
- GitHub Check: test_code (windows-latest, 3.11)
- GitHub Check: test_code (windows-latest, 3.10)
- GitHub Check: test_code (ubuntu-latest, 3.11)
- GitHub Check: test_code (macos-latest, 3.10)
- GitHub Check: test_code (ubuntu-latest, 3.12)
- GitHub Check: test_code_min_deps (windows-latest, 3.12)
- GitHub Check: test_code_min_deps (ubuntu-latest, 3.12)
- GitHub Check: test_code_min_deps (windows-latest, 3.13)
- GitHub Check: test_code_min_deps (macos-latest, 3.13)
- GitHub Check: test_code_min_deps (ubuntu-latest, 3.13)
- GitHub Check: test_code_min_deps (macos-latest, 3.12)
| def _clear_units_if_bool_dtype(patch): | ||
| """Clear the units on the patch if it is a boolean.""" | ||
| dtype = getattr(patch, "dtype", None) | ||
| if np.issubdtype(dtype, np.bool_): | ||
| return patch.update_attrs(data_units=None) | ||
| return patch |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Critical: boolean-unit clearing helper breaks all ufunc calls (bad dtype source, likely nonexistent API).
np.issubdtype(None, np.bool_) raises TypeError because dtype is read from the patch object, not its data. Also, Patch.update_attrs likely doesn’t exist; use patch.new(attrs=patch.attrs.update(...)).
Apply this diff:
-def _clear_units_if_bool_dtype(patch):
- """Clear the units on the patch if it is a boolean."""
- dtype = getattr(patch, "dtype", None)
- if np.issubdtype(dtype, np.bool_):
- return patch.update_attrs(data_units=None)
- return patch
+def _clear_units_if_bool_dtype(patch):
+ """Clear units when the patch's data dtype is boolean."""
+ dtype = np.asarray(patch.data).dtype
+ if np.issubdtype(dtype, np.bool_):
+ return patch.new(attrs=patch.attrs.update(data_units=None))
+ return patch📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| def _clear_units_if_bool_dtype(patch): | |
| """Clear the units on the patch if it is a boolean.""" | |
| dtype = getattr(patch, "dtype", None) | |
| if np.issubdtype(dtype, np.bool_): | |
| return patch.update_attrs(data_units=None) | |
| return patch | |
| def _clear_units_if_bool_dtype(patch): | |
| """Clear units when the patch's data dtype is boolean.""" | |
| dtype = np.asarray(patch.data).dtype | |
| if np.issubdtype(dtype, np.bool_): | |
| return patch.new(attrs=patch.attrs.update(data_units=None)) | |
| return patch |
🤖 Prompt for AI Agents
In dascore/utils/array.py around lines 52–57, the helper reads dtype from the
patch object and calls a nonexistent update_attrs, causing TypeError and
failures; instead retrieve dtype from the patch data (e.g., dtype =
getattr(patch, "data", None) and then getattr(data, "dtype", None) or use
np.asarray to get a dtype safely), check np.issubdtype only if dtype is not
None, and when clearing units return a new Patch without mutating by using the
Patch API (e.g., call patch.new(...) or construct a new attrs dict from
patch.attrs with data_units set to None) rather than patch.update_attrs.
| # We only support binary ops on patch. | ||
| assert patch_count >= 1, "apply_ufunc requires at least one Patch operand." | ||
| # Make sure patch is a patch. Reverse if needed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use explicit exception instead of assert for API validation.
Asserts can be stripped with -O. Raise ParameterError to enforce contract.
- assert patch_count >= 1, "apply_ufunc requires at least one Patch operand."
+ if patch_count < 1:
+ raise ParameterError("apply_ufunc requires at least one Patch operand.")📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| # We only support binary ops on patch. | |
| assert patch_count >= 1, "apply_ufunc requires at least one Patch operand." | |
| # Make sure patch is a patch. Reverse if needed. | |
| # We only support binary ops on patch. | |
| if patch_count < 1: | |
| raise ParameterError("apply_ufunc requires at least one Patch operand.") | |
| # Make sure patch is a patch. Reverse if needed. |
🤖 Prompt for AI Agents
In dascore/utils/array.py around lines 184 to 186, replace the assertion used to
validate that patch_count >= 1 with an explicit ParameterError raise to enforce
the API contract; change the code to check if patch_count < 1 and raise
ParameterError("apply_ufunc requires at least one Patch operand.") and ensure
ParameterError is imported from the module where it is defined (or added to the
file's imports) so the exception is always raised regardless of optimization
flags.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (4)
dascore/utils/array.py (4)
52-57: Fix boolean-unit clearing helper (wrong dtype source, likely bad API).This never clears units because Patch has no dtype; also
update_attrsmay not exist on Patch. Use data.dtype and Patch.new(attrs=attrs.update(...)).-def _clear_units_if_bool_dtype(patch): - """Clear the units on the patch if it is a boolean.""" - dtype = getattr(patch, "dtype", None) - if dtype is not None and np.issubdtype(dtype, np.bool_): - return patch.update_attrs(data_units=None) - return patch +def _clear_units_if_bool_dtype(patch): + """Clear units when the patch's data dtype is boolean.""" + dtype = np.asarray(patch.data).dtype + if np.issubdtype(dtype, np.bool_): + return patch.new(attrs=patch.attrs.update(data_units=None)) + return patch#!/bin/bash # Verify whether Patch exposes update_attrs anywhere (it likely doesn't). rg -nP 'def\s+update_attrs\s*\(' -C2 # Sanity: find any other dtype reads from Patch (should read from .data) rg -nP '\.dtype\b' -C2
184-186: Use explicit exception instead of assert (asserts can be stripped).Enforce API contract even under -O.
- assert patch_count >= 1, "apply_ufunc requires at least one Patch operand." + if patch_count < 1: + raise ParameterError("apply_ufunc requires at least one Patch operand.")
198-203: Critical: invalid isinstance union and missing boolean-unit clearing (binary path).
isinstance(x, Quantity | Unit)raises TypeError. Also comparisons produce bools; clear units when result is boolean.- if isinstance(other, Quantity | Unit): + if isinstance(other, (Quantity, Unit)): new_data, attrs = _apply_op_units(patch, other, operator, attrs, reversed) else: new_data = _apply_op(patch.data, other, operator, reversed) + # Boolean outputs must be unitless. + if hasattr(new_data, "dtype") and np.issubdtype(np.asarray(new_data).dtype, np.bool_): + attrs = attrs.update(data_units=None) new = patch.new(data=new_data, coords=coords, attrs=attrs) return new
376-379: Critical: invalid isinstance withstr | bytes.
isinstance(..., str | bytes)is invalid; use a tuple.- if isinstance(axis, Iterable) and not isinstance(axis, str | bytes): + if isinstance(axis, Iterable) and not isinstance(axis, (str, bytes)): axes = tuple(int(a) for a in axis) else: axes = (int(axis),)
🧹 Nitpick comments (5)
dascore/utils/array.py (5)
447-456: Raise ParameterError instead of assert when no patches are found.Improves robustness and error messaging.
- assert len(patches), "No patches found in apply_array_func" + if not patches: + raise ParameterError("No patches found in apply_array_func")
536-542: Silence Ruff ARG001: rename unusedselfto_.Signature compatibility is preserved; reduces noise.
-def patch_array_ufunc(self, ufunc, method, *inputs, **kwargs): +def patch_array_ufunc(_, ufunc, method, *inputs, **kwargs):
544-561: Silence Ruff ARG001: rename unusedselfto_.Same rationale as above.
-def patch_array_function(self, func, types, args, kwargs): +def patch_array_function(_, func, types, args, kwargs):
152-167: Avoid shadowing built-inreversed.Minor readability nit; rename local to
is_reversed.-def _apply_op(array1, array2, operator, reversed=False): +def _apply_op(array1, array2, operator, is_reversed=False): """Simply apply the operator, account for reversal.""" - if reversed: + if is_reversed: array1, array2 = array2, array1 return operator(array1, array2, *args, **kwargs) @@ -def _apply_op_units(patch, other, operator, attrs, reversed=False): +def _apply_op_units(patch, other, operator, attrs, is_reversed=False): @@ - try: - new_data_w_units = _apply_op(data, other, operator, reversed=reversed) + try: + new_data_w_units = _apply_op(data, other, operator, is_reversed=is_reversed) @@ - reversed = False + is_reversed = False if other_is_patch and not patch_is_patch: patch, other = other, patch - reversed = True + is_reversed = True @@ - new_data, attrs = _apply_op_units(patch, other, operator, attrs, reversed) + new_data, attrs = _apply_op_units(patch, other, operator, attrs, is_reversed) else: - new_data = _apply_op(patch.data, other, operator, reversed) + new_data = _apply_op(patch.data, other, operator, is_reversed)Also applies to: 187-191
427-444: Prefer explicit error over assert for unknown ufunc method.Edge-case, but better UX if a non-reduce/accumulate method sneaks in.
- name = getattr(func, "__name__", None) - assert name in ufunc_method_name - sig = inspect.signature(ufunc_method_name[name]) + name = getattr(func, "__name__", None) + if name not in ufunc_method_name: + raise ParameterError(f"Unsupported ufunc method '{name}'.") + sig = inspect.signature(ufunc_method_name[name])
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
dascore/utils/array.py(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
dascore/utils/array.py (7)
dascore/compat.py (2)
array(31-37)is_array(40-43)dascore/units.py (1)
get_quantity(70-94)dascore/utils/misc.py (1)
iterate(256-268)dascore/utils/patch.py (5)
_merge_aligned_coords(879-897)_merge_models(900-924)align_patch_coords(731-781)get_dim_axis_value(558-642)swap_kwargs_dim_to_axis(1183-1234)dascore/utils/time.py (5)
dtype_time_like(415-425)is_datetime64(405-407)to_datetime64(22-55)to_timedelta64(141-173)to_float(327-333)dascore/core/patch.py (8)
dtype(236-238)data(221-223)coords(216-218)attrs(211-213)shape(226-228)ndim(201-203)Patch(28-422)dims(196-198)dascore/proc/basic.py (2)
update_attrs(79-105)update(167-207)
🪛 Ruff (0.12.2)
dascore/utils/array.py
536-536: Unused function argument: self
(ARG001)
544-544: Unused function argument: self
(ARG001)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (18)
- GitHub Check: test_code (windows-latest, 3.12)
- GitHub Check: test_code (windows-latest, 3.13)
- GitHub Check: test_code (windows-latest, 3.10)
- GitHub Check: test_code (windows-latest, 3.11)
- GitHub Check: test_code (ubuntu-latest, 3.13)
- GitHub Check: test_code (macos-latest, 3.10)
- GitHub Check: test_code (ubuntu-latest, 3.12)
- GitHub Check: test_code (macos-latest, 3.12)
- GitHub Check: test_code (ubuntu-latest, 3.10)
- GitHub Check: test_code (macos-latest, 3.11)
- GitHub Check: test_code (ubuntu-latest, 3.11)
- GitHub Check: test_code_min_deps (windows-latest, 3.12)
- GitHub Check: test_code_min_deps (ubuntu-latest, 3.12)
- GitHub Check: test_code_min_deps (macos-latest, 3.13)
- GitHub Check: test_code_min_deps (windows-latest, 3.13)
- GitHub Check: test_code_min_deps (macos-latest, 3.12)
- GitHub Check: test_build_docs
- GitHub Check: test_code_min_deps (ubuntu-latest, 3.13)
Description
This PR adds better support for patch boolean operations (eg
patch > 2now works as expected) and refactors how patches plug into numpy's array_func and array_ufunc interface.Checklist
I have (if applicable):
Summary by CodeRabbit
New Features
Refactor
Tests
Chores