Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MAINT: Add static type checking #28

Merged
merged 29 commits into from
Jan 23, 2025

Conversation

jhlegarreta
Copy link
Contributor

Add static type checking: use mypy to ensure that variable and function calls are using the appropriate types.

Configure and run mypy checks in CI, including the spell checking.

Documentation:
https://mypy.readthedocs.io/en/stable/index.html

@jhlegarreta
Copy link
Contributor Author

jhlegarreta commented Dec 20, 2024

@effigies I see that in this repository the style checking is being done through an explicit call to ruff in a dedicated GHA workflow, as opposed to the way it is done in nireports which I watched as the inspiration for this PR:

- name: Lint NiFreeze
run: pipx run ruff check --diff
- name: Format NiFreeze
run: pipx run ruff format --diff

vs
https://github.com/nipreps/nireports/blob/main/tox.ini#L72
https://github.com/nipreps/nireports/blob/main/.github/workflows/build_test_deploy.yml#L159
https://github.com/nipreps/nireports/actions/runs/12416179089/job/34664200380

The latter seems to be more elegant and self-contained. Is there a canonical way to do this through the NiPreps?

@jhlegarreta jhlegarreta force-pushed the AddStaticTypeChecking branch from 8baa17c to fdf477e Compare December 20, 2024 19:25
Copy link

codecov bot commented Dec 20, 2024

Codecov Report

Attention: Patch coverage is 84.12698% with 10 lines in your changes missing coverage. Please review.

Project coverage is 68.10%. Comparing base (75d26c6) to head (02ff9c4).
Report is 30 commits behind head on main.

Files with missing lines Patch % Lines
src/nifreeze/data/pet.py 30.76% 9 Missing ⚠️
src/nifreeze/data/base.py 94.73% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #28      +/-   ##
==========================================
+ Coverage   67.45%   68.10%   +0.65%     
==========================================
  Files          20       20              
  Lines         977      994      +17     
  Branches      129      130       +1     
==========================================
+ Hits          659      677      +18     
+ Misses        265      263       -2     
- Partials       53       54       +1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@jhlegarreta jhlegarreta force-pushed the AddStaticTypeChecking branch 2 times, most recently from 9e3f60a to e050924 Compare December 20, 2024 20:23
@jhlegarreta
Copy link
Contributor Author

@jhlegarreta jhlegarreta force-pushed the AddStaticTypeChecking branch 4 times, most recently from 363c6e8 to 470c510 Compare December 22, 2024 00:29
@jhlegarreta
Copy link
Contributor Author

jhlegarreta commented Dec 22, 2024

Spent a few hours on this; solved all those warnings that I could: down to 10 8 locally using the --ignore-missing-import flag and including changes in PRs #29, #30 and #44. A few notes:

README.rst Outdated Show resolved Hide resolved
docs/conf.py Outdated Show resolved Hide resolved
src/nifreeze/cli/parser.py Show resolved Hide resolved
src/nifreeze/model/gpr.py Show resolved Hide resolved
src/nifreeze/testing/simulations.py Outdated Show resolved Hide resolved
@oesteban
Copy link
Member

  • seems legitimate: we are requiring scikit-learn>=0.18 (a pretty old one) and n_targets did not appear until scikit_learn==1.3.0. However, testing is using scikit-learn 1.6.0:

We should then upgrade the pinning.

@jhlegarreta jhlegarreta force-pushed the AddStaticTypeChecking branch from 470c510 to e518c42 Compare January 15, 2025 14:00
@jhlegarreta
Copy link
Contributor Author

We should then upgrade the pinning.

Do you want that to be done in this PR or in a separate one so as to avoid the change going unnoticed among all these commits? The potential new PR could be merged before this one, then this one rebased.

@jhlegarreta jhlegarreta force-pushed the AddStaticTypeChecking branch 2 times, most recently from 39bbf76 to 516d0d7 Compare January 15, 2025 19:36
oesteban added a commit that referenced this pull request Jan 20, 2025
@oesteban oesteban changed the title ENH: Add static type checking MAINT: Add static type checking Jan 20, 2025
jhlegarreta pushed a commit to jhlegarreta/nifreeze that referenced this pull request Jan 20, 2025
@jhlegarreta jhlegarreta force-pushed the AddStaticTypeChecking branch from d5268d7 to 88d2565 Compare January 20, 2025 16:58
@effigies effigies self-assigned this Jan 21, 2025
Copy link
Member

@effigies effigies left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mypy is now passing on this project. I think the changes I made were in the spirit of what is intended here.

@@ -52,7 +58,7 @@ def _cmp(lh: Any, rh: Any) -> bool:


@attr.s(slots=True)
class BaseDataset:
class BaseDataset(Generic[*Ts]):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the main new thing I introduced. This BaseDataset class needs to permit a dynamic number of return values for __getitem__, dependent on the subclass. In the future, we should be able to set this as a default:

class BaseDataset[*Ts=Unpack[tuple[()]]]:
    ...

Which, while not particularly pretty, will allow us not to have to type BaseDataset[()] when annotating a variable that is actually this superclass.

Copy link
Member

@effigies effigies left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've reviewed the whole thing, including my own contributions, and I'm good with this. It could probably do with another pair of eyes.

jhlegarreta and others added 16 commits January 22, 2025 22:37
Group keyword arguments into a single dictionary.

Fixes:
```
scripts/optimize_registration.py:133: error:
 Argument "output_transform_prefix" to "generate_command" has incompatible type "str"; expected "dict[Any, Any]"  [arg-type]
```

raised for example in:
https://github.com/nipreps/nifreeze/actions/runs/12437972140/job/34728973936#step:8:141
Use `str` instead of `Path` for `_parse_yaml_config` parameter since
`argparse` passes the argument as a string, not as a `Path`.

Fixes:
```
src/nifreeze/cli/parser.py:74: error:
 Argument "type" to "add_argument" of "_ActionsContainer" has incompatible type
 "Callable[[Path], dict[Any, Any]]"; expected "Callable[[str], Any] | FileType | str"  [arg-type]
```

raised for example in:
https://github.com/nipreps/nifreeze/actions/runs/12437972140/job/34728973936#step:8:47
Use arrays in NumPy's `percentile` function arguments.

Fixes:
```
src/nifreeze/data/filtering.py:80: error:
 Argument 1 to "percentile" has incompatible type "ndarray[Any, dtype[number[Any] | bool_]] | ndarray[tuple[int, ...], dtype[number[Any] | bool_]]";
 expected "_SupportsArray[dtype[bool_ | integer[Any] | floating[Any]]] | _NestedSequence[_SupportsArray[dtype[bool_ | integer[Any] | floating[Any]]]] | bool | int | float | _NestedSequence[bool | int | float]"  [arg-type]
src/nifreeze/data/filtering.py:81: error:
 Argument 1 to "percentile" has incompatible type
 "ndarray[Any, dtype[number[Any] | bool_]] | ndarray[tuple[int, ...], dtype[number[Any] | bool_]]";
 expected "_SupportsArray[dtype[bool_ | integer[Any] | floating[Any]]] | _NestedSequence[_SupportsArray[dtype[bool_ | integer[Any] | floating[Any]]]] | bool | int | float | _NestedSequence[bool | int | float]"  [arg-type]
```

raised for example in:
https://github.com/nipreps/nifreeze/actions/runs/12437972140/job/34728973936#step:8:130
List `sigma_sq` in the GP model slots.

Fixes:
```
src/nifreeze/model/_dipy.py:128: error:
 Trying to assign name "sigma_sq" that is not in "__slots__" of type "nifreeze.model._dipy.GaussianProcessModel"  [misc]
```

raised for example in:
https://github.com/nipreps/nifreeze/actions/runs/12437972140/job/34728973936#step:8:102
Add the dimensionality to the `mask` ndarray parameter annotation.

Fixes:
```
src/nifreeze/model/_dipy.py:140: error:
 "ndarray" expects 2 type arguments, but 1 given  [type-arg]
```

raised for example in:
https://github.com/nipreps/nifreeze/actions/runs/12437972140/job/34728973936#step:8:103
Fix type hint for `figsize` parameter: use `float` as values can be
floating point numbers.

Fixes:
```
src/nifreeze/viz/signals.py:40: error:
 Incompatible default for argument "figsize" (default has type "tuple[float, float]", argument has type "tuple[int, int]")  [assignment]
```

raised for example in:
https://github.com/nipreps/nifreeze/actions/runs/12437972140/job/34728973936#step:8:49
Provide appropriate type hints to the `reg_target_type` parameter of the
`ants._run_registration` function. The expression is set to a tuple in
https://github.com/nipreps/nifreeze/blob/6b6ba707c47b2763c040f3d3a2ceabdcf450eefe/src/nifreeze/estimator.py#L103

Fixes:
```
src/nifreeze/registration/ants.py:459: error:
 Incompatible types in assignment (expression has type "tuple[str, str]", variable has type "str")  [assignment]
```

raised for example in:
https://github.com/nipreps/nifreeze/actions/runs/12437972140/job/34728973936#step:8:133
Import `Bounds` from `scipy.optimize`.

Fixes:
```
src/nifreeze/model/gpr.py:32: error:
 Module "scipy.optimize._minimize" does not explicitly export attribute "Bounds"  [attr-defined]
```

raised for example in:
https://github.com/nipreps/nifreeze/actions/runs/12437972140/job/34728973936#step:8:70
Avoid type checking for private function import statement.

Fixes:
```
src/nifreeze/model/gpr.py:215: error:
 Module "sklearn.utils.optimize" has no attribute "_check_optimize_result"  [attr-defined]
```

raised for example in:
https://github.com/nipreps/nifreeze/actions/runs/12437972140/job/34728973936#step:8:73
Remove unused `namedtuple` definition in test.

Fixes:
```
test/test_gpr.py:31: error:
 First argument to namedtuple() should be "GradientTablePatch", not "gtab"  [name-match]
```

raised for example in:
https://github.com/nipreps/nifreeze/actions/runs/12437972140/job/34728973936#step:8:129
Use `ClassVar` for type hinting the `GaussianProcessRegressor`
`_parameter_constraints` class variable in child class.

Fixes:
```
src/nifreeze/model/gpr.py:156: error:
 Cannot override class variable (previously declared on base class "GaussianProcessRegressor") with instance variable  [misc]
```

raised for example in:
https://github.com/nipreps/nifreeze/actions/runs/12437972140/job/34728973936#step:8:71
Annotate the `optimizer` attribute type in the DiffusionGPR GPR child
class.

Fixes:
```
src/nifreeze/model/gpr.py:234: error:
 "DiffusionGPR" has no attribute "optimizer"  [attr-defined]
```

raised for example in:
https://github.com/nipreps/nifreeze/actions/runs/12437972140/job/34728973936#step:8:82
@jhlegarreta jhlegarreta force-pushed the AddStaticTypeChecking branch from e03f7f2 to 126a5bb Compare January 23, 2025 03:37
Requires making BaseDataset generic on indexable fields
model.predict can return 1, 2 or 3 arrays
Annotate `Kernel.diag()` argument `X`: use `npt.ArrayLike`.

Fixes:
```
src/nifreeze/model/gpr.py:335: error:
 Argument 1 of "diag" is incompatible with supertype "Kernel";
 supertype defines the argument type as
 "Buffer | _SupportsArray[dtype[Any]] | _NestedSequence[_SupportsArray[dtype[Any]]] | bool | int | float | complex | str | bytes | _NestedSequence[bool | int | float | complex | str | bytes]"
  [override]
src/nifreeze/model/gpr.py:335: note: This violates the Liskov substitution principle
src/nifreeze/model/gpr.py:335: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides
src/nifreeze/model/gpr.py:445: error:
 Argument 1 of "diag" is incompatible with supertype "Kernel";
 supertype defines the argument type as
 "Buffer | _SupportsArray[dtype[Any]] | _NestedSequence[_SupportsArray[dtype[Any]]] | bool | int | float | complex | str | bytes | _NestedSequence[bool | int | float | complex | str | bytes]"
  [override]
src/nifreeze/model/gpr.py:445: note: This violates the Liskov substitution principle
src/nifreeze/model/gpr.py:335: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides
```

raised for example in:
https://github.com/nipreps/nifreeze/actions/runs/12437972140/job/34728973936#step:8:93

Documentation:
https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides
@jhlegarreta jhlegarreta force-pushed the AddStaticTypeChecking branch from 126a5bb to 02ff9c4 Compare January 23, 2025 03:40
@oesteban
Copy link
Member

This is such a big PR I fear we will not be able to ever finish it if we stop at every rock on the way. Let's merge this one in and then resolve side effects and nuances (#29, #30, #64) later.

@oesteban oesteban merged commit d9b390b into nipreps:main Jan 23, 2025
11 checks passed
@jhlegarreta jhlegarreta deleted the AddStaticTypeChecking branch January 23, 2025 13:55
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.

3 participants