Skip to content

Add generics to input handlers and several other fixes - #3

Merged
FichteFoll merged 13 commits into
mainfrom
feature/generic-input-handlers
Aug 16, 2026
Merged

Add generics to input handlers and several other fixes#3
FichteFoll merged 13 commits into
mainfrom
feature/generic-input-handlers

Conversation

@FichteFoll

Copy link
Copy Markdown
Member

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.

FichteFoll and others added 13 commits August 15, 2026 12:10
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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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, and sublime.ListInputItem generic with appropriate bounds/defaults, and update related method signatures accordingly.
  • Introduce stub-only ValueLike and CommandArgsLike aliases and widen inbound API parameters to accept them, while keeping outbound/value-return types as Value.
  • Extend the generator to emit module-level TypeVar blocks and extra sublime_types aliases, 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.

@FichteFoll
FichteFoll merged commit 0a25c0f into main Aug 16, 2026
10 checks passed
@FichteFoll
FichteFoll deleted the feature/generic-input-handlers branch August 16, 2026 10:10
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.

2 participants