Skip to content

Commit

Permalink
pytest-lsp: Add syntax for selecting a specific version of a client
Browse files Browse the repository at this point in the history
  • Loading branch information
alcarney committed Oct 21, 2023
1 parent 9e9e163 commit 5a800cb
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 21 deletions.
2 changes: 2 additions & 0 deletions lib/pytest-lsp/changes/101.enhancement.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
It is now possible to select a specific version of a client when using the ``client_capabilities()`` function.
e.g. ``client-name@latest``, ``client-name@v2`` or ``[email protected]``. ``pytest-lsp`` will choose the latest available version of the client that satisfies the given constraint.
1 change: 1 addition & 0 deletions lib/pytest-lsp/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ classifiers = [
]
dependencies = [
"importlib-resources; python_version<\"3.9\"",
"packaging",
"pygls>=1.1.0",
"pytest",
"pytest-asyncio",
Expand Down
70 changes: 57 additions & 13 deletions lib/pytest-lsp/pytest_lsp/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,18 @@
import json
import logging
import os
import pathlib
import sys
import traceback
import typing
from typing import Dict
from typing import List
from typing import Optional
from typing import Union

from lsprotocol import types
from lsprotocol.converters import get_converter
from packaging.version import parse as parse_version
from pygls.exceptions import JsonRpcException
from pygls.exceptions import PyglsError
from pygls.lsp.client import BaseLanguageClient
Expand Down Expand Up @@ -207,32 +210,73 @@ def show_document(
def client_capabilities(client_spec: str) -> types.ClientCapabilities:
"""Find the capabilities that correspond to the given client spec.
This function supports the following syntax
``client-name`` or ``client-name@latest``
Return the capabilities of the latest version of ``client-name``
``client-name@v2``
Return the latest release of the ``v2`` of ``client-name``
``[email protected]``
Return exactly ``v2.3.1`` of ``client-name``
Parameters
----------
client_spec
A string describing the client to load the corresponding
The string describing the client to load the corresponding
capabilities for.
Raises
------
ValueError
If the requested client's capabilities could not be found
Returns
-------
ClientCapabilities
The requested client capabilities
"""

# Currently, we only have a single version of each client so let's just return the
# first one we find.
#
# TODO: Implement support for [email protected]
# TODO: Implement support for client@latest?
filename = None
candidates: Dict[str, pathlib.Path] = {}

client_spec = client_spec.replace("-", "_")
target_version = "latest"

if "@" in client_spec:
client_spec, target_version = client_spec.split("@")
if target_version.startswith("v"):
target_version = target_version[1:]

for resource in resources.files("pytest_lsp.clients").iterdir():
filename = typing.cast(pathlib.Path, resource)

# Skip the README or any other files that we don't care about.
if not resource.name.endswith(".json"):
if not filename.suffix == ".json":
continue

if resource.name.startswith(client_spec.replace("-", "_")):
filename = resource
break
name, version = filename.stem.split("_v")
if name == client_spec:
if version.startswith(target_version) or target_version == "latest":
candidates[version] = filename

if len(candidates) == 0:
raise ValueError(
f"Could not find capabilities for '{client_spec}@{target_version}'"
)

if not filename:
raise ValueError(f"Unknown client: '{client_spec}'")
# Out of the available candidates, choose the latest version
selected_version = sorted(candidates.keys(), key=parse_version, reverse=True)[0]
filename = candidates[selected_version]

converter = get_converter()
capabilities = json.loads(filename.read_text())

params = converter.structure(capabilities, types.InitializeParams)
logger.info(
"Selected %s v%s",
params.client_info.name, # type: ignore[union-attr]
params.client_info.version, # type: ignore[union-attr]
)

return params.capabilities
45 changes: 37 additions & 8 deletions lib/pytest-lsp/tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,51 @@

import pygls.uris as uri
import pytest

import pytest_lsp


@pytest.mark.parametrize(
"client_spec,client_capabilities",
"client_spec,capabilities",
[
*itertools.product(
["visual_studio_code", "visual-studio-code"],
["visual_studio_code_v1.65.2.json"],
),
("neovim", "neovim_v0.6.1.json"),
*itertools.product(
["[email protected]", "[email protected]", "[email protected]"],
["neovim_v0.6.1.json"],
),
*itertools.product(
["neovim", "neovim@latest", "neovim@v0", "[email protected]", "[email protected]"],
["neovim_v0.9.1.json"],
),
],
)
def test_client_capabilities(
pytester: pytest.Pytester, client_spec: str, client_capabilities: str
pytester: pytest.Pytester, client_spec: str, capabilities: str
):
"""Ensure that the plugin can mimic the requested client's capabilities."""
"""Ensure that the plugin can mimic the requested client's capabilities correctly.
Parameters
----------
pytester
pytest's built in pytester fixture.
client_spec
The string used to select the desired client and version
client_capabilities
The filename containing the expected client capabilities
"""

python = sys.executable
testdir = pathlib.Path(__file__).parent
server = testdir / "servers" / "capabilities.py"
root_uri = uri.from_fs_path(str(testdir))

clients_dir = pathlib.Path(pytest_lsp.__file__).parent / "clients"
with (clients_dir / client_capabilities).open() as f:
with (clients_dir / capabilities).open() as f:
# Easiest way to reformat the JSON onto a single line
expected = json.dumps(json.load(f)["capabilities"])

Expand Down Expand Up @@ -71,14 +91,23 @@ async def client(lsp_client: LanguageClient):
f"""
import json
import pytest
from lsprotocol.types import ExecuteCommandParams
from lsprotocol import types
from lsprotocol.converters import get_converter
@pytest.mark.asyncio
async def test_capabilities(client):
actual = await client.workspace_execute_command_async(
ExecuteCommandParams(command="return.client.capabilities")
types.ExecuteCommandParams(command="return.client.capabilities")
)
assert actual == json.loads('{expected}')
expected = json.loads('{expected}')
# lsprotocol is going to filter out any quirks of the client
# so we can't compare the dicts directly.
converter = get_converter()
actual_capabilities = converter.structure(actual, types.ClientCapabilities)
expected_capabilities = converter.structure(expected, types.ClientCapabilities)
assert actual_capabilities == expected_capabilities
"""
)

Expand Down

0 comments on commit 5a800cb

Please sign in to comment.