Skip to content

Enable ty's invalid-argument-type rule - #836

Merged
d-chambers merged 11 commits into
devfrom
ty-arg-type
Aug 8, 2026
Merged

Enable ty's invalid-argument-type rule#836
d-chambers merged 11 commits into
devfrom
ty-arg-type

Conversation

@d-chambers

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

Copy link
Copy Markdown
Contributor

Description

Takes ty's invalid-argument-type from 18 to 0 and removes it from [tool.ty.rules], so the rule now gates CI. Follows #830, which took it 34 → 18. invalid-return-type stays ignored and is the next PR — it fell from 31 to 26 as a side effect of the fixes here.

No suppression comments were needed; one existing suppression became unnecessary and was removed.

Bugs found along the way

These are real defects, not annotation noise:

  • WARNING_ACTIONS advertised "all", which Python only began accepting in 3.14. On 3.11/3.12 it trips an assertion and on 3.13 it raises ValueError — three of the four supported interpreters reject it. Nothing passed it, and it aliases "always".
  • Path(candidate) in the directory walker was built from a yield type including UPath, which is not os.PathLike unless it resolved to a local path. coerce_to_local_path handles it and also resolves file:// URIs.
  • to_float's fallback called float() on anything unregistered, which pd.Timestamp does not support — reachable only because registration shadows it. Now pinned by a test.
  • _coord_record_from_row narrowed only the min, leaving float(None) on the max reachable as far as a checker could tell. Every producer writes min and max together, so that is asserted rather than given invented behaviour.
  • Several annotations contradicted their own bodies: CoordManager.__rich__ declared -> str but returns Text; to_int/to_float declared -> np.ndarray while returning int, float, or Series; the filesystem generators declared a SendType of str although the indexer primes them with send(None).

Notable changes

  • to_int/to_float become overloaded wrappers over private _to_int/_to_float dispatchers, so a Series in is typed as a Series out and an array as an array. Stacking @overload on a singledispatch loses .register, hence the split. Measured at ~15 ns per scalar call and ~30 ns per array call. to_int.register / to_float.register no longer exist — register on the private dispatchers.
  • convert_units drops its constrained numeric TypeVar, which rejected None, quantities, and numpy scalars that it accepts and returns. That also makes its # ty: ignore[unsupported-operator] unnecessary.
  • hash_array goes through ndarray.data instead of memoryview(arr). Same zero-copy object; digests verified byte-identical for C-contiguous, Fortran-ordered, strided datetime64, timedelta64, and empty arrays.
  • Two ad-hoc expressions replaced by existing dascore helpers: invert_quantity and coerce_to_local_path.

Verification

Checked on every supported Python version, since ty resolves stdlib stubs per version and 3.14 alone caught the WARNING_ACTIONS bug:

Python enabled-rule diagnostics
3.11 0
3.12 0
3.13 0
3.14 0

Full suite 8120 passed / 251 skipped / 2 xfailed; pre-commit run --all-files green.

Changelog

  • changed breaking: to_int and to_float are overloaded wrappers over private singledispatch implementations, so to_int.register(...) no longer exists — register on _to_int/_to_float instead. Runtime behaviour is unchanged.

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

  • New Features

    • Added broader support for payload scanning, UPath-based I/O, segmented coordinates, PRODML writing, and enhanced metadata.
    • Expanded time conversion for scalars, sequences, quantities, and pandas Series.
    • Added runtime configuration context support and richer scan payload details.
  • Bug Fixes

    • Improved unit conversion, filtering, coordinate transformations, array hashing, and index edge-case handling.
    • Corrected warning-action compatibility for Python 3.11–3.13.
  • Documentation

    • Updated the changelog with API changes, behavior updates, removed features, and the Python 3.11 minimum requirement.

CoordManager.__rich__ declared -> str but returns Text.assemble(...).

The filesystem generators declared a SendType of str, but None is an
explicitly supported send value: the indexer primes them with send(None)
and both sub-generators test `if signal is not None`. All three change
together because yield from delegates the send value.
_get_transformed_coord hand-rolled `1 / coord.units`; invert_quantity
already does exactly this, including the None/NaN guard, and is used for
the same expression in transform/fourier.py and proc/filter.py.

The directory walker built Path(candidate) from a yield type that
includes UPath. UPath is not os.PathLike unless it resolved to a local
path, so that would raise TypeError for a remote resource;
coerce_to_local_path handles it and also resolves file:// URIs.
A constrained TypeVar was the wrong tool: it demands an exact match, so
callers passing None (returned unchanged, and relied upon) or a value the
checker only knows as object were rejected outright. The -> numeric
promise was false anyway, since an int in yields a float out.

Widening it also makes the unsupported-operator suppression unnecessary,
which has to go with it or unused-ignore-comment fires.

_maybe_transform_units rebound filt inside the try, so the except branch
was typed as the union of both states.
ndarray declares __buffer__ only for Python 3.12+, so a checker resolving
this project's 3.11 floor cannot see it. Going through .data, which numpy
types as a memoryview, says the same thing at runtime -- it is the same
zero-copy object, and digests are byte-identical for C-contiguous,
Fortran-ordered, strided datetime64, timedelta64, and empty arrays.

FrozenDict._dict inferred as dict[str, Unknown] because **kwargs makes
the checker pick typeshed's str-keyed dict overload, so lookups by K
failed. Declaring it and casting in the constructor says what the class
holds. new() merges in a dict literal now, since update's overloads all
require str keys.
_UNSET was a bare object(), so target_units collapsed to object and the
checker could not see the str | None it actually holds. An enum member
narrows on an is comparison and keeps the identity semantics the three
comparison sites rely on. The distinction is load-bearing -- None means
the coordinate is unitless, which cannot answer a query carrying units --
so it now has a test of its own.

_coord_record_from_row narrowed only the min, leaving float(None) on the
max reachable as far as the checker could tell. Every producer writes min
and max together, so this asserts that rather than inventing behaviour
for a state no caller can construct.
PatchSummary is not a Mapping at runtime, but it is not final either, so
a checker has to assume a subclass could be both and leaves the
intersection in the Mapping branch. Testing for it first narrows it out.
No input can change branch, since the intersection is empty.

dict() on a TypedDict erases its value types, so attrs came out as
object. _validate_scan_payload has already raised unless every key holds
what ScanPayload declares, so the cast states what is known there.
Both bases claimed to return np.ndarray while actually returning an int,
a float, or a Series depending on what was passed, so every registered
implementation with an honest return annotation was rejected.

Overloads say it properly -- a Series stays a Series, other sequences
become arrays, everything else is a scalar -- but stacking @overload on a
singledispatch loses .register, so the dispatchers move to _to_int and
_to_float and the public names become wrappers. Measured at ~15 ns per
scalar call and ~30 ns per array call.

to_int's scalar overload is int | np.integer | float, not int: time-like
values convert through numpy and yield np.int64, and null yields NaN.

The to_float fallback also called float() on anything unregistered, which
pd.Timestamp does not support; it is reachable only for types float()
already handles, and there is now a test pinning that.
WARNING_ACTIONS advertised "all", which Python only began accepting in
3.14; on 3.11 and 3.12 it trips an assertion and on 3.13 it raises
ValueError, so three of the four supported interpreters reject it.
Nothing passes it, and it aliases "always".

With that the rule reaches zero on every supported Python version, so it
no longer needs to be ignored.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@d-chambers d-chambers added the ready_for_review PR is ready for review label Aug 7, 2026
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@d-chambers, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 56 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 15bf64dd-a206-4b41-a1d0-4e4959f64d66

📥 Commits

Reviewing files that changed from the base of the PR and between 4f72f5b and f6c1520.

📒 Files selected for processing (1)
  • dascore/units.py
📝 Walkthrough

Walkthrough

The PR updates typing contracts and runtime compatibility across time conversion, scanning, indexing, units, utilities, visualization, documentation, and type-checker configuration. It adds edge-case tests for unit handling and numeric conversion.

Changes

Typing and runtime compatibility

Layer / File(s) Summary
Time conversion dispatch and overloads
dascore/utils/time.py, tests/test_utils/test_time.py, docs/changelog.qmd
to_int and to_float now use private dispatchers with overloaded public wrappers. Tests cover Decimal, Timestamp, Series, sequences, and fallback conversion.
Scan and index contracts
dascore/io/core.py, dascore/io/index/..., dascore/utils/misc.py, tests/test_io/test_index/test_index_edge_cases.py
Scan payload typing, result narrowing, path coercion, coordinate envelope validation, generator send annotations, and omitted-unit handling are updated.
Unit conversion and coordinate behavior
dascore/units.py, dascore/proc/filter.py, dascore/viz/spectrogram.py
convert_units accepts broader inputs. Ragged filter inputs retain fallback processing. Spectrogram coordinates use invert_quantity.
Utility contracts and type-checker cleanup
dascore/constants.py, dascore/core/coordmanager.py, dascore/utils/array.py, dascore/utils/mapping.py, pyproject.toml
Utility annotations and hashing access are refined. The unsupported "all" warning action and an invalid-argument-type suppression are removed.

Possibly related PRs

  • DASDAE/dascore#629: Both changes modify array hashing in dascore/utils/array.py.
  • DASDAE/dascore#748: Both changes refine datetime and timedelta conversion in dascore/utils/time.py.
  • DASDAE/dascore#759: Both changes modify filesystem traversal in DBDirectoryIndexer._walk.

Suggested labels: documentation, CI, bug

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 72.92% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main change: enabling ty's invalid-argument-type rule.
Description check ✅ Passed The description explains the changes, lists verification results, and completes the applicable checklist items.
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 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ty-arg-type

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 documentation Improvements or additions to documentation labels Aug 7, 2026

@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

🤖 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/io/index/planned.py`:
- Around line 104-108: Replace the assert on hi in the relevant
envelope-processing logic with explicit null validation that rejects both None
and pandas NaN/NaT values before numeric or datetime conversion, raising the
appropriate runtime exception. Preserve the existing handling for valid maxima
and the special string-envelope case.

In `@dascore/units.py`:
- Around line 195-209: Update convert_units to return data unchanged immediately
when data is None, before applying any conversion factors or using from_units.
Preserve the existing conversion behavior for non-None inputs.

In `@docs/changelog.qmd`:
- Line 7: Update the changelog entry describing to_float so it explicitly
documents the new fallback behavior: unsupported values such as Decimal("3") and
"1.5" are converted through float(). Replace the statement that runtime behavior
is unchanged with wording that distinguishes this public behavior change from
the unchanged existing conversions.
🪄 Autofix

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: 73b528d7-8da4-4b4b-8763-7be9c814abc0

📥 Commits

Reviewing files that changed from the base of the PR and between 23b575a and 49083fa.

📒 Files selected for processing (17)
  • dascore/constants.py
  • dascore/core/coordmanager.py
  • dascore/io/core.py
  • dascore/io/index/indexer.py
  • dascore/io/index/planned.py
  • dascore/io/index/query.py
  • dascore/proc/filter.py
  • dascore/units.py
  • dascore/utils/array.py
  • dascore/utils/mapping.py
  • dascore/utils/misc.py
  • dascore/utils/time.py
  • dascore/viz/spectrogram.py
  • docs/changelog.qmd
  • pyproject.toml
  • tests/test_io/test_index/test_index_edge_cases.py
  • tests/test_utils/test_time.py

Comment on lines +104 to +108
# Only the str envelope above represents a missing max. Every producer
# writes {name}_min and {name}_max together -- _output_records feeds
# whole dataframe rows (a missing value is NaN, not None) and the aux
# info dict always sets both -- so a max cannot be absent past here.
assert hi is not None

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Reject null maxima before conversion.

Line 108 checks only hi is not None. Pandas row dictionaries represent missing values as NaN, and NaN is not None, so the assertion passes. The numeric branch can then store nan, while the datetime branch can propagate NaT into the coordinate record.

Use an explicit null check and exception. Do not use assert for this runtime validation.

Proposed fix
-    assert hi is not None
+    if hi is None or pd.isnull(hi):
+        raise ValueError(
+            f"Non-string coordinate {name!r} requires a non-null maximum."
+        )
📝 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
# Only the str envelope above represents a missing max. Every producer
# writes {name}_min and {name}_max together -- _output_records feeds
# whole dataframe rows (a missing value is NaN, not None) and the aux
# info dict always sets both -- so a max cannot be absent past here.
assert hi is not None
# Only the str envelope above represents a missing max. Every producer
# writes {name}_min and {name}_max together -- _output_records feeds
# whole dataframe rows (a missing value is NaN, not None) and the aux
# info dict always sets both -- so a max cannot be absent past here.
if hi is None or pd.isnull(hi):
raise ValueError(
f"Non-string coordinate {name!r} requires a non-null maximum."
)
🤖 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/io/index/planned.py` around lines 104 - 108, Replace the assert on hi
in the relevant envelope-processing logic with explicit null validation that
rejects both None and pandas NaN/NaT values before numeric or datetime
conversion, raising the appropriate runtime exception. Preserve the existing
handling for valid maxima and the special string-envelope case.

Comment thread dascore/units.py Outdated
Comment thread docs/changelog.qmd

## Unreleased API Changes

- `dascore.utils.time.to_int` and `to_float` are now overloaded wrappers over private `singledispatch` implementations, so a `Series` input is typed as returning a `Series` and an array as returning an array. Their runtime behaviour is unchanged, but `to_int.register(...)` and `to_float.register(...)` no longer exist; register new implementations on `_to_int` / `_to_float` instead. `convert_units` no longer declares a constrained `numeric` type variable — it accepted (and still accepts) `None`, quantities, and numpy scalars, none of which that variable admitted. `WARNING_ACTIONS` no longer lists `"all"`, which Python only began accepting in 3.14 and which raises on the 3.11–3.13 interpreters DASCore also supports; use `"always"`, which it aliases.

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Document the new to_float fallback behavior.

Line 7 says that runtime behavior is unchanged. The new fallback accepts values such as Decimal("3") and "1.5" through float(). State this public behavior change in the changelog.

🤖 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 `@docs/changelog.qmd` at line 7, Update the changelog entry describing to_float
so it explicitly documents the new fallback behavior: unsupported values such as
Decimal("3") and "1.5" are converted through float(). Replace the statement that
runtime behavior is unchanged with wording that distinguishes this public
behavior change from the unchanged existing conversions.

@codecov

codecov Bot commented Aug 7, 2026

Copy link
Copy Markdown

Codecov Report

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

Additional details and impacted files
@@            Coverage Diff            @@
##               dev      #836   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files          164       164           
  Lines        17936     17945    +9     
=========================================
+ Hits         17936     17945    +9     
Flag Coverage Δ
network 48.51% <84.37%> (+0.03%) ⬆️
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.

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown

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

# Conflicts:
#	dascore/utils/time.py
#	docs/changelog.qmd
An @overload set is only verified against its implementation when the
implementation's own return is annotated; without it the return is
untyped and satisfies anything, so a wrong overload passes silently and
is then believed at every call site.

With the return declared, ty reports invalid-overload for an overload
whose return is not assignable to it -- confirmed by declaring
to_float(pd.Series) -> str and watching it fail.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
dascore/utils/time.py (1)

367-368: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Replace the catch-all Any overloads.

These overloads make every argument valid for to_int/to_float, while the runtime implementation still rejects unsupported inputs raises NotImplementedError. Use explicit supported-input overloads for these functions at lines 388-397 and 495-508.

🤖 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/utils/time.py` around lines 367 - 368, Replace the catch-all Any
overloads for to_int and to_float with overloads that enumerate only their
supported input types, matching the runtime implementations’ accepted values.
Keep unsupported arguments excluded from static typing so they remain rejected
by the existing NotImplementedError path.
🤖 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.

Outside diff comments:
In `@dascore/utils/time.py`:
- Around line 367-368: Replace the catch-all Any overloads for to_int and
to_float with overloads that enumerate only their supported input types,
matching the runtime implementations’ accepted values. Keep unsupported
arguments excluded from static typing so they remain rejected by the existing
NotImplementedError path.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c72455a2-e516-4cdc-bd8f-2cd7bd4f64e1

📥 Commits

Reviewing files that changed from the base of the PR and between 316ff62 and 4f72f5b.

📒 Files selected for processing (1)
  • dascore/utils/time.py

None only survives the from_units is None path, where the data is
returned untouched; with real conversion factors it raises.
@d-chambers
d-chambers merged commit 04c42de into dev Aug 8, 2026
27 checks passed
@d-chambers
d-chambers deleted the ty-arg-type branch August 8, 2026 10:20
@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

bug Something isn't working CI continuous integration documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant