Skip to content

refactor: tighten five annotations that were narrower than the values - #129

Merged
michael-denyer merged 1 commit into
masterfrom
chore/pyrefly-src-remainder
Jul 27, 2026
Merged

refactor: tighten five annotations that were narrower than the values#129
michael-denyer merged 1 commit into
masterfrom
chore/pyrefly-src-remainder

Conversation

@michael-denyer

Copy link
Copy Markdown
Owner

Continues the pyrefly burn-down. 77 → 70 entries. Seven entries across five src
modules, each a declaration that did not describe what the code actually passes or holds.

Site Errors Fix
core/progress.progress_iterator(iterable: Iterator) 1 Iterable
io/plink.py total= gets int | ndarray 1 int(bed.sid_count)
lmm/likelihood._frozen(data: list[int]) called with tuples 2 Sequence[int]
lmm/loco.py union annotation inside the branch 1 declare before the branch
lmm/special.chi2_sf_batch .astype on a frompyfunc result 1 np.asarray(..., dtype=)
gwas.GWASResult.timing = field(default_factory=dict) 1 default_factory=GWASTiming

A few worth expanding:

  • progress_iterator only ever does for i, item in enumerate(iterable), so Iterable
    is what it actually requires. Iterator rejected the range that io/plink.py passes it.
    Widening a parameter cannot break a caller.
  • loco.py annotated snps_global_mask: np.ndarray | None on the assignment inside the
    branch that builds it, which made the declared type a union at the point of the very next
    item assignment. Declared before the branch instead.
  • _frozen passes data straight to np.array, which takes any sequence; two of its five
    call sites pass tuples.
  • GWASResult.timing is the same fix as PipelineResult.timing in refactor: declare real types in pipeline.py and pipeline_config.py #123.

Verification

Two of these sit on numerically sensitive paths — special.py feeds p-value computation and
loco.py gates SNP selection — so this was checked, not argued.

chi2_sf_batch is bit-identical across 13 edge cases (NaN, negative, zero, 1e-300, 1e-12,
0.5, 1, 2, 10, 100, 700, 1e6, inf), comparing against the previous expression evaluated side
by side in the same process:

chi2_sf_batch bit-identical across 13 edge cases: True
Compared (before vs after) Result
All 4 LMM modes × both backends, .assoc.txt byte-identical
LOCO: .assoc.txt, pve/pve_se, per-chromosome kinship sums, stats cache identical
pytest tests/ 2243 passed, 10 skipped, 3 xfailed
ruff check / ruff format --check clean
pyrefly check --baseline on 3.11.13/numpy 2.4.6 and 3.13.5/numpy 2.5.1 0 errors both

What's left in src/

Seven entries, deliberately not in this PR because they are not one-line annotations:

  • cli.py:549, gwas.py:189backend: str reaching PipelineConfig's Literal. The CLI
    already constrains it with click.Choice, so the type can simply say so.
  • cli.py:339-gk/-lmm are validated as mutually exclusive and one-required ~70 lines
    above the dispatch, so lmm is not narrowed at the call.
  • lmm/runner_numpy_streaming.py ×4 — RunnerTiming misuse (.clear() and
    dict(...) on a TypedDict) plus a _LazySnpMeta assigned to a list | None parameter,
    currently held together by an assert in production code.

Next PR.

Clears 7 pyrefly baseline entries across five src modules (77 -> 70). Each is
a declaration that did not describe what the code actually passes or holds.

`core/progress.progress_iterator(iterable: Iterator)` only ever does
`for i, item in enumerate(iterable)`, so `Iterable` is what it requires;
`Iterator` rejected the `range` that `io/plink.py` passes it. Widening a
parameter cannot break a caller.

`io/plink.py` also fed `total=` an `int | ndarray`, because `bed.sid_count`
carries that type through `n_chunks`. It is an int at runtime, so it says so.

`lmm/likelihood._frozen(data: list[int])` is called with tuples on two of its
five call sites. It passes `data` straight to `np.array`, which takes any
sequence, so the parameter is `Sequence[int]`.

`lmm/loco.py` annotated `snps_global_mask: np.ndarray | None` on the assignment
*inside* the branch that builds it, which made the declared type a union at the
point of the very next item assignment. Declared before the branch instead, with
the array built through a local.

`lmm/special.chi2_sf_batch` called `.astype` on the result of a
`np.frompyfunc` ufunc, declared as returning a scalar. `np.asarray(..., dtype=)`
is the same cast and types correctly.

`gwas.GWASResult.timing = field(default_factory=dict)` infers
`dict[Unknown, Unknown]` rather than `GWASTiming`; the TypedDict is callable and
returns the same `{}`. Same fix as `PipelineResult.timing` in #123.

No behaviour change, and checked rather than argued, because `special.py` feeds
p-value computation and `loco.py` gates SNP selection:

- `chi2_sf_batch` is bit-identical across 13 edge cases (NaN, negative, zero,
  1e-300, 1e-12, 700, 1e6, inf) against the previous expression evaluated
  side by side.
- All four LMM modes on both backends produce byte-identical .assoc.txt.
- The LOCO path is identical: same .assoc.txt, pve/pve_se, per-chromosome
  kinship sums and stats cache.

`pytest tests/` -- 2243 passed, 10 skipped, 3 xfailed.

Baseline regenerated on Python 3.11 / numpy 2.4.6 per #121; 0 errors on both
3.11.13/2.4.6 and 3.13.5/2.5.1.
@michael-denyer
michael-denyer merged commit 20e8766 into master Jul 27, 2026
12 checks passed
@michael-denyer
michael-denyer deleted the chore/pyrefly-src-remainder branch July 27, 2026 10:26
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