Add generics to input handlers and several other fixes - #3
Merged
Conversation
The generator had no way to emit a module-level `TypeVar` declaration, nor to change what a class inherits from, both of which the upcoming generic input handlers need: the reference sources declare no TypeVars at all, and a class is made generic by adding a base the reference does not have. Two new tables in `stub_overrides.py` cover it, following the existing `VALUE_TABLES` pattern so unmatched entries are reported as stale: - `TYPE_VARS`, keyed by module name, holds the complete declaration block, emitted at the top of the module body, ahead of every declaration using it. - `CLASS_BASES`, keyed by `module.Class`, replaces the rendered base list verbatim. The `@override` and inheritance bookkeeping keeps following `self.ref.bases`: the reference's own bases are what say which methods a class inherits, whatever the stub declares. `Generic` joins `TYPING_NAMES`, and `Never` and `TypeVar` join `TYPING_EXTENSIONS_NAMES`, so the imports appear once the tables are filled. Both come from `typing_extensions` rather than `typing`: `typing.Never` only exists from 3.11 on, and only `typing_extensions.TypeVar` carries the PEP 696 `default=` argument on the Python versions the checkers target. Both tables are empty here, so the generated stubs are unchanged and `--check` reports no drift. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`ListInputItem.value` was `Any`, both on the attribute and on the `__init__`
parameter, so nothing a plugin author put into an item survived to the command
that received it back. The class is now `Generic[_T_Value]`, with
_T_Value = TypeVar("_T_Value", bound=Value, default=Value)
emitted into `sublime` through the new `TYPE_VARS` table, and
`CLASS_BASES["sublime.ListInputItem"]` supplying the `Generic` base the
reference class does not have.
The bound is `Value` because that is what the item's value is: "A `Value`
passed to the command if the row is selected"
(`references/python38/sublime.py:4331`). The PEP 696 `default=` is what keeps
bare uses working -- the runtime class defines no `__class_getitem__` on the
Python 3.8 host, so it cannot be subscripted there and plenty of code will
keep writing plain `ListInputItem`. `default=Value` rather than `default=Any`
so that such a bare item still has to narrow its value instead of silently
accepting anything.
`TYPING_EXTENSIONS_NAMES` is reordered case-insensitively: it doubles as the
emission order of the `from typing_extensions import ...` line, and adding
`TypeVar` to `sublime`'s existing `deprecated, override` made ruff's `I001`
fire on the generated stub.
`reportAny` / `reportExplicitAny` stay disabled, but their justifying comment
in `pyproject.toml` no longer names `ListInputItem.value`: `set_timeout`
ignoring its callback's return is now the only reason left.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`CommandInputHandler` becomes `Generic[_T_Value_contra]`, `TextInputHandler` becomes `CommandInputHandler[str]` and `ListInputHandler` becomes `CommandInputHandler[_T_Value]`, so the value a selected input passes to the command is one type throughout the handler API. Two TypeVars, because the variance differs. `CommandInputHandler` only *consumes* its value -- the host hands the selected item's value to `preview_`, `validate_` and `confirm_` (`references/python38/sublime_plugin.py:1220-1242`) -- so it is contravariant, with `default=Never`. That makes bare `CommandInputHandler` the top of the handler lattice, which is why `next_input` and `Command.input` keep the reference's own bare `Optional[CommandInputHandler]` return annotation and still accept a `TextInputHandler`, with no `Any` and no `RETURNS` override. `ListInputHandler` also *produces* the value through `list_items`, so `_T_Value` stays invariant and defaults to `Value`, identical to the declaration `sublime` already emits. `preview`, `validate` and `confirm` take that value type instead of the reference's `str`, and `ListInputHandler.description`'s `value` moves from `Value` to `_T_Value`. `list_items` is retyped as an `Iterable` union over the *element* type: `setup_` only checks for the `(items, index)` tuple and then iterates, dispatching per item (`references/python38/sublime_plugin.py:1343-1372`), so any iterable works and the three item forms may be mixed. `BackInputHandler` deliberately keeps its bare reference base: it consumes nothing, so `CommandInputHandler[Never]` is exact. The samples in `tests/typing/check_sublime_plugin.py` spell `validate` and `confirm` overrides with the optional `event` parameter: omitting it narrows the signature and is an override error independent of this change. Three of the samples are written specifically to discriminate the new stubs rather than merely to accept them: the `List[Value]`-parameterized handler overrides `preview`/`validate`/`confirm` with its own value type, which no `text: str` base accepts, and declares the `(items, index)` pre-select form over an `Iterable` rather than a `list`, and `preview_of` annotates its parameter with a `CommandInputHandler[str]` alias that a bare `CommandInputHandler[Never]` is not assignable to under contravariance. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`Value` spells its containers `list[Value]` and `dict[str, Value]`, both of which are invariant, so a plugin author holding a `List[str]` or a `Dict[str, str]` cannot pass it anywhere the stubs expect a `Value` -- not to `Settings.set`, not to `encode_value`, and not as a type argument to the generic input handlers. Add `ValueLike`, the covariant companion spelled with `Sequence` and `Mapping`, plus `CommandArgsLike` mirroring `CommandArgs`. Both are stub-only `sublime_types` names, re-exported from `sublime` (and `ValueLike` from `sublime_plugin`) the way `Value` already is. Nothing consumes them yet; later commits move the input handler bounds and the inbound parameters over. Return types keep describing what Sublime Text hands back, which is always a real `list` or `dict`, so they stay `Value`. `Mapping` deliberately overshoots the runtime, which gates mappings on `isinstance(x, dict)`; spelling it `dict[str, ValueLike]` would be accurate but would not solve the invariance problem the alias exists for. The full rationale, including the `Sequence` duck-typing and `Region` findings, is in the comment above the new `EXTRA_TYPE_ALIASES` table. Mechanically this adds a third stub-only table beside `TYPE_ALIASES` and `EXTRA_TYPE_ALIAS_CLASSES`. Like the latter it has no reference-side counterpart, so it stays out of `VALUE_TABLES` and out of the stale check, and the `SUBLIME_TYPES_REEXPORTS` validation learns to accept its names. `emit_extra_type_aliases` runs after the reference-derived aliases and before the extra classes, so the new aliases land among the plain aliases rather than among the `TypedDict`s, and `Mapping` joins `COLLECTIONS_ABC_NAMES`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Change `bound=Value` to `bound=ValueLike` in all three `TypeVar` declarations in `TYPE_VARS`: `sublime._T_Value`, `sublime_plugin._T_Value` and `sublime_plugin._T_Value_contra`. A bound constrains what an author may parameterize on, which is an inbound position. `Value` spells its containers `list[Value]` and `dict[str, Value]`, both invariant, so `ListInputHandler[List[str]]` and `ListInputItem[Dict[str, str]]` were rejected even though Sublime Text accepts both at runtime. `ValueLike`'s covariant `Sequence` and `Mapping` admit them. Every `default=` stays as it is: `_T_Value` still defaults to `Value` and `_T_Value_contra` still defaults to `Never`. The default is what a bare, unparameterized use resolves to, and a bare use describes what the plugin host actually delivers -- always a real `list` or `dict` -- so it stays the narrow type. The asymmetry between the bound and the default is deliberate and is recorded next to the declarations. The tests exercise both halves of the bound: `TagsInputHandler` is re-parameterized from `List[sublime.Value]` to `List[str]` across `list_items`, `description`, `preview`, `validate` and `confirm`, and a new `LabelsInputHandler` is parameterized on `Dict[str, str]`. `make_list_input_item` in `check_sublime.py` moves to `List[str]`; the two `assert_type` cases above it are left alone, since they pin the `default=Value` behaviour this commit keeps. Against the pre-change stubs these cases fail with 12 errors under pyright and basedpyright and 11 under mypy, all `reportInvalidTypeArguments` / `[type-var]`. `ty` reports nothing: it does not enforce `TypeVar` bounds at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Widen the seventeen parameters through which a plugin passes a value *into* Sublime Text -- `Settings.set`/`__setitem__`/`setdefault`/`get`/ `update`, `sublime.encode_value`, `sublime.expand_variables`'s `value`, `Window.set_project_data`, and the `args` of `sublime.run_command`, `Window.run_command`, `View.run_command`, `View.begin_edit`, `sublime.format_command`, `sublime.html_format_command`, `sublime.command_url` and `CompletionItem.command_completion` -- from `Value`/`CommandArgs` to `ValueLike`/`CommandArgsLike`. The inbound half widens because `Value`'s containers, `list` and `dict`, are invariant: a plugin author holding a `List[str]` or a `Dict[str, str]` could not pass it to any of these. `ValueLike` spells its containers as the covariant `Sequence`/`Mapping` protocols instead, so a concrete container is accepted as it is. The outbound half stays narrow -- `Settings.get`'s return, `Settings.to_dict` and `Settings.setdefault`'s return are untouched, because a return type describes what Sublime Text actually hands back, which is always a real `list` or `dict`; only what a plugin supplies benefits from the wider, duck-typed containers. `Window.set_layout`'s `layout` and `choose_font_dialog`'s `default` are parameters, not returns, and stay untouched for a different reason: they already take a `TypedDict`, not a bare `Value`, so this surface does not reach them. `Settings.update.other` additionally drops `dict[str, Value]` for `Mapping[str, ValueLike]` because it is implemented in Python and genuinely iterates any `Mapping` (`references/python38/sublime.py:3862-3883`), not just the overshoot the rest of the group accepts. Add cases to `tests/typing/check_sublime.py` exercising the widened surface, and `assert_type` cases pinning `Settings.get` and `Settings.to_dict` to their pre-existing, narrower return types. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds `ValueLike` and `CommandArgsLike` to `README.md`'s API quirks list and stub-only names list, amends the "input handlers are generic" bullet to say the parameter is bounded by `ValueLike` and defaults to `Value`, records both entries in `CHANGELOG.md` under `[Unreleased]`, and notes in `CONTRIBUTING.md` that `EXTRA_TYPE_ALIASES` and `EXTRA_TYPE_ALIAS_CLASSES` are exempt from the reference staleness check and must still be paired with a `SUBLIME_TYPES_REEXPORTS` entry. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Both are stub-only names, but unlike every other stub-only `sublime_types` name they are aliases rather than classes, so they carry no docstring, and they sit directly among `Value`, `CommandArgs` and `CompletionValue`, all of which do exist at runtime. Nothing at the point of use told a reader that `from sublime_types import ValueLike` raises `ImportError` on the plugin host; only `README.md` did. Give each the same notice the extra classes emit, and fold the short comment that explained `ValueLike` into its docstring, where an editor shows it on hover rather than only to someone reading the `.pyi`. The emitted blocks keep clear of the bare word `sublime` and of every name in the generator's import tables, so the import derivation is unaffected; `TYPE_CHECKING` is not among those names. `emit_extra_type_aliases` now separates the blocks with a kind of its own. `separate` packs consecutive assignments without a blank line, which was already making the old comment read as documentation of `CompletionValue` above it, and would have run each docstring into the next declaration. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`set_timeout()`/`set_timeout_async()` were the last spot using `Any`; their callback's return value is discarded by the runtime, so `Callable[[], object]` describes it without an escape hatch. With no `Any` left in the stubs, turn `reportAny` and `reportExplicitAny` (basedpyright only) on. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A `.sublime-keymap` context's `operand` can be a JSON string, number, or boolean, e.g. `"operand": 1` for `num_selections` or `"operand": true` for a boolean setting, and the plugin host passes it through unconverted. The reference docstrings spell it `str` on both `EventListener` and `ViewEventListener`; override it to `Value` via `stub_overrides.py`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`README.md` addresses users of the stubs, so it now states only the consequences of the API quirks it lists. The reasoning behind each one already lives next to the code that implements it, in `tools/stub_overrides.py` and `tools/generate_stubs.py`, and was duplicated here in prose. `CONTRIBUTING.md` gains the missing direction of that rule: an override that changes what plugin authors write belongs in the README as a consequence, not as a rationale. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR enhances the generated typing stubs for the Sublime Text plugin API by introducing generics for input handlers and by widening “value layer” input types to accept covariant containers (via new stub-only aliases). It also updates the stub generator and typing test fixtures to validate the new type behavior.
Changes:
- Make
CommandInputHandler,ListInputHandler, andsublime.ListInputItemgeneric with appropriate bounds/defaults, and update related method signatures accordingly. - Introduce stub-only
ValueLikeandCommandArgsLikealiases and widen inbound API parameters to accept them, while keeping outbound/value-return types asValue. - Extend the generator to emit module-level
TypeVarblocks and extrasublime_typesaliases, and add typing tests/docs/changelog updates for the new behavior.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| tools/stub_overrides.py | Adds generic/type-alias override tables and widens inbound parameter/return overrides to support ValueLike/CommandArgsLike and generic input handlers. |
| tools/generate_stubs.py | Updates import derivation and emission order to support TypeVar blocks, class base overrides, and extra stub-only aliases. |
| tests/typing/check_sublime.py | Adds typing assertions validating ListInputItem generics plus ValueLike/CommandArgsLike widening behavior. |
| tests/typing/check_sublime_plugin.py | Adds typing fixtures for generic input handlers, including the Python 3.8 host limitation workaround (TYPE_CHECKING aliases). |
| stubs/sublime-stubs/init.pyi | Regenerates sublime stubs with ValueLike/CommandArgsLike inputs and a generic ListInputItem. |
| stubs/sublime_types-stubs/init.pyi | Regenerates sublime_types stubs to include the new stub-only ValueLike/CommandArgsLike aliases. |
| stubs/sublime_plugin-stubs/init.pyi | Regenerates sublime_plugin stubs with generic input handlers and updated method signatures/return types. |
| README.md | Documents ValueLike/CommandArgsLike and generic input handler usage and constraints for stub consumers. |
| pyproject.toml | Tightens basedpyright configuration around Any usage and clarifies which rules are basedpyright-only. |
| CONTRIBUTING.md | Documents the new “extra alias/class” exceptions to staleness validation and contributor responsibilities. |
| CHANGELOG.md | Records the new generic input handler support and ValueLike/CommandArgsLike as user-visible changes. |
| AGENTS.md | Clarifies documentation expectations (“consequences in README; reasoning in code/commits”). |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
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.
This started as adding generics to input handlers and then ended up changing a whole lot of related stuff.
I plan to release another beta version with this shortly.