-
Notifications
You must be signed in to change notification settings - Fork 188
refactor(types): Type parameters and return value of Request #2741
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
base: main
Are you sure you want to change the base?
Conversation
plugin/core/protocol.py
Outdated
| P = TypeVar('P', bound=None | bool | int | Uint | float | str | Mapping[str, Any] | Iterable[Any]) | ||
| R = TypeVar('R') |
There was a problem hiding this comment.
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?
| 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)) |
There was a problem hiding this comment.
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]): |
There was a problem hiding this comment.
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.
| 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] |
There was a problem hiding this comment.
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.
plugin/core/protocol.py
Outdated
| UINT_MAX = INT_MAX | ||
|
|
||
| R = TypeVar('R') | ||
| LspPayload: TypeAlias = Union[None, bool, int, Uint, float, str, Mapping[str, Any], Iterable[Any]] |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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]]
There was a problem hiding this comment.
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)) # ✓ WorksSolution 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) # ✓ WorksSolution 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) # ✓ WorksSolution 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:
TypedDicthas fixed, known keys with specific typesdict[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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
Type params and return value of
Requestobject. TypingRequestensures that all thesend_requestvariants infer param/return types and enforce those for the handlers/chained promise. So more safety.Also type
NotificationandResponsebut those don't benefit much from typing since there is no response to handle.