Skip to content

fix(feature_utils): bind X and ndf_ on the routes that read them - #2097

Merged
lmeyerov merged 1 commit into
masterfrom
fix/pyright-feature-pipeline
Sep 18, 2026
Merged

lmeyerov merged 1 commit into
masterfrom
fix/pyright-feature-pipeline

Conversation

@lmeyerov

Copy link
Copy Markdown
Contributor

Fifth of the pyright-ratchet cleanup series. Ratchet 255 → 245.

Two functions read a local that an earlier branch may never have assigned, and both are reachable.

transform()

y and T are pre-initialized at the top; X is bound only inside the nodes/edges branches. kind is a plain str that FastEncoder stores without validation, so any other value falls through to if not tX.empty and not X.empty:

transform(df, DataFrame([]), res, 'bogus', ...)
master: UnboundLocalError: cannot access local variable 'X'
here:   X.empty=True y.empty=True

get_numeric_transformers()

Returns ndf_ unconditionally but assigns it only under if ndf is not None — while the sibling y_ = y is assigned before its guard, which is the shape the author clearly intended. get_numeric_transformers(None, None) raised UnboundLocalError on master; it now returns ndf_=None.

Two sites deliberately left alone

Both looked like the same defect and neither is a bug:

  • The skrub names are function-locals from a guarded import, but every use sits under the same has_skrub flag — including the isinstance at line 1025, which I checked individually.
  • scale()'s scaler/scaler_target look unbound for an unrecognized kind, but kind is validated upstream (kind must be one of 'nodes' or 'edges') and a missing encoder raises AttributeError before the branch chain. I tried to construct a reaching call and could not.

Hardening those would have meant three # pragma: no cover lines in a file no gate-feeding lane can execute — a poor trade for changes that fix nothing. They stay in the ratchet baseline, visible.

A gap worth naming

feature_utils.py collects zero coverage in the lanes the changed-line gate combines (core, gfql, polars), because none of them installs scikit-learn — the umap/ai lanes that do exercise it produce no coverage artifact at all. Any change to this file is therefore invisible to that gate.

The one sklearn-only line here carries a pragma, and I checked its claim rather than asserting it: line 873 is covered when the umap tests run with sklearn present.

Wiring an umap/ai coverage artifact into changed-line-coverage would fix this properly and unblock future work in this file. It is a real CI change (new artifact + combine step + a needs edge, and per #2018 a skipped prerequisite skips the gate), so I have not folded it in here.

Verification

bin/lint.sh (ruff, type-hygiene 4448, comment-density 1151) · pyright guard 245 · mypy 351 files · changed-line coverage 1/1 = 100% · tests pass with and without sklearn (2 passed/1 skipped, then 3 passed) · 9 passed in the umap env across feature/umap suites · meta-guards run · the recognized-kind test confirmed discriminating by disabling the nodes branch.

🤖 Generated with Claude Code

https://claude.ai/code/session_017ropeBMLJUuy6ViYwy15ud

Two functions read a local that an earlier branch may never have assigned, and
both are reachable.

transform() pre-initializes y and T at the top and then binds X only inside its
`nodes`/`edges` branches. `kind` is a plain str and FastEncoder stores it
without validation, so any other value falls through to `if not tX.empty and
not X.empty` and raises UnboundLocalError. Demonstrated against master:

    transform(df, DataFrame([]), res, 'bogus', ...)
    master: UnboundLocalError: cannot access local variable 'X'
    here:   X.empty=True y.empty=True

get_numeric_transformers() returns ndf_ unconditionally but assigns it only
under `if ndf is not None`, while the sibling y_ is assigned before its guard.
get_numeric_transformers(None, None) raised UnboundLocalError on master and now
returns ndf_=None.

Two further sites in this file were considered and deliberately left alone,
because neither is a bug. The skrub names are function-locals from a guarded
import, but every use sits under the same has_skrub flag. scale()'s scaler and
scaler_target look unbound for an unrecognized kind, but kind is validated
upstream ("kind must be one of 'nodes' or 'edges'") and a missing encoder
raises AttributeError before the branch chain. Hardening those would have meant
three `# pragma: no cover` lines in a file no lane feeding the changed-line
gate can execute -- a poor trade for changes that fix nothing.

That gap is worth naming: feature_utils.py collects ZERO coverage in the lanes
the changed-line gate combines (core, gfql, polars), because none installs
scikit-learn. The one sklearn-only line here carries a pragma whose claim was
checked -- it is covered when the umap tests run with sklearn present.

Pyright ratchet 255 -> 245 (-10). Changed-line coverage 1/1.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_017ropeBMLJUuy6ViYwy15ud
@lmeyerov
lmeyerov merged commit f5ac27c into master Sep 18, 2026
80 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant