Skip to content

Enable ty's not-subscriptable and no-matching-overload rules - #800

Merged
d-chambers merged 5 commits into
devfrom
ty-burn-3
Aug 4, 2026
Merged

Enable ty's not-subscriptable and no-matching-overload rules#800
d-chambers merged 5 commits into
devfrom
ty-burn-3

Conversation

@d-chambers

@d-chambers d-chambers commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Description

Third step of the ty burn-down (#795, #796, #797). not-subscriptable and no-matching-overload both go to zero and come off the ignore list, leaving three ignored rules instead of five.

The one API question

BaseCoord.shape was typed tuple[int, ...] | None but every coord has a shape: each subclass derives it in a before-validator, and the field validator already normalized a missing value to (). The None was a placeholder that no constructed coord ever carried, which is why the four self.shape[0] sites read as subscripting None.

Making it required instead traded those four errors for 21 missing-argument reports, since the before-validators that supply it are invisible to type checkers, so the annotation keeps a default. CoordPartial does declare it required: it is the one coord which cannot re-derive its shape on the way back from model_dump(exclude_defaults=True) — the array coords rebuild it from values, CoordRange from start/stop/step, CoordSegmented from its segments — so with () as the base default a dimensionless partial coord would lose its only defining field on the round trip through CoordManager. That has a regression test.

Root-cause fixes

  • _get_dx_or_spacing_and_axes is overloaded on require_evenly_spaced. With it set, get_coord requires an evenly sampled coord, so every returned value is a scalar spacing rather than an array of values; the two callers that pass it no longer see the union.
  • NUMPY_TIME_UNIT_MAPPING declares its literal unit codes, which is what numpy's unit parameter accepts — not str.
  • The degenerate-array branches in _array_to_datetime64 and _array_to_timedelta64 unpack with [()] instead of rebuilding the scalar through np.datetime64/np.timedelta64, which drops the duplicated astype in each. _array_to_timedelta64 was also annotated -> np.datetime64; it returns timedeltas, and fixing that removed three invalid-return-type reports as well.
  • maybe_mem_map checks for a name before mapping instead of handing np.memmap a None it is guaranteed to reject. The fallback now also covers a name that exists but cannot be mapped (an empty file, an fd number), which gets a test.
  • Spool.split branches on count rather than relying on np.ceil to absorb a None. _spool_map was passing it a float size, so it rounds up itself and split's int annotation is now true.
  • patch_function calls pydantic.validate_call as the decorator factory it is rather than passing func and config together.
  • _get_band_attr_data_type compares against the first mapped band once instead of scanning for None separately.
  • The spool query coerces with pd.Timestamp.to_datetime64.

Carried over from #797

_required_resource_type read the _required_type marker through an unconditional cast, which had no runtime behavior and turned an unwrapped method into an AttributeError rather than None, while _get_fiber_io_and_req_type did the same lookup a second way with getattr. One concept, one idiom, on its own commit.

Tests

The split-by-count path had no test — the one named for it split by size. That is now two tests, and the empty-shape round trip and unmappable-file fallback are covered.

The counts comment in pyproject.toml is refreshed: invalid-argument-type 166, invalid-return-type 67, invalid-method-override 36. Bringing tests/ into scope stays for a later step.

Changelog

none

Checklist

I have (if applicable):

  • referenced the GitHub issue this PR closes.
  • documented the new feature with docstrings and/or appropriate doc page.
  • included tests. See testing guidelines.
  • added the "ready_for_review" tag once the PR is ready to be reviewed.

Summary by CodeRabbit

  • Bug Fixes

    • Improved datetime and timedelta conversions, including reliable scalar handling.
    • Fixed file loading for unnamed, empty, or non-memory-mappable files.
    • Improved spool splitting for uneven sizes and requested part counts.
    • Strengthened coordinate shape handling and serialization reliability.
    • Improved resource detection during scan operations.
  • Refactor

    • Enhanced type checking and clarified public typing behavior.
    • Improved handling of supported time units and band data types.
  • Tests

    • Added regression coverage for coordinate serialization, spool splitting, and file loading.

_required_resource_type read the marker through an unconditional cast,
which had no runtime behavior and turned an unwrapped method into an
AttributeError rather than None. Meanwhile _get_fiber_io_and_req_type
did the same lookup a second way with getattr. One concept, one idiom.
Both rules go to zero and come off the ignore list.

BaseCoord.shape was typed `tuple[int, ...] | None` but every coord has
one: each subclass derives it in a before-validator, and the field
validator already normalized a missing value to (). The None was only a
placeholder, so the four `self.shape[0]` sites read as subscripting
None. Making shape required instead traded those four errors for 21
missing-argument reports, since the before-validators are invisible to
type checkers, so the annotation keeps a default.

The remaining fixes:

- _get_dx_or_spacing_and_axes is overloaded on require_evenly_spaced.
  With it set, get_coord requires an evenly sampled coord, so every
  returned value is a scalar spacing rather than an array of values;
  callers no longer see the union.
- NUMPY_TIME_UNIT_MAPPING declares its literal unit codes, which is what
  numpy's unit parameter accepts.
- The degenerate-array branches in _array_to_datetime64 and
  _array_to_timedelta64 unpack with `[()]` instead of rebuilding the
  scalar through np.datetime64/np.timedelta64, which drops the duplicated
  astype in each. _array_to_timedelta64 was also annotated
  `-> np.datetime64`; it returns timedeltas.
- maybe_mem_map checks for a name before mapping instead of handing
  np.memmap a None it is guaranteed to reject. The fallback now also
  covers a name that exists but cannot be mapped, which gets a test.
- Spool.split branches on count rather than relying on np.ceil to absorb
  a None. _spool_map was passing it a float size, so it rounds up itself
  and split's `int` annotation is now true.
- patch_function calls pydantic.validate_call as the decorator factory it
  is rather than passing func and config together.
- _get_band_attr_data_type compares against the first mapped band once
  instead of scanning for None separately.
- The spool query coerces with pd.Timestamp.to_datetime64.

The split-by-count path had no test; the one named for it split by size.
CoordPartial redeclares shape without a default. It is the one coord
which cannot re-derive its shape on the way back from a
model_dump(exclude_defaults=True) -- the array coords rebuild it from
values, CoordRange from start/stop/step, CoordSegmented from its
segments -- so with () as the base default a dimensionless partial coord
lost its only defining field on the round trip through CoordManager.

Spool.split keeps ceiling a non-integral size rather than slicing with
whatever it was handed.
@d-chambers d-chambers added the ready_for_review PR is ready for review label Aug 4, 2026
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3829d1d1-8261-4df0-838b-70bff2eb62a0

📥 Commits

Reviewing files that changed from the base of the PR and between 70084ad and a6786ab.

📒 Files selected for processing (1)
  • tests/test_utils/test_misc.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/test_utils/test_misc.py

📝 Walkthrough

Walkthrough

The pull request strengthens type annotations, updates type-checker configuration, and adjusts coordinate, spool, I/O, time, and memory-mapping behavior. It adds regression coverage for dimensionless coordinates, spool splitting, and unsupported memory-mapped files.

Changes

Typing and runtime robustness updates

Layer / File(s) Summary
Type contracts and checker alignment
dascore/constants.py, dascore/utils/patch.py, pyproject.toml
Supported NumPy time units and patch helper return types now have explicit typing. validate_call uses the decorator-factory form. Type-checker ignores and diagnostic counts are updated.
Coordinate and spool behavior
dascore/core/coords.py, dascore/core/spool.py, tests/test_core/test_coords.py, tests/test_core/test_spool.py
Coordinate shapes and summary steps have explicit contracts. Spool splitting uses separate count and size calculations, including ceiling behavior for non-integral sizes. Tests cover dimensionless coordinate reconstruction and patch preservation.
I/O resolution and data conversion
dascore/io/core.py, dascore/io/index/query.py, dascore/io/sintela/protobuf_utils.py
Resource type lookup tolerates missing metadata. Datetime strings use pandas-derived values. BAND mappings return empty units when mappings are unknown or inconsistent.
Array and time utilities
dascore/utils/misc.py, dascore/utils/time.py, tests/test_utils/test_misc.py
Spool map chunk sizes are rounded up. Memory mapping falls back to regular arrays for unnamed, unsupported, or empty files. Datetime and timedelta conversions normalize nanosecond values and unwrap scalar arrays.

Possibly related PRs

Suggested labels: CI, IO, spool

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly states the primary change: enabling ty's not-subscriptable and no-matching-overload rules.
Description check ✅ Passed The description explains the typing changes, root causes, tests, and checklist status; issue references are present.
Docstring Coverage ✅ Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ty-burn-3

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.

@coderabbitai coderabbitai Bot added bug Something isn't working CI continuous integration IO Work for reading/writing different formats patch related to Patch class spool related to Spool class labels Aug 4, 2026
@codecov

codecov Bot commented Aug 4, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (cbb84e4) to head (a6786ab).

Additional details and impacted files
@@            Coverage Diff            @@
##               dev      #800   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files          164       164           
  Lines        17824     17829    +5     
=========================================
+ Hits         17824     17829    +5     
Flag Coverage Δ
network 48.38% <50.00%> (+<0.01%) ⬆️
unittests 100.00% <100.00%> (ø)

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

☔ View full report in Codecov by Harness.
📢 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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
tests/test_core/test_spool.py (1)

606-611: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Exercise the non-integral size path.

The production change rounds non-integral sizes, but Line 608 passes size=2. Use a fractional value such as size=1.5 and keep the expected lengths 2 and 1.

Proposed test change
-        split = list(random_spool.split(size=2))
+        split = list(random_spool.split(size=1.5))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_core/test_spool.py` around lines 606 - 611, Update
test_uneven_size to call random_spool.split with a fractional size such as 1.5,
while preserving the existing assertions that the two resulting spools have
lengths 2 and 1.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@dascore/core/spool.py`:
- Around line 587-591: Validate the split parameters before the step calculation
in the affected spool splitting method: reject any provided count or size that
is less than or equal to zero by raising ParameterError. Ensure this validation
occurs before computing step so count=0, size=0, and negative values cannot
produce invalid or non-advancing iteration.

In `@dascore/utils/misc.py`:
- Around line 1015-1026: Update maybe_mem_map() to include OSError, including
FileNotFoundError, in the exceptions caught around np.memmap() so unreadable or
unmappable named file-like objects use the existing fid.seek(0) and
np.frombuffer fallback. Add a regression test covering a readable named
file-like object whose path cannot be mapped.

In `@dascore/utils/time.py`:
- Around line 245-254: Update _array_to_timedelta64 to use array.size instead of
len(array) when checking for empty inputs, allowing zero-dimensional arrays to
proceed without TypeError. Ensure zero-dimensional np.datetime64 inputs are
converted through the existing datetime path and unpacked to a scalar, matching
the degenerate timedelta result behavior.

---

Nitpick comments:
In `@tests/test_core/test_spool.py`:
- Around line 606-611: Update test_uneven_size to call random_spool.split with a
fractional size such as 1.5, while preserving the existing assertions that the
two resulting spools have lengths 2 and 1.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7e8f7f03-e7d7-43c5-9e96-af0b135f3594

📥 Commits

Reviewing files that changed from the base of the PR and between cbb84e4 and e5d3d98.

📒 Files selected for processing (13)
  • dascore/constants.py
  • dascore/core/coords.py
  • dascore/core/spool.py
  • dascore/io/core.py
  • dascore/io/index/query.py
  • dascore/io/sintela/protobuf_utils.py
  • dascore/utils/misc.py
  • dascore/utils/patch.py
  • dascore/utils/time.py
  • pyproject.toml
  • tests/test_core/test_coords.py
  • tests/test_core/test_spool.py
  • tests/test_utils/test_misc.py

Comment thread dascore/core/spool.py
Comment on lines +587 to +591
if count is not None:
step = int(np.ceil(len(self) / count))
else:
assert size is not None # the check above sets exactly one of them
step = int(np.ceil(size)) # tolerate a non-integral size

@coderabbitai coderabbitai Bot Aug 4, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Reject non-positive split parameters before computing step.

Line 588 divides by count, so count=0 raises ZeroDivisionError. Line 591 can produce step=0 for size=0. Lines 592-594 then never advance. Negative values can also make the generator run forever. Validate that both parameters are greater than zero and raise ParameterError.

Proposed fix
         if not ((count is not None) ^ (size is not None)):
             msg = "Spool.split requires either spool_count or spool_size."
             raise ParameterError(msg)
+        if count is not None and count <= 0:
+            raise ParameterError("count must be greater than zero.")
+        if size is not None and size <= 0:
+            raise ParameterError("size must be greater than zero.")
         start = 0
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dascore/core/spool.py` around lines 587 - 591, Validate the split parameters
before the step calculation in the affected spool splitting method: reject any
provided count or size that is less than or equal to zero by raising
ParameterError. Ensure this validation occurs before computing step so count=0,
size=0, and negative values cannot produce invalid or non-advancing iteration.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Real, but out of scope here and pre-existing: size=0 and negative sizes have produced a non-terminating generator since the method was written, and this PR did not touch that path. The only thing that changed is count=0, which now raises ZeroDivisionError where it previously raised TypeError — both unhelpful. Guarding the parameters means adding new ParameterError paths to a public API, which wants its own PR and tests rather than riding along in a type-checking change. Tracking it separately.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@d-chambers, acknowledged. This PR should remain focused on the type-checking changes.

A follow-up change can define the public behavior for non-positive count and size, then add the required tests. I will leave this finding unresolved for the separately tracked work.

You are interacting with an AI system.

Comment thread dascore/utils/misc.py
Comment thread dascore/utils/time.py
maybe_mem_map only caught the errors a bad name argument produces, so a
name that is a real path numpy still cannot map -- unlinked since the
handle was opened, on a filesystem without mmap -- failed a read the
handle could have served. The comment already claimed the broader
behavior; now the code does too.

Also covers the non-integral split size, which nothing exercised: every
test passed a whole number, so the ceil could have been dropped unnoticed.

Both from review of #800.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/test_utils/test_misc.py`:
- Around line 769-778: Update test_name_no_longer_on_disk to skip the
open-handle unlink scenario when sys.platform == "win32", before attempting
path.unlink(); preserve the existing assertions and behavior on platforms that
support unlinking an open file.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 28044766-36dc-418e-b592-ff3e90543810

📥 Commits

Reviewing files that changed from the base of the PR and between e5d3d98 and 70084ad.

📒 Files selected for processing (3)
  • dascore/utils/misc.py
  • tests/test_core/test_spool.py
  • tests/test_utils/test_misc.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/test_core/test_spool.py
  • dascore/utils/misc.py

Comment thread tests/test_utils/test_misc.py Outdated
Windows will not unlink a file that is still open, so the test raised
PermissionError before reaching what it was checking. A name pointing
nowhere reaches the same fallback on every platform, and matches the
_NamedBytesIO pattern the buffer-size test above it already uses.
@coderabbitai coderabbitai Bot removed bug Something isn't working patch related to Patch class labels Aug 4, 2026
@d-chambers
d-chambers merged commit 071b3dd into dev Aug 4, 2026
29 checks passed
@d-chambers
d-chambers deleted the ty-burn-3 branch August 4, 2026 07:47
@coderabbitai coderabbitai Bot mentioned this pull request Aug 7, 2026
4 tasks
@d-chambers d-chambers removed the ready_for_review PR is ready for review label Aug 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CI continuous integration IO Work for reading/writing different formats spool related to Spool class

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant