Skip to content

Commit

Permalink
Require minimum ruff version (#254)
Browse files Browse the repository at this point in the history
**Summary** ruff-lsp will crash on start rather than behave weirdly when
there is a too low `ruff` version

Fixes astral-sh/ruff#7408

I've bump the minimum to 0.0.291 due to the formatter changes, but also
allowed 0.1 already since it should be compatible (CC @zanieb).

When the error occurs in vs-code, we show a little pop-up with a full
traceback in the log:


![image](https://github.com/astral-sh/ruff-lsp/assets/6826232/e4c72e08-0b2f-4b8c-9985-b51785b4a7a2)

There's duplication because we both show the error and don't catch the
exception.

**Test Plan** I've tested this by using 0.0.290, which works with the
linter by errors with the formatter
  • Loading branch information
konstin authored Oct 2, 2023
1 parent b12a277 commit 54d07df
Show file tree
Hide file tree
Showing 6 changed files with 100 additions and 38 deletions.
4 changes: 2 additions & 2 deletions justfile
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
default: fmt check

lock:
pip-compile --resolver=backtracking --upgrade -o requirements.txt pyproject.toml
pip-compile --resolver=backtracking --upgrade --extra dev -o requirements-dev.txt pyproject.toml
pip-compile --resolver=backtracking --generate-hashes --upgrade -o requirements.txt pyproject.toml
pip-compile --resolver=backtracking --generate-hashes --upgrade --extra dev -o requirements-dev.txt pyproject.toml

install:
pip install --no-deps -r requirements.txt
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ classifiers = [
]
urls = { repository = "https://github.com/astral-sh/ruff-lsp" }
dependencies = [
"packaging>=23.1",
"pygls>=1.0.1",
"lsprotocol>=2023.0.0a1",
"ruff>=0.0.274",
Expand Down
21 changes: 11 additions & 10 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# This file is autogenerated by pip-compile with Python 3.7
# by the following command:
#
# pip-compile --extra=dev --generate-hashes --output-file=./requirements-dev.txt --resolver=backtracking ./pyproject.toml
# pip-compile --extra=dev --generate-hashes --output-file=requirements-dev.txt --resolver=backtracking pyproject.toml
#
attrs==23.1.0 \
--hash=sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04 \
Expand Down Expand Up @@ -47,7 +47,7 @@ lsprotocol==2023.0.0b1 \
--hash=sha256:f7a2d4655cbd5639f373ddd1789807450c543341fa0a32b064ad30dbb9f510d4
# via
# pygls
# ruff-lsp (./pyproject.toml)
# ruff-lsp (pyproject.toml)
mypy==1.4.1 \
--hash=sha256:01fd2e9f85622d981fd9063bfaef1aed6e336eaacca00892cd2d82801ab7c042 \
--hash=sha256:0dde1d180cd84f0624c5dcaaa89c89775550a675aff96b5848de78fb11adabcd \
Expand Down Expand Up @@ -75,7 +75,7 @@ mypy==1.4.1 \
--hash=sha256:ca637024ca67ab24a7fd6f65d280572c3794665eaf5edcc7e90a866544076878 \
--hash=sha256:e02d700ec8d9b1859790c0475df4e4092c7bf3272a4fd2c9f33d87fac4427b8f \
--hash=sha256:e5952d2d18b79f7dc25e62e014fe5a23eb1a3d2bc66318df8988a01b1a037c5b
# via ruff-lsp (./pyproject.toml)
# via ruff-lsp (pyproject.toml)
mypy-extensions==1.0.0 \
--hash=sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d \
--hash=sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782
Expand All @@ -86,18 +86,19 @@ packaging==23.1 \
# via
# build
# pytest
# ruff-lsp (pyproject.toml)
pip-tools==6.14.0 \
--hash=sha256:06366be0e08d86b416407333e998b4d305d5bd925151b08942ed149380ba3e47 \
--hash=sha256:c5ad042cd27c0b343b10db1db7f77a7d087beafbec59ae6df1bba4d3368dfe8c
# via ruff-lsp (./pyproject.toml)
# via ruff-lsp (pyproject.toml)
pluggy==1.2.0 \
--hash=sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849 \
--hash=sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3
# via pytest
pygls==1.0.2 \
--hash=sha256:6d278d29fa6559b0f7a448263c85cb64ec6e9369548b02f1a7944060848b21f9 \
--hash=sha256:888ed63d1f650b4fc64d603d73d37545386ec533c0caac921aed80f80ea946a4
# via ruff-lsp (./pyproject.toml)
# via ruff-lsp (pyproject.toml)
pyproject-hooks==1.0.0 \
--hash=sha256:283c11acd6b928d2f6a7c73fa0d01cb2bdc5f07c57a2eeb6e83d5e56b97976f8 \
--hash=sha256:f271b298b97f5955d53fb12b72c1fb1948c22c1a6b70b315c54cedaca0264ef5
Expand All @@ -107,15 +108,15 @@ pytest==7.4.2 \
--hash=sha256:a766259cfab564a2ad52cb1aae1b881a75c3eb7e34ca3779697c23ed47c47069
# via
# pytest-asyncio
# ruff-lsp (./pyproject.toml)
# ruff-lsp (pyproject.toml)
pytest-asyncio==0.21.1 \
--hash=sha256:40a7eae6dded22c7b604986855ea48400ab15b069ae38116e8c01238e9eeb64d \
--hash=sha256:8666c1c8ac02631d7c51ba282e0c69a8a452b211ffedf2599099845da5c5c37b
# via ruff-lsp (./pyproject.toml)
# via ruff-lsp (pyproject.toml)
python-lsp-jsonrpc==1.0.0 \
--hash=sha256:079b143be64b0a378bdb21dff5e28a8c1393fe7e8a654ef068322d754e545fc7 \
--hash=sha256:7bec170733db628d3506ea3a5288ff76aa33c70215ed223abdb0d95e957660bd
# via ruff-lsp (./pyproject.toml)
# via ruff-lsp (pyproject.toml)
ruff==0.0.291 \
--hash=sha256:13f0d88e5f367b2dc8c7d90a8afdcfff9dd7d174e324fd3ed8e0b5cb5dc9b7f6 \
--hash=sha256:1d5f0616ae4cdc7a938b493b6a1a71c8a47d0300c0d65f6e41c281c2f7490ad3 \
Expand All @@ -134,7 +135,7 @@ ruff==0.0.291 \
--hash=sha256:c61109661dde9db73469d14a82b42a88c7164f731e6a3b0042e71394c1c7ceed \
--hash=sha256:d867384a4615b7f30b223a849b52104214442b5ba79b473d7edd18da3cde22d6 \
--hash=sha256:fd17220611047de247b635596e3174f3d7f2becf63bd56301fc758778df9b629
# via ruff-lsp (./pyproject.toml)
# via ruff-lsp (pyproject.toml)
tomli==2.0.1 \
--hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \
--hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f
Expand Down Expand Up @@ -199,7 +200,7 @@ typing-extensions==4.7.1 \
# importlib-metadata
# mypy
# pytest-asyncio
# ruff-lsp (./pyproject.toml)
# ruff-lsp (pyproject.toml)
# typeguard
ujson==5.7.0 \
--hash=sha256:00343501dbaa5172e78ef0e37f9ebd08040110e11c12420ff7c1f9f0332d939e \
Expand Down
14 changes: 9 additions & 5 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# This file is autogenerated by pip-compile with Python 3.7
# by the following command:
#
# pip-compile --generate-hashes --output-file=./requirements.txt --resolver=backtracking ./pyproject.toml
# pip-compile --generate-hashes --output-file=requirements.txt --resolver=backtracking pyproject.toml
#
attrs==23.1.0 \
--hash=sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04 \
Expand All @@ -29,11 +29,15 @@ lsprotocol==2023.0.0b1 \
--hash=sha256:f7a2d4655cbd5639f373ddd1789807450c543341fa0a32b064ad30dbb9f510d4
# via
# pygls
# ruff-lsp (./pyproject.toml)
# ruff-lsp (pyproject.toml)
packaging==23.1 \
--hash=sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61 \
--hash=sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f
# via ruff-lsp (pyproject.toml)
pygls==1.0.2 \
--hash=sha256:6d278d29fa6559b0f7a448263c85cb64ec6e9369548b02f1a7944060848b21f9 \
--hash=sha256:888ed63d1f650b4fc64d603d73d37545386ec533c0caac921aed80f80ea946a4
# via ruff-lsp (./pyproject.toml)
# via ruff-lsp (pyproject.toml)
ruff==0.0.291 \
--hash=sha256:13f0d88e5f367b2dc8c7d90a8afdcfff9dd7d174e324fd3ed8e0b5cb5dc9b7f6 \
--hash=sha256:1d5f0616ae4cdc7a938b493b6a1a71c8a47d0300c0d65f6e41c281c2f7490ad3 \
Expand All @@ -52,7 +56,7 @@ ruff==0.0.291 \
--hash=sha256:c61109661dde9db73469d14a82b42a88c7164f731e6a3b0042e71394c1c7ceed \
--hash=sha256:d867384a4615b7f30b223a849b52104214442b5ba79b473d7edd18da3cde22d6 \
--hash=sha256:fd17220611047de247b635596e3174f3d7f2becf63bd56301fc758778df9b629
# via ruff-lsp (./pyproject.toml)
# via ruff-lsp (pyproject.toml)
typeguard==3.0.2 \
--hash=sha256:bbe993854385284ab42fd5bd3bee6f6556577ce8b50696d6cb956d704f286c8e \
--hash=sha256:fee5297fdb28f8e9efcb8142b5ee219e02375509cd77ea9d270b5af826358d5a
Expand All @@ -63,7 +67,7 @@ typing-extensions==4.7.1 \
# via
# cattrs
# importlib-metadata
# ruff-lsp (./pyproject.toml)
# ruff-lsp (pyproject.toml)
# typeguard
zipp==3.15.0 \
--hash=sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b \
Expand Down
90 changes: 71 additions & 19 deletions ruff_lsp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import sys
import sysconfig
from pathlib import Path
from typing import Sequence, cast
from typing import NamedTuple, Sequence, cast

from lsprotocol import validators
from lsprotocol.types import (
Expand Down Expand Up @@ -52,6 +52,7 @@
TextEdit,
WorkspaceEdit,
)
from packaging.specifiers import SpecifierSet, Version
from pygls import server, uris, workspace
from typing_extensions import TypedDict

Expand Down Expand Up @@ -81,7 +82,15 @@
GLOBAL_SETTINGS: UserSettings = {}
WORKSPACE_SETTINGS: dict[str, WorkspaceSettings] = {}
INTERPRETER_PATHS: dict[str, str] = {}
EXECUTABLE_VERSIONS: dict[str, str] = {}


class VersionModified(NamedTuple):
version: Version
"""Last modified of the executable"""
modified: float


EXECUTABLE_VERSIONS: dict[str, VersionModified] = {}
CLIENT_CAPABILITIES: dict[str, bool] = {
CODE_ACTION_RESOLVE: True,
}
Expand All @@ -96,6 +105,11 @@
TOOL_MODULE = "ruff.exe" if sys.platform == "win32" else "ruff"
TOOL_DISPLAY = "Ruff"

# Require at least Ruff v0.0.291 for formatting, but allow older versions for linting.
VERSION_REQUIREMENT_FORMATTER = SpecifierSet(">=0.0.291,<0.2.0")
VERSION_REQUIREMENT_LINTER = SpecifierSet(">=0.0.189,<0.2.0")
VERSION_REQUIREMENT_ALL_SELECTOR = SpecifierSet(">=0.0.198,<0.2.0")

# Arguments provided to every Ruff invocation.
CHECK_ARGS = [
"--force-exclude",
Expand Down Expand Up @@ -346,7 +360,7 @@ async def hover(params: HoverParams) -> Hover | None:
if start <= params.position.character < end:
code = match.group()
result = await _run_subcommand_on_document(
document, args=["--explain", code]
document, VERSION_REQUIREMENT_LINTER, args=["--explain", code]
)
if result.stdout:
return Hover(
Expand Down Expand Up @@ -1002,7 +1016,35 @@ def _get_settings_by_document(document: workspace.Document | None) -> WorkspaceS
###


def _executable_path(settings: WorkspaceSettings) -> str:
class Executable(NamedTuple):
path: str
"""The path to the executable."""

version: Version
"""The version of the executable."""


def _find_ruff_binary(
settings: WorkspaceSettings, version_requirement: SpecifierSet
) -> Executable:
"""Returns the executable along with its version.
If the executable doesn't meet the version requirement, raises a RuntimeError and
displays an error message.
"""
path = _find_ruff_binary_path(settings)

version = _executable_version(path)
if not version_requirement.contains(version, prereleases=True):
message = f"Ruff {version_requirement} required, but found {version} at {path}"
show_error(message)
raise RuntimeError(message)
log_to_output(f"Found ruff {version} at {path}")

return Executable(path, version)


def _find_ruff_binary_path(settings: WorkspaceSettings) -> str:
"""Returns the path to the executable."""
bundle = get_bundle()

Expand Down Expand Up @@ -1057,13 +1099,18 @@ def _executable_path(settings: WorkspaceSettings) -> str:
return path


def _executable_version(executable: str) -> str:
def _executable_version(executable: str) -> Version:
"""Returns the version of the executable."""
if executable not in EXECUTABLE_VERSIONS:
# If the user change the file (e.g. `pip install -U ruff`), invalidate the cache
modified = Path(executable).stat().st_mtime
if (
executable not in EXECUTABLE_VERSIONS
or EXECUTABLE_VERSIONS[executable].modified != modified
):
version = utils.version(executable)
log_to_output(f"Inferred version {version} for: {executable}")
EXECUTABLE_VERSIONS[executable] = version
return EXECUTABLE_VERSIONS[executable]
EXECUTABLE_VERSIONS[executable] = VersionModified(version, modified)
return EXECUTABLE_VERSIONS[executable].version


async def _run_check_on_document(
Expand All @@ -1083,7 +1130,7 @@ async def _run_check_on_document(

settings = _get_settings_by_document(document)

executable = _executable_path(settings)
executable = _find_ruff_binary(settings, VERSION_REQUIREMENT_LINTER)
argv: list[str] = CHECK_ARGS + list(extra_args)

for arg in settings["args"]:
Expand All @@ -1095,15 +1142,17 @@ async def _run_check_on_document(
# If we're trying to run a single rule, add it to the command line, and disable
# all other rules (if the Ruff version is sufficiently recent).
if only:
if _executable_version(executable) >= "0.0.198":
if VERSION_REQUIREMENT_ALL_SELECTOR.contains(
executable.version, prereleases=True
):
argv += ["--extend-ignore", "ALL"]
argv += ["--extend-select", only]

# Provide the document filename.
argv += ["--stdin-filename", document.path]

return await run_path(
executable,
executable.path,
argv,
cwd=settings["cwd"],
source=document.source,
Expand All @@ -1121,7 +1170,7 @@ async def _run_format_on_document(document: workspace.Document) -> RunResult | N
return None

settings = _get_settings_by_document(document)
executable = _executable_path(settings)
executable = _find_ruff_binary(settings, VERSION_REQUIREMENT_FORMATTER)
argv: list[str] = [
"format",
"--force-exclude",
Expand All @@ -1131,23 +1180,26 @@ async def _run_format_on_document(document: workspace.Document) -> RunResult | N
]

return await run_path(
executable,
executable.path,
argv,
cwd=settings["cwd"],
source=document.source,
)


async def _run_subcommand_on_document(
document: workspace.Document, *, args: Sequence[str]
document: workspace.Document,
version_requirement: SpecifierSet,
*,
args: Sequence[str],
) -> RunResult:
"""Runs the tool subcommand on the given document."""
settings = _get_settings_by_document(document)

executable = _executable_path(settings)
executable = _find_ruff_binary(settings, version_requirement)
argv: list[str] = list(args)
return await run_path(
executable,
executable.path,
argv,
cwd=settings["cwd"],
source=document.source,
Expand All @@ -1163,10 +1215,10 @@ def log_to_output(message: str) -> None:
LSP_SERVER.show_message_log(message, MessageType.Log)


def log_error(message: str) -> None:
def show_error(message: str) -> None:
"""Show a pop-up with an error. Only use for critical errors."""
LSP_SERVER.show_message_log(message, MessageType.Error)
if os.getenv("LS_SHOW_NOTIFICATION", "off") in ["onError", "onWarning", "always"]:
LSP_SERVER.show_message(message, MessageType.Error)
LSP_SERVER.show_message(message, MessageType.Error)


def log_warning(message: str) -> None:
Expand Down
8 changes: 6 additions & 2 deletions ruff_lsp/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
import sys
from typing import Any

from packaging.version import Version


def as_list(content: Any | list[Any] | tuple[Any, ...]) -> list[Any]:
"""Ensures we always get a list"""
Expand Down Expand Up @@ -57,9 +59,11 @@ def scripts(interpreter: str) -> str:
)


def version(executable: str) -> str:
def version(executable: str) -> Version:
"""Returns the version of the executable at the given path."""
return subprocess.check_output([executable, "--version"]).decode().strip()
output = subprocess.check_output([executable, "--version"]).decode().strip()
version = output.replace("ruff ", "") # no removeprefix in 3.7 :/
return Version(version)


class RunResult:
Expand Down

0 comments on commit 54d07df

Please sign in to comment.