Skip to content

Conversation

@d-chambers
Copy link
Contributor

@d-chambers d-chambers commented Sep 15, 2025

Description

This PR adds better support for patch boolean operations (eg patch > 2 now works as expected) and refactors how patches plug into numpy's array_func and array_ufunc interface.

Checklist

I have (if applicable):

  • referenced the GitHub issue this PR closes.
  • documented the new feature with docstrings or appropriate doc page.
  • included a test. See testing guidelines.
  • your name has been added to the contributors page (docs/contributors.md).
  • added the "ready_for_review" tag once the PR is ready to be reviewed.

Summary by CodeRabbit

  • New Features

    • NumPy ufuncs/functions work on Patch objects (arithmetic, comparisons, reductions) with broadcasting, coord alignment, unit-aware results, nan-safe aggregators, boolean reductions (any/all), .T, dtype, size, and defined boolean conversion semantics; dim→axis kwargs helper.
  • Refactor

    • Unified ufunc/aggregation pipeline and shared operator handling for consistent NumPy compatibility and ergonomics.
  • Tests

    • Expanded test coverage for NumPy interop, ufuncs, reductions, comparisons, units, broadcasting, and dim→axis behavior.
  • Chores

    • Documentation environment updated to include tk for plotting.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 15, 2025

Walkthrough

Centralizes aggregators in dascore/constants.py, adds dascore/utils/array.py for patch-aware NumPy ufunc/array-function support, refactors aggregation to use a shared _apply_aggregator, moves binary/operator logic to the new utils, extends Patch with NumPy-compatible ops/properties, and adds a dim→axis helper.

Changes

Cohort / File(s) Summary of changes
Aggregation constants
dascore/constants.py
Add _AGG_FUNCS: Mapping[str, Callable] as an immutable MappingProxyType mapping aggregation names to nan-safe NumPy functions and "first"/"last" via partial(np.take, indices=...).
Patch core & NumPy integration
dascore/core/patch.py
Operators now delegate to dascore.utils.array.apply_ufunc; add comparison ops (__gt__,__ge__,__lt__,__le__), __bool__, numpy protocol hooks (__array_ufunc__=patch_array_ufunc, __array_function__=patch_array_function), __array_priority__, reverse op aliases, dtype and T properties, size signature change, aggregation shortcuts (any/all), and PatchUFunc-backed ufunc wrappers.
Aggregation refactor
dascore/proc/aggregate.py
Replace inline per-dimension aggregation logic with a single _apply_aggregator path; import _AGG_FUNCS from constants; add boolean reductions any(...) and all(...); update docs and imports.
Basic proc refactor & bool helper
dascore/proc/basic.py
Add bool_patch(self) -> bool; remove local apply_ufunc implementation and import _apply_binary_ufunc from dascore.utils.array; set apply_operator = _apply_binary_ufunc; remove now-unused imports.
Array utilities (new)
dascore/utils/array.py
New module implementing patch-aware ufunc/array-function handling: apply_ufunc, apply_array_func, patch_array_ufunc, patch_array_function, PatchUFunc (with bound variants, reduce/accumulate), UFUNC_MAP, broadcasting/alignment, unit handling, reduction/accumulator helpers, and reassembly utilities.
Patch utilities
dascore/utils/patch.py
Add swap_kwargs_dim_to_axis(patch, kwargs) to convert dim kwargs into axis indices; minor docstring and error-message formatting fixes.
Documentation
docs/tutorial/patch.qmd
Document numpy-like functions on Patch, PatchUFunc usage, broadcasting examples, NumPy-protocol support, and dim→axis conversion notes.
Tests — core & utils
tests/test_core/test_patch.py, tests/test_utils/test_array_utils.py
Update/add tests for new Patch features (.T, comparisons, __bool__) and extensive tests for apply_ufunc/PatchUFunc/apply_array_func covering unary/binary ufuncs, broadcasting, units, reductions, and NumPy interoperability.
Tests — proc & basic & patch utils
tests/test_proc/test_aggregate.py, tests/test_proc/test_basic.py, tests/test_utils/test_patch_utils.py
Adjust aggregate assertions to compare arrays (agg.data); remove legacy unit-related tests/imports and TestApplyUfunc from test_basic.py; add tests for swap_kwargs_dim_to_axis.
Environments
.github/doc_environment.yml, .github/min_deps_environment.yml
Add tk dependency for plotting in both environment files.

Possibly related PRs

  • typo fixes #530: modifies operator implementations in dascore/core/patch.py (related overlap in operator/ufunc handling).

Suggested labels

ready_for_review

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title Check ✅ Passed The title "Patch boolean logic and better numpy array protocol support" succinctly and accurately summarizes the PR's primary changes: improved Patch boolean/comparison behavior and refactoring to integrate NumPy's array/ufunc protocols. It is specific, concise, and directly related to the changes shown in the provided summaries (boolean operators, PatchUFunc/array protocol hooks, and related utils refactors).
Description Check ✅ Passed The PR description includes a clear "Description" that states the two main goals (better boolean ops for Patch and refactoring NumPy array_func/array_ufunc integration) and provides the checklist template, so it gives a valid high-level summary for reviewers. It omits some reviewer-useful details such as links to related issues/discussions, a short list of the most affected modules or notable API changes, and any backward-compatibility or migration notes.
Docstring Coverage ✅ Passed Docstring coverage is 93.75% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch gt_lt_patch

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@d-chambers d-chambers added the patch related to Patch class label Sep 15, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a 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.

-        pass
dascore/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

📥 Commits

Reviewing files that changed from the base of the PR and between 13b2bf2 and 655fe00.

📒 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?

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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 method test_l1_norm.

This method duplicates the logic of test_l1 on 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 unused noqa directive.

The noqa: N802 directive 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_units could 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: Unused self parameter in protocol methods.

Both patch_array_ufunc and patch_array_function have an unused self parameter. 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_patch function 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_ufuncs but should be test_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 None or "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

📥 Commits

Reviewing files that changed from the base of the PR and between 655fe00 and f41d2de.

📒 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_FUNCS registry is well-structured, using MappingProxyType to ensure immutability. The choice of nan-safe numpy functions (nanmean, nanmedian, etc.) is appropriate for handling DAS data that may contain missing values. Using partial for first/last operations is a clean functional approach.

tests/test_proc/test_basic.py (2)

200-200: Fix typo: TestStandardize is spelled correctly.

The class was renamed from the misspelled TestStandarize to TestStandardize, which is the correct spelling.


644-645: Updated signature for apply_operator reflects 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 in dascore.utils.array.

tests/test_core/test_patch.py (4)

427-430: Good test coverage for the new .T property.

The test properly verifies that the .T property works as an alias for .transpose(), matching NumPy conventions.


620-624: Excellent expansion of comparison operator coverage.

The addition of ge, le, gt, lt operators 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 new bool_patch implementation.


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_FUNCS registry from dascore.constants and the _apply_aggregator helper from dascore.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 uses np.any through the aggregation framework.


262-278: Complete boolean aggregation pair with all() method.

The all() method properly complements any(), using np.all for complete boolean aggregation support.

dascore/proc/basic.py (2)

156-165: Well-implemented boolean conversion following NumPy conventions.

The bool_patch function correctly implements NumPy's boolean conversion semantics by delegating to bool(self.data), which will raise ValueError for multi-element arrays and return the truthiness of single elements.


372-375: Smooth migration path for apply_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_ufunc with 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 like patch > 2.


143-144: LGTM! Proper boolean conversion following NumPy convention.

The __bool__ method correctly delegates to dascore.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 size and dtype properties enhance NumPy compatibility by providing expected attributes.


366-367: LGTM! Valuable boolean aggregation methods.

The addition of any and all aggregation 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_ufunc function 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_param provides 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
Copy link

codecov bot commented Sep 16, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.88%. Comparing base (13b2bf2) to head (05c09a4).
⚠️ Report is 3 commits behind head on master.

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            
Flag Coverage Δ
unittests 99.88% <100.00%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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

📥 Commits

Reviewing files that changed from the base of the PR and between f41d2de and ccd8d7e.

📒 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_patch to 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_axes function properly handles all axis specifications (None, int, sequence) and deduplicates while preserving order. The signature binding is correctly done with bind_partial to 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: Unused self parameter - expected for protocol.

The self parameter is intentionally unused as this function implements the __array_ufunc__ protocol. The static analysis warning can be safely ignored.


416-433: Unused self parameter - expected for protocol.

The self parameter is intentionally unused as this function implements the __array_function__ protocol. The static analysis warning can be safely ignored.

@d-chambers d-chambers added the documentation Improvements or additions to documentation label Sep 16, 2025
@coderabbitai coderabbitai bot added the proc Related to processing module label Sep 16, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a 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 NotImplemented
dascore/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

📥 Commits

Reviewing files that changed from the base of the PR and between ccd8d7e and 33789f6.

📒 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.

@github-actions
Copy link

github-actions bot commented Sep 16, 2025

✅ Documentation built:
👉 Download
Note: You must be logged in to github and a DASDAE member to access the link.

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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"] = axis

Also, in the example comment, rename new_kwarg to new_kwargs for 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 text

The 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 formatting

Use 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 coverage

Exercises 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_ufunc

Rename the parameter to _self to 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_function

Same 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 data parameter in the shape_changing_func is 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

📥 Commits

Reviewing files that changed from the base of the PR and between 5d4ca9a and b4205dd.

📒 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 good

Clarity/readability improved; no functional changes.

tests/test_utils/test_patch_utils.py (1)

651-711: Good coverage of swap_kwargs_dim_to_axis

Covers 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 good

Switching to public ParameterError import from dascore.exceptions aligns with exported API.


427-430: Nice .T alias test

Validates transpose alias behavior.


645-645: Updated apply_operator call-site matches new signature

The (op, patch, other) order is consistent with the refactor.


647-652: Boolean comparison assertions are solid

Covers 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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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) throws TypeError. 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 unused self warnings 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_units for 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

📥 Commits

Reviewing files that changed from the base of the PR and between b4205dd and 10d6415.

📒 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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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 invalid isinstance(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.isnan should 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 None and same for unary isfinite?

🧹 Nitpick comments (1)
dascore/utils/array.py (1)

528-533: Silence Ruff ARG001 by prefixing unused self parameters.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 10d6415 and 14a5b50.

📒 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_out is 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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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 using str | 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: unused self (keeps protocol intact).

Rename to _self to 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: unused self (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

📥 Commits

Reviewing files that changed from the base of the PR and between 14a5b50 and 7d78e4b.

📒 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)

Comment on lines 52 to 57
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Suggested change
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.

Comment on lines +184 to +186
# 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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Suggested change
# 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.

@coderabbitai coderabbitai bot added the ready_for_review PR is ready for review label Sep 17, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a 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_attrs may 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 with str | 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 unused self to _.

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 unused self to _.

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-in reversed.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 1f2bb82 and 05c09a4.

📒 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)

@d-chambers d-chambers merged commit fb657eb into master Sep 17, 2025
22 checks passed
@d-chambers d-chambers deleted the gt_lt_patch branch September 17, 2025 18:35
This was referenced Sep 27, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation patch related to Patch class proc Related to processing module ready_for_review PR is ready for review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants