chore: make the ruff lint gate pass - #3
Conversation
The lint job has never passed on main: `ruff check .` reported 6,130 errors, and because the test job declares `needs: lint`, no PR has been able to run the test suite. Fix the linter configuration and the code it flags: - Move `select`/`ignore` under `[tool.ruff.lint]`, which ruff has deprecated the top-level form in favour of. - Exclude tutorial notebooks and .ipynb_checkpoints. Notebooks rely on cross-cell state that ruff reads as undefined names; nbsphinx already exercises them at docs build time. - Ignore UP006/UP007/UP045. The package still supports Python 3.8 and rewriting ~950 annotations to PEP 585/604 form is churn with no runtime effect under `from __future__ import annotations`. - Per-file-ignore F401 in __init__.py, where imports are re-exports. Genuine defects the linter was correctly reporting: - `Union` was used in annotations but never imported in seven modules (lda, network, statistics). Deferred annotations kept this latent, but it raises NameError under typing.get_type_hints(), which signature introspection relies on. - `plt` was referenced in module-level annotations in viz/config.py while only imported lazily inside functions; added a TYPE_CHECKING import that keeps matplotlib optional at runtime. - `SpatialLDA`, `MapperResult` and `SpatialTissueData` were used as forward references without TYPE_CHECKING imports. The rest is mechanical: trailing whitespace, import ordering, unused locals, and `== True` to `is True` in assertions. Optional-dependency probes and Moran's I variables carry documented noqa comments. No behaviour change: 456 passed / 2 skipped, unchanged from main, and the public API surface of every module is byte-identical.
With the lint gate passing, the test matrix ran for the first time and every job failed at install: ERROR: Could not find a version that satisfies the requirement fastmcp<3,>=2.0; extra == "all" The package declared `requires-python = ">=3.8"`, but the `mcp` extra pulls in fastmcp, which requires 3.10 or newer. The two have never been consistent; nothing surfaced it because no CI job had reached the install step. Python 3.8 reached end of life in October 2024 and 3.9 in October 2025. The current releases of the core scientific stack have already moved past both: numpy and scipy now require 3.12, and pandas, scikit-learn and matplotlib require 3.11. Raise requires-python to >=3.10, update the classifiers and the black, ruff and mypy target versions to match, and change the CI matrix to 3.10-3.13. Also ignore UP035 alongside the other annotation-style rules. Raising ruff's target surfaced it, and it cannot be applied independently: dropping the typing imports while the annotations still say `Dict[...]` would not import.
|
Pushed With the lint gate passing, the test matrix reached the install step for the first time and every job failed:
Python 3.8 hit EOL in Oct 2024 and 3.9 in Oct 2025. The core scientific stack has already moved past both: current numpy and scipy require 3.12; pandas, scikit-learn and matplotlib require 3.11. The Raised Raising ruff's target surfaced 120 |
The lint job has never passed on
main.ruff check .reports 6,130 errors there, and since the test job declaresneeds: lint, no PR has ever been able to run the test suite — including #2.This PR gets
ruff check .to zero without changing behavior. It should land before #2, which then rebases green.This is not caused by #2
mainfix/physicell-reader-defects(#2)#2 reduced the count by ~1,080. The CI failure it hit is inherited from
main.Why the obvious fix doesn't work
The suggestion going around is
ruff check --fix tests/. Two problems:ruff check ., nottests/. Most errors are inspatialtissuepy/.--fixwould strip 12F401re-exports out of__init__.pyfiles, silently amputating the public API.Configuration
select/ignoremoved under[tool.ruff.lint](ruff deprecated the top-level form — it was emitting a warning on every run)..ipynb_checkpoints/. Notebooks rely on cross-cell state ruff reads as undefined names; nbsphinx already exercises them at docs build.UP006/UP007/UP045with rationale in-file. The package still declares Python 3.8 support, and rewriting ~950 annotations to PEP 585/604 form is pure churn underfrom __future__ import annotations— and would have conflicted heavily with fix: PhysiCell reader orientation and label handling #2. Worth revisiting when the 3.8 floor drops (3.8 has been EOL since Oct 2024, and the CI matrix still tests it).per-file-ignoresforF401in__init__.py.Config changes alone: 6,130 → 4,963.
Real bugs the linter was correctly reporting
These are the reason this PR isn't purely mechanical:
Unionused but never imported inlda/analysis.py,lda/metrics.py,network/clustering.py,network/communicability.py,statistics/colocalization.py,statistics/hotspots.py— 29 annotation sites across 7 modules. Latent only becausefrom __future__ import annotationsdefers evaluation; it raisesNameErrorundertyping.get_type_hints(). Signature introspection does exactly that, which makes this a live risk for the MCP server.pltreferenced in module-level annotations inviz/config.pywhile only imported lazily inside functions. Added aTYPE_CHECKINGimport so matplotlib stays an optional runtime dependency.SpatialLDA,MapperResult,SpatialTissueDataused as forward references with noTYPE_CHECKINGimport.Judgment calls
== True→is Trueintest_physicell_io.py, not ruff's suggested bare truth check. These assertions deliberately verify aboolis returned;assert is_alive(0)would pass on any truthy value and weaken the test.import networkxinsidetry) carry# noqa: F401 (optional dependency probe). The import being unused is the entire point.# noqa: E741 (Moran's I)at 4 sites.I = (n / W) * (numerator / denominator)is the statistic's canonical form; renaming it would obscure the formula.One thing worth a look
topology/nerve.py:263declarededge_set: Set[Tuple[int, int]] = set() # Track added edgesand never used it. I removed it as dead code, but the comment suggests intended edge-deduplication that was never wired up. Not changing that here — flagging it in case nerve construction is emitting duplicate edges.Verification
ruff check .→ All checks passedmain's baseline (the 2 skips are pre-existing optional-NetworkX cases)pkgutil.walk_packagesbefore and after;diffis empty. This was the main risk of touchingF401, so it's checked rather than assumed.Sequencing
Merge this, then I'll rebase #2. There'll be a small conflict in
synthetic/physicell/reader.py(both branches touch the import block) — trivial to resolve.