Skip to content

Commit

Permalink
Fix <MethodName>Options type checking
Browse files Browse the repository at this point in the history
The machine readable version of the LSP spec (and therefore
`lsprotocol`) provides a mapping from an LSP method's name to its
`RegistrationOptions` type, which is an extension of the method's
`Options` type used when computing a server's capabilities.

This means the `RegistrationOptions` type includes additional fields
that are not valid within the `ServerCapabilities` response.

This commit introduces a new `get_method_options_type` function that
returns the correct `Options` type for a given method, automatically
deriving the type name from the result of the existing
`get_method_registration_options_type` function when appropriate.
  • Loading branch information
alcarney committed Aug 20, 2022
1 parent 0acadbf commit 557f942
Show file tree
Hide file tree
Showing 3 changed files with 65 additions and 6 deletions.
4 changes: 2 additions & 2 deletions pygls/feature_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
ATTR_REGISTERED_NAME, ATTR_REGISTERED_TYPE, PARAM_LS)
from pygls.exceptions import (CommandAlreadyRegisteredError, FeatureAlreadyRegisteredError,
ThreadDecoratorError, ValidationError)
from pygls.lsp import get_method_registration_options_type, is_instance
from pygls.lsp import get_method_options_type, is_instance

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -169,7 +169,7 @@ def decorator(f):
self._features[feature_name] = wrapped

if options:
options_type = get_method_registration_options_type(feature_name)
options_type = get_method_options_type(feature_name)
if options_type and not is_instance(options, options_type):
raise TypeError(
(f'Options of method "{feature_name}"'
Expand Down
65 changes: 62 additions & 3 deletions pygls/lsp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,28 @@
# See the License for the specific language governing permissions and #
# limitations under the License. #
############################################################################
from typing import Any, Union
from typing import Any, Optional, Union

import attrs
from lsprotocol.types import METHOD_TO_TYPES
from lsprotocol.types import (
ALL_TYPES_MAP,
METHOD_TO_TYPES,
TEXT_DOCUMENT_SEMANTIC_TOKENS_FULL,
TEXT_DOCUMENT_SEMANTIC_TOKENS_FULL_DELTA,
TEXT_DOCUMENT_SEMANTIC_TOKENS_RANGE,
SemanticTokensLegend,
SemanticTokensRegistrationOptions
)
from typeguard import check_type

from pygls.exceptions import MethodTypeNotRegisteredError

METHOD_TO_OPTIONS = {
TEXT_DOCUMENT_SEMANTIC_TOKENS_FULL: Union[SemanticTokensLegend, SemanticTokensRegistrationOptions],
TEXT_DOCUMENT_SEMANTIC_TOKENS_FULL_DELTA: Union[SemanticTokensLegend, SemanticTokensRegistrationOptions],
TEXT_DOCUMENT_SEMANTIC_TOKENS_RANGE: Union[SemanticTokensLegend, SemanticTokensRegistrationOptions],
}

@attrs.define
class JsonRPCNotification:
"""A class that represents json rpc notification message."""
Expand Down Expand Up @@ -49,13 +63,58 @@ class JsonRPCResponseMessage:
result: Union[Any, None] = attrs.field(default=None)


def get_method_registration_options_type(method_name, lsp_methods_map=METHOD_TO_TYPES):
def get_method_registration_options_type(
method_name, lsp_methods_map=METHOD_TO_TYPES
) -> Optional[Any]:
"""The type corresponding with a method's options when dynamically registering
capability for it."""

try:
return lsp_methods_map[method_name][3]
except KeyError:
raise MethodTypeNotRegisteredError(method_name)


def get_method_options_type(
method_name, lsp_options_map=METHOD_TO_OPTIONS, lsp_methods_map=METHOD_TO_TYPES
) -> Optional[Any]:
"""Return the type corresponding with a method's ``ServerCapabilities`` fields.
In the majority of cases this simply means returning the ``<MethodName>Options``
type, which we can easily derive from the method's
``<MethodName>RegistrationOptions`` type.
However, where the options are more involved (such as semantic tokens) and
``pygls`` does some extra work to help derive the options for the user the type
has to be provided via the ``lsp_options_map``
Arguments:
method_name:
The lsp method name to retrieve the options for
lsp_options_map:
The map used to override the default options type finding behavior
lsp_methods_map:
The standard map used to look up the various method types.
"""

options_type = lsp_options_map.get(method_name, None)
if options_type is not None:
return options_type

registration_type = get_method_registration_options_type(method_name, lsp_methods_map)
if registration_type is None:
return None

type_name = registration_type.__name__.replace('Registration', '')
options_type = ALL_TYPES_MAP.get(type_name, None)

if options_type is None:
raise MethodTypeNotRegisteredError(method_name)

return options_type

def get_method_params_type(method_name, lsp_methods_map=METHOD_TO_TYPES):
try:
return lsp_methods_map[method_name][2]
Expand Down
2 changes: 1 addition & 1 deletion tests/test_feature_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ class Options:
TypeError,
match=(
f'Options of method "{methods.COMPLETION}" should be instance of type '
"<class 'pygls.lsp.types.language_features.completion.CompletionOptions'>"
"<class 'lsprotocol.types.CompletionOptions'>"
), # noqa
):

Expand Down

0 comments on commit 557f942

Please sign in to comment.