Skip to content

Enable ty's unresolved-attribute rule - #797

Merged
d-chambers merged 8 commits into
devfrom
ty-burn-2
Aug 4, 2026
Merged

Enable ty's unresolved-attribute rule#797
d-chambers merged 8 commits into
devfrom
ty-burn-2

Conversation

@d-chambers

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

Copy link
Copy Markdown
Contributor

Description

Second step of the ty burn-down started in #795/#796. unresolved-attribute goes from 138 diagnostics to zero and the rule is enabled everywhere — no per-file holdbacks.

The rule earns its keep: it found real latent bugs, and most of the other fixes are hints that were simply wrong.

Root-cause fixes (each removes a cluster)

  • optional_import is now overloaded: with the default on_missing="raise" it returns a module, not ModuleType | None. That alone silences ~15 false "attribute not defined on None" reports at the segyio/obspy/xarray/findiff call sites.
  • H5Reader/H5Writer inherit _ManagedH5pyFile under TYPE_CHECKING. FiberIO methods annotate the caster class, but the io machinery hands them the managed handle get_handle returns, so the annotation now describes the value the method actually receives. Runtime is untouched.
  • The index's itertuples() loops read their columns directly (zip(meta["attr_name"], …, strict=True)) instead of building a namedtuple per row — faster, and no dynamic row type is left to resolve. The one loop that genuinely needs attribute-style rows (assemble_source_records, which does getattr(row, field) over record fields) keeps itertuples behind iter_rows(df, Row). Those row classes are now the index schema itself: each stored table is declared once as a NamedTuple, and TABLES (the logical column types the DDL is built from) is derived from the annotations, so what a reader sees and what SQLite gets cannot disagree. The derived mapping is identical to the literals it replaces, so the emitted DDL is unchanged and no INDEX_VERSION bump is needed; nullability is now recorded (| None) though not yet enforced as NOT NULL.
  • Three io/xml_binary/utils.py helpers were annotated XMLLaserZones but take XMLBinaryInfo, and CoordManager.to_summary_dict claimed it could return tuple[str, ...] when it only ever returns CoordSummary.
  • The two dynamic-attribute contracts are declared rather than hidden: _TypeCasterMethod (io/core) and _PatchFunction (utils/patch) are Protocols naming the markers those decorators stamp onto the functions they wrap.

The two API questions the rule raised, answered

  • Can a PatchCatalog exist without a resolver? No — a catalog without one holds rows it can never resolve, and every construction path (from_patches, from_file, from_directory, union, _view, and the two direct calls) already passed one. resolver is now a required keyword argument, which makes the None branches in __getstate__, resolve_row, remove, and Spool.has_live_patches dead code; they are gone.
  • What does BaseCoord promise? values, which every coord exposes but the array-backed coords store as a pydantic field while the rest compute in a property (pydantic refuses to let a field shadow an inherited property, and a field would make values a required init argument, so it is declared for type checkers only); and _get_index, which now has a base implementation — see below. start/stop are not promised: approx_equal reaches for them only for evenly sampled coords, which are exactly CoordRange, so it narrows with isinstance instead.

Requiring resolver does break a bare PatchCatalog()/PatchCatalog(backend=...), which used to construct and then fail at resolution time. That constructor has never been in a release (the index landed in #751, after v0.1.20) and every documented path uses the factories, so this seemed the moment to make it say what it needs. It stays keyword-only, so nothing positional moves.

Spool._catalog is likewise declared as a PatchCatalog rather than defaulting to None. __init__ sets it on every construction path (including copy-construction and unpickling), so the None guards in indexer, has_live_patches, and update went with it.

Latent bugs fixed

  • get_next_index on a sorted string coord raised AttributeError: 'CoordString' object has no attribute '_get_index'. String coords have no value spacing to index into, so BaseCoord._get_index now raises CoordError the way the other unsupported string-coord operations do.
  • _coord_record_from_row crashed on a NaT timedelta bound (pd.NaT.to_timedelta64 does not exist); it now uses dc.to_timedelta64, which handles NaT.
  • _apply_binary_ufunc's dimensionality-error handler read other.units while building the message, which the type is not required to carry.

Other

  • CoordManager._get_dim_array_dict(keep_coord=...) is split into _get_dim_coord_dict() and _get_dim_array_dict(); the boolean was a return-type switch with three call sites.
  • BaseCoord.sorted, .evenly_sampled, and .reverse_sorted were annotated -> tuple[int, ...]; they return bools.
  • PatchResolver.live_entries() is typed dict rather than Mapping: every implementation returns the registry dict and remove() pops from it.
  • maybe_mem_map asks for the name it needs (getattr(fid, "name", None)) instead of relying on AttributeError from fid.name.
  • Remaining narrowings use assert with a comment stating the invariant, plus two tagged inline ignores (pydantic hides BaseModel.__getattr__ from checkers on purpose; the numba module object is the dummy shim in the not-installed branch).

The counts comment in pyproject.toml is refreshed: invalid-argument-type 171, invalid-return-type 69, invalid-method-override 36, no-matching-overload 13, not-subscriptable 5. Bringing tests/ into scope (133 diagnostics under the rules already enabled) 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.

Take unresolved-attribute from 138 diagnostics to 26 and enforce the
rule everywhere except core/coords.py and core/spool.py, which are held
back by a documented temporary override until the coord/spool API
questions behind them are settled.

Most fixes are hints that were wrong: optional_import is overloaded so
its default raise-on-missing mode returns a module; the h5 caster
classes inherit the managed handle under TYPE_CHECKING (FiberIO methods
annotate the caster but receive the handle); the index reads its frames
by column instead of through per-row namedtuples; the xml_binary helpers
take XMLBinaryInfo, not XMLLaserZones; to_summary_dict only ever returns
CoordSummary. The dynamic markers stamped by the type-caster and
patch_function decorators are now declared as Protocols.

Three latent bugs surfaced along the way: a NaT timedelta bound crashed
_coord_record_from_row, maybe_mem_map leaned on AttributeError from
fid.name, and the binary-ufunc error path raised AttributeError instead
of UnitError for unitless operands.
@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR refines static typing across coordinate, I/O, utility, and index modules. It adds typed index row views, strict column-wise processing, and runtime state checks.

Changes

Typing and index processing

Layer / File(s) Summary
Coordinate contracts and accessors
dascore/core/coordmanager.py, dascore/core/coords.py, dascore/core/proc/coords.py
Coordinate mappings distinguish coordinate objects from raw arrays. Coordinate accessors and indexing receive type declarations and validation checks.
Typed index row views
dascore/io/index/schema.py, dascore/utils/pd.py, dascore/io/index/ingest.py, dascore/io/index/backend.py, tests/test_io/test_index/test_schema.py
Named row types define table schemas. Typed dataframe iteration replaces generic row access. Schema declarations are tested against SQLite tables.
Column-wise index processing
dascore/io/index/backend.py, dascore/io/index/indexer.py, dascore/io/index/planned.py, dascore/io/index/query.py, dascore/io/index/ingest.py, tests/test_io/test_index/test_planned.py
Index metadata and source statistics use strict column-wise zipping. Unit mappings and timedelta conversion use explicit conversions.
Catalog resolver contracts
dascore/io/index/catalog.py, dascore/core/spool.py
Resolver methods return dictionaries. Catalog construction and state handling require resolvers. Pickling, iteration, source removal, and spool access use the required catalog state.
I/O and utility typing
dascore/io/core.py, dascore/io/tdms/utils.py, dascore/io/xml_binary/utils.py, dascore/utils/*, dascore/units.py, pyproject.toml, tests/test_utils/test_misc.py, tests/test_units.py
Callable metadata, patch wrappers, optional imports, HDF5 inheritance, unit handling, file fallbacks, and checker configuration receive typed contracts or defensive checks. Tests cover stack-name fallback and invalid filter units.

Possibly related PRs

  • DASDAE/dascore#751: Directly relates to typed index schemas, catalog handling, ingestion, and resolver code.
  • DASDAE/dascore#757: Directly relates to CoordManager and coordinate-preserving operations.
  • DASDAE/dascore#796: Relates to static typing changes across coordinate, spool, and utility modules.

Suggested labels: CI

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 84.29% 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.
Title check ✅ Passed The title clearly identifies the primary change: enabling ty's unresolved-attribute rule.
Description check ✅ Passed The description explains the changes, references related issues, documents testing, and completes the applicable checklist items.
✨ 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-2

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 the CI continuous integration label Aug 1, 2026
@codecov

codecov Bot commented Aug 1, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (6a23ffc) to head (8a2d60a).
⚠️ Report is 1 commits behind head on dev.

Additional details and impacted files
@@            Coverage Diff             @@
##               dev      #797    +/-   ##
==========================================
  Coverage   100.00%   100.00%            
==========================================
  Files          164       164            
  Lines        17717     17824   +107     
==========================================
+ Hits         17717     17824   +107     
Flag Coverage Δ
network 48.37% <75.75%> (+0.18%) ⬆️
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: 4

🤖 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/schema.py`:
- Line 281: Update the type annotations for PatchRecord.sample_count_total and
CoordRecord.length from int to an optional integer type. Ensure both row-view
fields explicitly allow None before backend insertion, matching the NULL values
stored in rows.

In `@dascore/units.py`:
- Around line 385-386: Replace the assertion guarding to_quant in the public
to_unit handling with explicit validation: reject None or quantities whose
magnitude is not 1.0 by raising UnitError before accessing to_quant.units.
Preserve the existing valid-unit path and assign to_units only after validation.

In `@dascore/utils/patch.py`:
- Around line 97-99: Update the method_name history-formatting branch to use the
same unnamed-callable fallback as the full history string, reusing the existing
callable-name resolution instead of accessing __name__ directly.

In `@dascore/utils/pd.py`:
- Around line 25-38: Update iter_rows to call DataFrame.itertuples with the
index excluded, so each typed row contains only dataframe column values matching
row_type; preserve the existing iterator cast and public function behavior.
🪄 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: b3f3ce83-ccd8-474c-aa14-20209bc7581e

📥 Commits

Reviewing files that changed from the base of the PR and between 6a23ffc and e275bb1.

📒 Files selected for processing (23)
  • dascore/core/coordmanager.py
  • dascore/core/coords.py
  • dascore/io/core.py
  • dascore/io/index/backend.py
  • dascore/io/index/catalog.py
  • dascore/io/index/indexer.py
  • dascore/io/index/ingest.py
  • dascore/io/index/planned.py
  • dascore/io/index/query.py
  • dascore/io/index/schema.py
  • dascore/io/tdms/utils.py
  • dascore/io/xml_binary/utils.py
  • dascore/proc/coords.py
  • dascore/units.py
  • dascore/utils/array.py
  • dascore/utils/hdf5.py
  • dascore/utils/jit.py
  • dascore/utils/misc.py
  • dascore/utils/patch.py
  • dascore/utils/patch_assembly.py
  • dascore/utils/pd.py
  • pyproject.toml
  • tests/test_io/test_index/test_schema.py

Comment thread dascore/io/index/schema.py Outdated
Comment thread dascore/units.py Outdated
Comment thread dascore/utils/patch.py Outdated
Comment thread dascore/utils/pd.py Outdated
The memmap name lookup moves back inside the try so a file object whose
name property raises still falls back to the in-memory read, iter_rows
passes index=False so the declared row shape lines up positionally with
what pandas yields, and a stack shallower than the requested level now
returns "<unknown>" instead of asserting (the helper runs while building
error messages, where crashing would mask the real error).
Mark the two stored columns records can leave NULL (patches
sample_count_total, coord_defs length) optional in the row views, share
one name helper between the full and method_name history strings so
unnamed callables work in both, and raise UnitError for a to_unit that
is not a magnitude-1 unit instead of asserting on it.
@d-chambers d-chambers added the benchmark Run the benchmark suite label Aug 1, 2026
@codspeed-hq

codspeed-hq Bot commented Aug 1, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 73 untouched benchmarks


Comparing ty-burn-2 (8a2d60a) with dev (52f095c)

Open in CodSpeed

The table column dicts and the row views said the same thing twice, kept
in step by a test. Declare each table as a NamedTuple naming its columns
in order and derive TABLES (the logical types the DDL is built from) from
the annotations, so the row a reader sees and the columns SQLite gets
cannot disagree. The per-column commentary moves onto the fields, and
nullability is now recorded (as `| None`) where it previously was not.

The derived mapping is identical to the literals it replaces, so the
emitted DDL is unchanged. The drift tests give way to one end-to-end
check that a created index's columns and types match the declaration.
@coderabbitai coderabbitai Bot added the IO Work for reading/writing different formats label Aug 1, 2026
Answer the two API questions the rule raised instead of holding the
files back with a per-file override.

A PatchCatalog without a resolver holds rows it can never resolve, and
all six construction paths already passed one, so resolver becomes a
required keyword argument and the None branches it justified go away.
Spool._catalog is declared as a PatchCatalog rather than defaulting to
None; __init__ sets it on every path (copy-construction and unpickling
included), so its None guards go with it.

BaseCoord promises _get_index, which now has a base implementation:
get_next_index on a sorted string coord raised AttributeError, and
string coords have no value spacing to index into, so it raises
CoordError like the other unsupported string-coord operations. start
and stop stay CoordRange's, so approx_equal narrows with isinstance.

Also fix the sorted/evenly_sampled/reverse_sorted return annotations
(they return bools, not tuples) and cover the NaT timedelta envelope.
The per-table alias constants were a third name for what the row
classes and TABLES already say; three of the seven had no users left.
Inserts now take their column list from the row class whose tuples
they are inserting.
@d-chambers d-chambers changed the title Enable ty's unresolved-attribute rule outside coords/spool Enable ty's unresolved-attribute rule Aug 3, 2026
@d-chambers d-chambers added the ready_for_review PR is ready for review label Aug 3, 2026
@coderabbitai coderabbitai Bot removed the IO Work for reading/writing different formats label Aug 3, 2026
String coords do have lexicographic insertion positions; what they lack
is positional semantics, by policy. And iter_rows names the row shape
for readers and type checkers only, not at runtime.
@d-chambers
d-chambers merged commit cbb84e4 into dev Aug 4, 2026
28 checks passed
@d-chambers
d-chambers deleted the ty-burn-2 branch August 4, 2026 05:30
@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

benchmark Run the benchmark suite CI continuous integration

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant