Skip to content

Add a code-quality CI job: ruff, mypy, interrogate, deptry and pip-audit - #245

Closed
tschm wants to merge 3 commits into
bodono:masterfrom
tschm:fix/236-code-quality-ci
Closed

tschm wants to merge 3 commits into
bodono:masterfrom
tschm:fix/236-code-quality-ci

Conversation

@tschm

@tschm tschm commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Closes #236

Stacked. This branch sits on top of #243 (doctests) and #241 (coverage gate) — interrogate cannot reach 100% without #243's docstrings, and the pyproject config sections would otherwise conflict with #241. The last commit is the one to review; it rebases cleanly once those merge.

No linter, type checker, docstring gate or dependency audit ran anywhere in this repo:

$ grep -n "ruff\|lint\|mypy\|bandit\|pip-audit\|interrogate" .github/workflows/*.yml pyproject.toml
(no matches)

The build and correctness side of CI is genuinely thorough — wheel ELF audit, pristine-container smoke test, TSan/ASan, free-threading race detection. This adds the missing axis.

The job

New .github/workflows/quality.yml. Static checks only — no submodules, no compilation, no pixi — so it reports in well under a minute and is the fastest signal on a PR. Every rule set and threshold lives in pyproject.toml, so a contributor running these locally gets the same verdict as CI. Tool versions are pinned: ruff enables new rules by default in each release, so an unpinned ruff check would turn an unrelated ruff release into a red build here.

What it found

A test that could never run. test_problems_with_longs in test_scs_basic.py is guarded by platform.python_version_tuple() < ("3", "0", "0"), which cannot be true on any supported interpreter (requires-python = ">=3.9"), and its body calls the Python 2 builtin long. The suite count is unchanged at 397 passed, which confirms it was never being collected.

Nine dead bindings where the call matters but the name is unused. Each was fixed by dropping the binding, not the call — including sol1 = solver.solve() in test_warm_start_with_spectral, where that first solve seeds the warm start the next line depends on.

Deliberately narrow lint config

select = ["E4", "E7", "E9", "F", "I"] — pyflakes, the pycodestyle error subset, import sorting — rather than ruff's rolling defaults (which flag 129 things here, mostly stylistic).

  • E741 ignored: l is the SCS cone key for the non-negative cone, the domain's name for that quantity.
  • F401 and I001 ignored under test/: the backend modules import an extension solely to discover whether this build has it, inside a try/except that skips the module otherwise — the unused import is the test. And _scs_mkl sets the MKL interface layer at import time, so import order there carries meaning a sorter cannot see. I had ruff sort them, saw it reorder import scs relative to numpy/scipy in the MKL modules, and reverted it — that is your call to make, not a linter's.

Also

Type annotations on the eight public signatures, a module docstring and a _load_module docstring (interrogate now reports 100%), and the meaningless shebang dropped from a package __init__.py. from scs import _scs_direct carries a narrow # type: ignore[attr-defined] because the extension is produced by the meson build and does not exist in the source tree for a static checker to resolve.

All five gates green locally, and the suite is unchanged: 397 passed, 67 skipped, coverage still 100%.

🤖 Generated with Claude Code

tschm and others added 3 commits September 6, 2026 21:17
The package carried prose docstrings on every public symbol but zero runnable
examples, so nothing verified that what they claim is still true — the failure
mode where a docstring keeps rendering perfectly after the behaviour beneath it
changes.

33 examples across `LinearSolver`, `SCS`, `SCS.solve`, `SCS.update` and the
legacy `solve()`. Each is self-contained and deterministic: tight eps_abs/eps_rel,
verbose=False, values rounded before printing.

Also fills two gaps in the same file: `SCS` had no class docstring, and the
public legacy `solve()` had only a `#` comment above it.

`test/test_doctests.py` runs them against the installed package and asserts
`attempted > 0` as well as `failed == 0`, so a module that silently loses its
examples fails rather than passes.

Closes bodono#240

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`--cov=scs` measures nothing when pytest runs from the repo root: `scs/` has
no `__init__.py`, so coverage resolves the name to that namespace-package
directory rather than to the installed package, and reports 0% with "No data
was collected". `source_pkgs` states that `scs` is an importable package name,
so coverage follows the import to site-packages instead.

The threshold lives in `[tool.coverage.report]`, so the CI invocation is a bare
`pytest --cov` with no inline flags. Gate added to build_openmp only; the other
test jobs run plain pytest, where the coverage config is inert.

Also gitignore the coverage data files — the existing entry was `python/.coverage`
from the pre-meson layout.

Closes bodono#238

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
No linter, type checker, docstring gate or dependency audit ran anywhere in
this repo. The build and correctness side of CI is thorough — wheel ELF audit,
pristine-container smoke test, TSan/ASan, free-threading race detection — but
nothing checked the code itself.

New `.github/workflows/quality.yml`: static checks only, no submodules and no
compilation, so it reports in well under a minute. Every rule set and threshold
lives in pyproject.toml, so running these locally gives the same verdict as CI.
Tool versions are pinned because ruff in particular enables new rules by default
in each release, which would otherwise turn an unrelated ruff release into a red
build.

Lint config is deliberately narrow — pyflakes, the pycodestyle error subset and
import sorting — rather than ruff's rolling defaults. E741 is ignored because
`l` is the SCS cone key for the non-negative cone, not a careless name. F401 and
I001 are ignored under test/: the backend modules import an extension purely to
discover whether the build has it, and `_scs_mkl` sets the MKL interface layer
at import time, so import order there carries meaning a sorter cannot see.

What the gates found and this commit fixes:

- `test_problems_with_longs` in test_scs_basic.py was unreachable. Its guard,
  `platform.python_version_tuple() < ("3", "0", "0")`, cannot be true on any
  supported interpreter (requires-python is >=3.9), and its body calls the
  Python 2 builtin `long`. The suite count is unchanged at 397, confirming it
  was never collected.
- Nine dead bindings where the call matters but the name is unused, including
  `sol1 = solver.solve()` in test_warm_start_with_spectral, whose solve seeds
  the warm start the next line depends on — kept as a bare call.

Also adds type annotations to the eight public signatures, a module docstring
and a `_load_module` docstring (interrogate now reports 100%), and drops the
meaningless shebang from a package `__init__.py`.

Closes bodono#236

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@bodono

bodono commented Sep 7, 2026

Copy link
Copy Markdown
Owner

Thanks for the work, but as noted on #236 we're not adding a lint/type-check/audit job for the Python layer. Closing.

@bodono bodono closed this Sep 7, 2026
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.

Add a code-quality CI job: ruff, mypy, interrogate and pip-audit over scs/py/

2 participants