Repository navigation
Run pyrefly instead of mypy - #928
Open
garth-wells wants to merge 7 commits into
Open
garth-wells wants to merge 7 commits into
garth-wells wants to merge 7 commits into
Conversation
Replaces `[tool.mypy]` with `[tool.pyrefly]`, mapping each option across: check_untyped_defs to check-unannotated-defs, disallow_untyped_defs to the implicit-any-parameter and unannotated-return error kinds, warn_unused_ignores to unused-ignore, and the two per-package overrides to sub-config blocks matching ffcx/ir and ffcx/codegeneration. The exempted modules use `replace-imports-with-any` rather than `ignore-missing-imports`, which applies only when a module cannot be found and so would give a different answer depending on the platform and the way the dependency was installed: pygraphviz is installed on Linux only, and UFL resolves when pip-installed from git but not from an editable checkout. Checking against UFL's own types reports 61 errors where treating it as Any reports 45, so it is replaced with Any, as the mypy configuration effectively did. basix and numba are now checked for real, which costs one further error and found a wrong return annotation. types-setuptools was a stub package for mypy. Nothing in ffcx imports setuptools, and pyrefly carries its own typeshed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Six are real: - access.py used `False` as the not-found sentinel for a dict of handlers, so the value type was `Callable | bool` and the truthy branch was not callable. Use None, as definitions.py already does; this also drops a `# type: ignore`. - interface.py declared form_generator returning `tuple[str]`. The C backend returns (declaration, implementation) and only numba returns a 1-tuple. Its sibling protocols already use `tuple[str, ...]`. - numba/utils.py declared get_void_pointer returning a RawPointer. `numba.types.voidptr(arr)` builds a Signature, as its docstring says. - elementtables.py returned a bare dict of the table and its offsets, so reading the offsets back gave `ndarray | int`. Give it a TypedDict. - optimizer.py's _drop_unreferenced returns whatever shape it is given; say so with overloads. - representation.py passed tensor_shape, a list, where a tuple is declared and only its length is read. Three are suppressed, all the same thing: `**`-unpacking a dict into a NamedTuple, which the checker can follow neither for the field set nor for their types. pyrefly is floored at 1.4 rather than left open below. 1.3 additionally reported `singledispatchmethod` making __call__ a descriptor where the protocol declares a plain method, and an isinstance that could not narrow; 1.4 reports neither. Since unused-ignore is on, suppressing them fails on 1.4 and not suppressing them fails on 1.3, so the floor picks the version whose answer the source matches. Note this cuts both ways: a later release that fixes one of the three suppressions above will fail the same way until the suppression is removed. integral.py has a seventh error that is left alone. `p["part"]` is the option string "full"/"diagonal" while TensorPart is a plain Enum over ints, so the comparison is always False and its branch never runs. Comparing TensorPart.from_str(p["part"]) instead sets rank = 1 and fails all 18 part="diagonal" tests, so the live behaviour is load-bearing and the intent needs deciding rather than the comparison being repaired. The call site carries a FIXME saying so. unbound-name is off: mypy's equivalent is opt-in and was not enabled, so turning it on would newly fail CI. It reports four sites, of which the `t` in elementtables.py looks genuinely reachable -- an unhandled 3D cell type falls through with it unset. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
garth-wells
force-pushed
the
garth/pyrefly
branch
from
October 18, 2026 17:59
52cb151 to
7e681dd
Compare
no-any-return is mypy's warn_return_any, which the configuration this replaced had and the conversion dropped. The two sites are asarray calls whose list argument is Any, because basix.geometry is; bind the result to the declared type the dtype= argument already asserts. unbound-name has no mypy equivalent in use: possibly-undefined is opt-in and was not enabled. Four sites: - elementtables.py built a permuted facet table for a tetrahedron or a hexahedron and fell through for anything else, so an unsupported cell reached `t` unset and raised UnboundLocalError rather than saying what was unsupported. Raise NotImplementedError naming the cell and tdim. - elementtables.py reads `weights` under the same `avg` test it is set under, which the checker cannot connect; assert the invariant. - main.py created the profiler under `if xargs.profile` and used it under a second one. Hold it in an Optional and test that instead. - naming.py sets `kind` on every iteration of a loop that could be empty. Note this changes an error path: compute_signature([]) raised NameError and now returns a signature with an empty kind. Nothing can depend on that, since it was a crash. implicit-reexport reports nothing today and keeps it that way. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Contributor
|
Why replace and not add as an additional check? The two catch different things. |
Member
Author
I don't see that two tools (or 3 or 4, or ..) brings added value in this case. We use pyrefly only for DOLFINx and Basix. This provides consistency across the projects. |
Contributor
|
Mypy is more mature, if only one tool is to be picked, I think that's the one. We have been running in dolfiny |
The strict preset costs 115 errors here, all of them from four kinds: implicit-any-lambda (43), implicit-any-type-argument (38), missing-override-decorator (18) and implicit-any-empty-container (16). Everything else in it already passes, so take the preset and opt out of those four, each recorded with what it currently costs. That turns on, among others, redundant-cast, redundant-condition, unnecessary-comparison, unnecessary-isinstance, method-assign, bad-typed-dict, unsafe-overlapping, assert-type, invalid-annotation, unknown-base-class and deprecated -- none of which mypy was running, and none of which FFCx violates today. No source changes: this is configuration only. The four opt-outs are annotation debt rather than defects, so they are best paid down a package at a time. Two further steps are available and not taken here: unknown-return-type (19) and checking UFL against its own types rather than replacing it with Any (36), the latter at the cost of the install-independence noted above. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
unused-ignore, mapped from mypy's warn_unused_ignores, reports an unused `# pyrefly: ignore` but says nothing about an unused `# type: ignore`: pyrefly leaves those alone deliberately, since they may be shared with another checker, and offers only a --remove-unused-ignores fixer. FFCx had 20 `# type: ignore` against 4 `# pyrefly: ignore`, so the check was watching 4 suppressions out of 24 where mypy had watched all of them. Seven of the twenty were already dead. Stripping all twenty and running `pyrefly suppress` brings back the thirteen that still suppress something, each with the code it needs -- no-matching-overload or bad-argument-type -- rather than a blanket ignore, and leaves the dead seven out. enabled-ignores keeps it that way: `# type: ignore` no longer suppresses at all, so a suppression cannot be added that unused-ignore does not watch. All 17 are now covered. Comments only; no behaviour change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
_graph_name arrived from #916 while this branch was open. Its `name` parameter is unannotated, so `parts` is a list[Any] and "_".join(parts) is an implicit Any against the declared -> str, which no-any-return now reports. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Replaces the
mypy -p ffcxCI step withpyrefly check, and fixes the type errors that surfaced.Two commits: the CI and configuration swap, then the source changes it required.
Configuration
[tool.mypy]becomes[tool.pyrefly], mapping each option across:check_untyped_defscheck-unannotated-defsdisallow_untyped_defsimplicit-any-parameter,unannotated-returnwarn_unused_ignoresunused-ignore[[sub-config]]onffcx/ir/**,ffcx/codegeneration/**The exempted modules use
replace-imports-with-anyrather thanignore-missing-imports. The latter applies only when a module cannot be found, so the result would depend on the platform and on how the dependency was installed: pygraphviz is installed on Linux only, and UFL resolves when pip-installed from git but not from an editable checkout. Checking against UFL's own types reports 61 errors where treating it asAnyreports 45, so it is replaced withAny, as the mypy configuration effectively did. basix and numba are now checked for real, which costs one further error and caught a wrong return annotation.types-setuptoolsis dropped: it was a stub package for mypy, nothing inffcximports setuptools, and pyrefly carries its own typeshed.unbound-nameis off. mypy's equivalent is opt-in and was not enabled, so turning it on would newly fail CI. It reports four sites; three are guarded twice on the same condition, but thetinelementtables.pylooks genuinely reachable — an unhandled 3D cell type falls through with it unset.Fixes
Six were real:
access.pyusedFalseas the not-found sentinel for a dict of handlers, so the value type wasCallable | booland the truthy branch was not callable. NowNone, asdefinitions.pyalready does — this also drops a# type: ignore.interface.pydeclaredform_generatorreturningtuple[str]. The C backend returns(declaration, implementation); only numba returns a 1-tuple. The sibling protocols already usetuple[str, ...].numba/utils.pydeclaredget_void_pointerreturning aRawPointer.numba.types.voidptr(arr)builds aSignature, as its own docstring says.elementtables.pyreturned a bare dict of the table and its offsets, so reading the offsets back gavendarray | int. It now has aTypedDict.optimizer.py's_drop_unreferencedreturns whatever shape it is given; overloads say so.representation.pypassedtensor_shape, a list, where a tuple is declared and only its length is read.Three are suppressed, all the same thing:
**-unpacking a dict into a NamedTuple, which the checker can follow neither for the field set nor for their types.Why pyrefly is floored at 1.4
Worth knowing before the next upgrade. pyrefly 1.3 additionally reports
singledispatchmethodmaking__call__a descriptor where the protocol declares a plain method, and anisinstancethat cannot narrow; 1.4 reports neither. Becauseunused-ignoreis on — mypy'swarn_unused_ignores, which this project had — suppressing them fails on 1.4 and not suppressing them fails on 1.3. There is no source that satisfies both, so the floor picks the version whose answer the source matches. Verified: clean on 1.4.0, 3 errors on 1.3.2.This cuts both ways: a later release that fixes one of the three suppressions above will fail CI the same way until that suppression is removed. That is
unused-ignoredoing its job, but it does couple the repo to the checker version.One bug left alone
ffcx/ir/integral.pycomparesp["part"], the option string"full"/"diagonal", againstTensorPart, a plainEnumover ints. That comparison is always False, so its branch never runs. ComparingTensorPart.from_str(p["part"])instead setsrank = 1and fails all 18part="diagonal"tests, so the live behaviour is load-bearing and the intent needs deciding rather than the comparison simply being repaired. The call site carries a FIXME; filed as #929.Verification
pyrefly checkreports 0 errors on 1.4.0, both with UFL resolvable and not, and exits 1 on a deliberately introduced error so the CI step fails when it should. Full suite 474 passed, demos 232 passed, ruff andruff formatclean.Note this touches
elementtables.py, which #903 restructures — they will conflict.🤖 Generated with Claude Code