Skip to content

Conversation

@rchl
Copy link
Member

@rchl rchl commented Jan 30, 2026

Type params and return value of Request object. Typing Request ensures that all the send_request variants infer param/return types and enforce those for the handlers/chained promise. So more safety.

Also type Notification and Response but those don't benefit much from typing since there is no response to handle.

Comment on lines 11 to 12
P = TypeVar('P', bound=None | bool | int | Uint | float | str | Mapping[str, Any] | Iterable[Any])
R = TypeVar('R')
Copy link
Member Author

Choose a reason for hiding this comment

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

Those are defined in two places. Potentially could be imported from a single place but importing such generic names feels weird. Perhaps should be given more meaningful names and then imported? Or leave as is?

Comment on lines +127 to 136
def handle_rename_async(self, responses: list[tuple[WorkspaceEdit | None | Error, weakref.ref[Session]]],
label: str, rename_command_args: dict[str, Any]) -> None:
for response, weak_session in responses:
if (session := weak_session()) and response:
if isinstance(response, Error):
print(f'LSP: Error response during rename: {response}')
return
prompt_for_workspace_edits(session, response, label=label) \
.then(partial(self.on_prompt_for_workspace_edits_concluded, weak_session, response, label)) \
.then(lambda accepted: accepted and self.window.run_command('lsp_rename_path', rename_command_args))
Copy link
Member Author

@rchl rchl Jan 30, 2026

Choose a reason for hiding this comment

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

Previously Error was not handled but in theory it can happen. I just interrupt rename if that happens but probably it's not the best way to do it...

result: NotRequired[Any]


class Request(Generic[P, R]):
Copy link
Member Author

@rchl rchl Jan 30, 2026

Choose a reason for hiding this comment

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

We could have third generic for error result but it's the same in 99.9% of cases and it would not be possible to optionally provide it (at least until Python 3.13) so skipped it. Only initialize really used different error type but we haven't used anything from it so using standard ResponseError works for it too.

Comment on lines 40 to 47
class HierarchyDataProvider(TreeDataProvider):

def __init__(
self,
weaksession: weakref.ref[Session],
request: Callable[..., Request],
request_handler: Callable[..., list[HierarchyItemWrapper]],
request: Callable[[Any], Request[Any, Any]],
request_handler: Callable[[Any], Any],
root_elements: list[HierarchyItemWrapper]
Copy link
Member Author

Choose a reason for hiding this comment

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

Cheating with Any here since typing this properly is pretty much impossible without refactoring.

UINT_MAX = INT_MAX

R = TypeVar('R')
LspPayload: TypeAlias = Union[None, bool, int, Uint, float, str, Mapping[str, Any], Iterable[Any]]
Copy link
Member

Choose a reason for hiding this comment

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

This should be exactly LSPAny, no?

Copy link
Member Author

Choose a reason for hiding this comment

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

That triggers errors:

Type "InitializeParams" cannot be assigned to type variable "P@Request"
  Type "InitializeParams" is not assignable to upper bound "LSPAny" for type variable "P@Request"
    Type "InitializeParams" is not assignable to type "LSPAny"
      "InitializeParams" is not assignable to "Dict[str, LSPAny]"
      "InitializeParams" is not assignable to "List[LSPAny]"
      "InitializeParams" is not assignable to "str"
      "InitializeParams" is not assignable to "int"
      "InitializeParams" is not assignable to "float"
      "InitializeParams" is not assignable to "bool"
  ... (basedpyright)

We actually need Mapping[str, Any], Iterable[Any] here.

Copy link
Member Author

Choose a reason for hiding this comment

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

More specifically we need Iterable[Any] because this works too: Union[LSPAny, Iterable[Any]]

Copy link
Member Author

Choose a reason for hiding this comment

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

I've asked Claude and it gave me the following (hopefully true) answer.


The issue is that TypedDict is not considered compatible with dict[str, T] by type checkers, even though at runtime they're both dictionaries. TypedDict is a specific structured type with known keys, while dict[str, T] is a general mapping.

Solution 1: Cast to dict (Runtime)

from __future__ import annotations
from typing import TypeAlias, TypedDict, cast

Uint = int

class InitializeParams(TypedDict):
    foo: int

LSPArray: TypeAlias = list[LSPAny]
LSPObject: TypeAlias = dict[str, LSPAny]
LSPAny: TypeAlias = LSPObject | LSPArray | str | int | Uint | float | bool | None

def f(p: LSPAny) -> None:
    pass

a: InitializeParams = {'foo': 1}

f(cast(dict, a))  # ✓ Works

Solution 2: Include TypedDict Types in LSPAny

from __future__ import annotations
from typing import TypeAlias, TypedDict, Mapping, Any

Uint = int

class InitializeParams(TypedDict):
    foo: int

LSPArray: TypeAlias = list[LSPAny]
LSPObject: TypeAlias = dict[str, LSPAny] | Mapping[str, Any]
LSPAny: TypeAlias = LSPObject | LSPArray | str | int | Uint | float | bool | None

def f(p: LSPAny) -> None:
    pass

a: InitializeParams = {'foo': 1}
f(a)  # ✓ Works

Solution 3: Use Protocol or Mapping

from __future__ import annotations
from typing import TypeAlias, TypedDict, Any
from collections.abc import Mapping

Uint = int

class InitializeParams(TypedDict):
    foo: int

LSPArray: TypeAlias = list[LSPAny]
LSPObject: TypeAlias = Mapping[str, Any]  # More permissive
LSPAny: TypeAlias = LSPObject | LSPArray | str | int | Uint | float | bool | None

def f(p: LSPAny) -> None:
    pass

a: InitializeParams = {'foo': 1}
f(a)  # ✓ Works

Solution 4: Add type: ignore Comment

from __future__ import annotations
from typing import TypeAlias, TypedDict

Uint = int

class InitializeParams(TypedDict):
    foo: int

LSPArray: TypeAlias = list[LSPAny]
LSPObject: TypeAlias = dict[str, LSPAny]
LSPAny: TypeAlias = LSPObject | LSPArray | str | int | Uint | float | bool | None

def f(p: LSPAny) -> None:
    pass

a: InitializeParams = {'foo': 1}

f(a)  # type: ignore[arg-type]

Why This Happens

Type checkers like mypy and Pyright treat TypedDict as incompatible with dict[str, T] because:

  • TypedDict has fixed, known keys with specific types
  • dict[str, T] has arbitrary string keys all with the same type
  • They're structurally different from a type-safety perspective

Best Solution: Use Solution 3 with Mapping[str, Any] if you want maximum compatibility with various dict-like objects including TypedDicts.

Copy link
Member Author

Choose a reason for hiding this comment

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

Note that we can always update LSPAny in https://github.com/sublimelsp/lsp-python-types to be what we want if we think it makes sense.

Copy link
Member

@jwortmann jwortmann Jan 31, 2026

Choose a reason for hiding this comment

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

I noticed that after 5e19a9e I would get a similar type incompatibility when using session.execute_command
https://github.com/sublimelsp/LSP-Tinymist/blob/4fe344f28e9a6fcae8cb1beda23c565b00612923/plugin.py#L267

So yes, I think making LSPAny more permissive for type checkers would be a good idea. I would also go with solution 3.


Edit: Although my use case that is linked above is the other way round compared to the example from Claude; the argument is typed LSPAny and the function input is PreviewResult (TypedDict). Would need to check if that works too.

Copy link
Member Author

Choose a reason for hiding this comment

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

It's a different issue.

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