Type public Epochs, Evoked, and io members fully#14056
Conversation
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Wait, MNE-Python is going typed now? |
|
We've discussed it and proposed it in grants but never gotten it funded IIRC |
drammock
left a comment
There was a problem hiding this comment.
thanks for tackling this @larsoner. I looked pretty closely at all the readers in io/, the tests, and the utils. io/base.py I skimmed, and epochs.py and evoked.py I skipped.
There are merge conflicts now but once those are gone and you've rebutted/addressed all my comments, +1 for merge.
| input_fname: Path | str | FileLike, | ||
| eog: list | tuple | None = None, | ||
| misc: list | tuple | None = None, | ||
| stim_channel: Literal["auto"] | str | list | int = "auto", |
There was a problem hiding this comment.
is there a point to saying Literal["auto"] | str instead of just str here (and elsewhere in the diff)?
There was a problem hiding this comment.
I actually think it's a tiny bit clearer -- "auto" is a special thing, whereas any other string is a channel name and behaves differently. This is consistent with how our docstring documents it as well
stim_channel : ``'auto'`` | str | list of str | int | list of int
... which is enforced to be consistent the checker!
| s = re.sub(r"\s*\(\s*default[^)]*\)", "", s, flags=re.I) # (default X) | ||
| s = re.sub(r"\s*,\s*default[:=]?\s*[^,|]+", "", s, flags=re.I) # , default X |
There was a problem hiding this comment.
these should be removed from the docstring sources, IMO. AFAIR our convention is to state the default in the param description prose, not in the type description. Can be a TODO for a follow-up.
There was a problem hiding this comment.
Yeah that's a good follow-up, and removing this would be a way to find the violators.
| s = s.replace("~", "") # drop the sphinx "abbreviate" marker | ||
| s = re.sub(r"\s*\(\s*default[^)]*\)", "", s, flags=re.I) # (default X) | ||
| s = re.sub(r"\s*,\s*default[:=]?\s*[^,|]+", "", s, flags=re.I) # , default X | ||
| s = re.sub(r"\s*,?\s*optional\b", "", s, flags=re.I) # , optional |
There was a problem hiding this comment.
"optional" is also not something we're supposed to do according to the contrib guide (item 2)
| s = re.sub( | ||
| r"\b(list|tuple|dict|set) of [\w.]+", r"\1", s, flags=re.I | ||
| ) # list of X -> list |
There was a problem hiding this comment.
the . in [\w.]+ should be escaped? Assume it's meant to capture the dot in e.g. "list of numpy.ndarray"
There was a problem hiding this comment.
I always have to look this stuff up ! Looks like it should be okay
In a character class (square brackets) any character except ^, -, ] or \ is a literal.
| s = re.sub( # textual Literal["a", "b"] (from string annotations) -> a | b | ||
| r"\bLiteral\[([^\]]*)\]", lambda m: m.group(1).replace(",", "|"), s | ||
| ) | ||
| s = re.sub( | ||
| r"\{([^}]*)\}", lambda m: m.group(1).replace(",", "|"), s | ||
| ) # {a, b} -> a|b |
There was a problem hiding this comment.
IIUC, these two will map Literal["a", "b"] or {"a", "b"} to "a"| "b" (note the asymmetric space). Does that matter?
| elif "array" in low or low == "ndarray": | ||
| atoms.add("array") |
There was a problem hiding this comment.
this could miss marray if we happen to take in or return masked arrays somewhere
There was a problem hiding this comment.
Yeah I expect we might have to adjust this if we add more types
| # free-form docstring types (e.g. "matplotlib colormap | (colormap, bool)") | ||
| # cannot be matched mechanically, so only validate comparable ones | ||
| if not doc_atoms or not all(_is_type_like(d) for d in doc_atoms): | ||
| continue |
There was a problem hiding this comment.
this feels like a case of "we shouldn't be doing that in the first place". Are there other cases like this besides the interactive colormap one? would it make sense to have an explicit allowlist (so we have something we can work on whittling down)?
| if not fname.startswith("_") and _defined_under(func, name): | ||
| _check_type_hints(func, cls=None, where=name, incorrect=incorrect) | ||
| for func, cls in _documented_callables(): | ||
| _check_type_hints(func, cls=cls, incorrect=incorrect) |
There was a problem hiding this comment.
seems weird that the helper mutates the input incorrect list, and doesn't return anything. Unexpected (but I guess ok as it's not user-facing and only used here?)
| ) | ||
| super().generic_visit(node) | ||
|
|
||
| def visit_If(self, node): |
There was a problem hiding this comment.
maybe worth a comment that the capital I is intentional / the func name is dictated by the way ast.NodeVisitor works
|
|
||
| def read_raw_fieldtrip(fname, info, data_name="data") -> RawArray: | ||
| def read_raw_fieldtrip( | ||
| fname: Path | str, info: dict | None, data_name: str = "data" |
There was a problem hiding this comment.
why info: dict | None here instead of info: Info | None?
There was a problem hiding this comment.
Preexisting cruft with the docstring ... I'll fix it here though!
|
Pushed a commit with the |
|
Any recommendations on how to proceed with adding type annotations to other parts of the codebase? When we write new stuff and re-write existing stuff, should we type annotate it? |
@larsoner and I are planning to crank through the rest of the codebase now-ish, one submodule at a time, with the help of the docstub package and an LLM. LMK if you want to assist, so we don't duplicate effort |
Okay this PR diff is big, but it's simple enough at its core:
mne/io,mne/evoked.py, andmne/epochs.py, ensuring the test passesI had Claude Opus 4.8 work on the test, but I simplified it and checked it. I also had Claude do the drudge-work of putting all those type annotations inline. The test ensures they're correct, though, so this should make review a bit easier (hopefully)!
This adds a little bit of work to future contributions having to type-define two places (docstring + code), but I don't think that's too onerous. Someday if numpy/numpydoc#196 (or numpy/numpydoc#601) lands we could maybe DRY things again.
I think with this scaffolding in place, we're safe to move to typing other modules in a similar fashion (or by using typestubs as a first pass, which @drammock is going to look into IIRC). In the meantime, I suggest we add this since it should make things slightly nicer for our end-users.
I considered
pydoclintas a checker, but it is a static tool, so we need to get rid of%/docdictbefore we could pursue that.