Skip to content

Commit

Permalink
Experimental ruff server now uses local ruff binaries when availa…
Browse files Browse the repository at this point in the history
…ble (#443)

## Summary

When `experimentalServer` is enabled, the Ruff binary path will be
determined in the following order:

1. **`ruff.path`** - if this is set, we take the first path in the list
and use that as the executable path.
2. **`sysconfig.get_path("scripts") / RUFF_EXE`** - `RUFF_EXE` is
`"ruff.exe"` on windows and `"ruff"` otherwise.
3. **`shutil.which("ruff")`**
4. The bundled binary path, **`./bundled/libs/bin/ruff`**, is used if
none of the prior options are available.

Before running the binary, we also check to make sure that it's running
a compatible version. At the moment, any version after or including
`0.3.3` is supported (except for `0.3.4`, since that release had a major
bug in the server that broke editor integration). Since `0.3.3` is a
rather bare-bones implementation, this minimum supported version will
increase as the extension stabilizes.

## Test Plan

1. Run `ruff --version` in your shell. If it's not `ruff 0.3.5` or
later, install/upgrade `ruff` and run `ruff --version` again to confirm
that the ruff executable in your `PATH` is now `>=0.3.5`.
2. Begin debugging the extension. You should see the following log
somewhere in the output: `Configuration file watcher successfully
registered`. This should be a confirmation that you're running `ruff
>=0.3.5` instead of the embedded `ruff 0.3.3`.
3. Add an entry to `ruff.path` in the extension settings, and pass in a
ruff binary not in your `PATH`. A good way to check that you are in fact
using this unique binary is by passing in the path to a local debug
build after adding an additional log statement in a function like
`Server::new`. If this log statement appears in the output, you know
that you're using the custom executable provided in `ruff.path`.
4. Now, uninstall the ruff executable that was in your `PATH`. Confirm
that no executable exists in your path by running `ruff --version` in
your shell - you should get your shell's variant of the `command not
found: ruff` error.
5. At this point, the extension should still be using the custom
executable you added to `ruff.path`. Now, remove that path so that
`ruff.path` is an empty list. The extension should now be using the
bundled `v0.3.3` binary (a quick way to check this is making sure no
`Configuration file watcher successfully registered` message appears,
though you could also confirm this by seeing if the diagnostics show up
highlighted in red instead of yellow).
  • Loading branch information
snowsignal authored Apr 11, 2024
1 parent 74c65cb commit a64505d
Show file tree
Hide file tree
Showing 7 changed files with 168 additions and 55 deletions.
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# Ruff extension for Visual Studio Code

[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
[![image](https://img.shields.io/pypi/v/ruff/0.3.3.svg)](https://pypi.python.org/pypi/ruff)
[![image](https://img.shields.io/pypi/l/ruff/0.3.3.svg)](https://pypi.python.org/pypi/ruff)
[![image](https://img.shields.io/pypi/pyversions/ruff/0.3.3.svg)](https://pypi.python.org/pypi/ruff)
[![image](https://img.shields.io/pypi/v/ruff/0.3.6.svg)](https://pypi.python.org/pypi/ruff)
[![image](https://img.shields.io/pypi/l/ruff/0.3.6.svg)](https://pypi.python.org/pypi/ruff)
[![image](https://img.shields.io/pypi/pyversions/ruff/0.3.6.svg)](https://pypi.python.org/pypi/ruff)
[![Actions status](https://github.com/astral-sh/ruff-vscode/workflows/CI/badge.svg)](https://github.com/astral-sh/ruff-vscode/actions)

A Visual Studio Code extension for [Ruff](https://github.com/astral-sh/ruff), an extremely fast
Expand All @@ -12,7 +12,7 @@ Python linter and code formatter, written in Rust. Available on the [Visual Stud
Ruff can be used to replace Flake8 (plus dozens of plugins), Black, isort, pyupgrade, and more,
all while executing tens or hundreds of times faster than any individual tool.

The extension ships with `ruff==0.3.3`.
The extension ships with `ruff==0.3.6`.

(Interested in using [Ruff](https://github.com/astral-sh/ruff) with another editor? Check out
[`ruff-lsp`](https://github.com/astral-sh/ruff-lsp).)
Expand Down Expand Up @@ -294,7 +294,7 @@ At the moment, the experimental server has the following known limitations:
- Commands like `Fix all` and `Organize Imports` have not yet been implemented. (Quick Fixes should still work, though)
- Hierarchial configuration for individual files is not yet supported. At the moment, the language server uses the `ruff.toml` / `pyproject.toml` at the workspace root to configure the formatter and linter.
- Jupyter Notebook files are not supported yet.
- Using local Ruff binaries is not yet supported. At the moment, the extension will always use the bundled Ruff binary. (`v0.3.3`)
- Using local Ruff binaries is not yet supported. At the moment, the extension will always use the bundled Ruff binary. (`v0.3.6`)

## Commands

Expand Down
92 changes: 92 additions & 0 deletions bundled/tool/ruff_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import os
import shutil
import site
import subprocess
import sys
import sysconfig
from pathlib import Path

RUFF_EXE = "ruff.exe" if sys.platform == "win32" else "ruff"

BUNDLE_DIR = Path(__file__).parent.parent


def update_sys_path(path_to_add: str) -> None:
"""Add given path to `sys.path`."""
if os.path.isdir(path_to_add):
# The `site` module adds the directory at the end, if not yet present; we want
# it to be at the beginning, so that it takes precedence over any other
# installed versions.
sys.path.insert(0, path_to_add)

# Allow development versions of libraries to be imported.
site.addsitedir(path_to_add)


# This is separate from the 'main' entrypoint because we need
# to update the system path _before_ importing `pacakging`
if __name__ == "__main__":
# Ensure that we can import bundled libraries like `packaging`
update_sys_path(os.fspath(BUNDLE_DIR / "libs"))


from packaging.specifiers import SpecifierSet
from packaging.version import Version

# This is subject to change in the future
RUFF_VERSION_REQUIREMENT = SpecifierSet(">=0.3.5")


def executable_version(executable: str) -> Version:
"""Return the version of the executable at the given path."""
output = subprocess.check_output([executable, "--version"]).decode().strip()
version = output.replace("ruff ", "")
return Version(version)


def check_compatibility(
executable: str,
requirement: SpecifierSet,
) -> None:
"""Check the executable for compatibility against various version specifiers."""
version = executable_version(executable)
if not requirement.contains(version, prereleases=True):
message = f"Ruff {requirement} required, but found {version} at {executable}"
raise RuntimeError(message)


def find_ruff_bin(fallback: Path) -> Path:
"""Return the ruff binary path."""
path = Path(sysconfig.get_path("scripts")) / RUFF_EXE
if path.is_file():
return path

if sys.version_info >= (3, 10):
user_scheme = sysconfig.get_preferred_scheme("user")
elif os.name == "nt":
user_scheme = "nt_user"
elif sys.platform == "darwin" and sys._framework:
user_scheme = "osx_framework_user"
else:
user_scheme = "posix_user"

path = Path(sysconfig.get_path("scripts", scheme=user_scheme)) / RUFF_EXE
if path.is_file():
return path

path = shutil.which("ruff")
if path:
return path

return fallback


if __name__ == "__main__":
ruff = os.fsdecode(
find_ruff_bin(
Path(BUNDLE_DIR / "libs" / "bin" / RUFF_EXE),
),
)
check_compatibility(ruff, RUFF_VERSION_REQUIREMENT)
completed_process = subprocess.run([ruff, *sys.argv[1:]], check=False)
sys.exit(completed_process.returncode)
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ authors = [{ name = "Charlie Marsh", email = "[email protected]" }]
maintainers = [{ name = "Charlie Marsh", email = "[email protected]" }]
requires-python = ">=3.7"
license = "MIT"
dependencies = ["packaging>=23.1", "ruff-lsp==0.0.53", "ruff==0.3.3"]
dependencies = ["packaging>=23.1", "ruff-lsp==0.0.53", "ruff==0.3.6"]

[project.optional-dependencies]
dev = ["mypy==1.2.0", "python-lsp-jsonrpc==1.0.0"]
Expand Down
36 changes: 18 additions & 18 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -74,24 +74,24 @@ python-lsp-jsonrpc==1.0.0 \
--hash=sha256:079b143be64b0a378bdb21dff5e28a8c1393fe7e8a654ef068322d754e545fc7 \
--hash=sha256:7bec170733db628d3506ea3a5288ff76aa33c70215ed223abdb0d95e957660bd
# via ruff-vscode (./pyproject.toml)
ruff==0.3.3 \
--hash=sha256:0171aab5fecdc54383993389710a3d1227f2da124d76a2784a7098e818f92d61 \
--hash=sha256:0da458989ce0159555ef224d5b7c24d3d2e4bf4c300b85467b08c3261c6bc6a8 \
--hash=sha256:1eca7ff7a47043cf6ce5c7f45f603b09121a7cc047447744b029d1b719278eb5 \
--hash=sha256:2700a804d5336bcffe063fd789ca2c7b02b552d2e323a336700abb8ae9e6a3f8 \
--hash=sha256:352e95ead6964974b234e16ba8a66dad102ec7bf8ac064a23f95371d8b198aab \
--hash=sha256:38671be06f57a2f8aba957d9f701ea889aa5736be806f18c0cd03d6ff0cbca8d \
--hash=sha256:45817af234605525cdf6317005923bf532514e1ea3d9270acf61ca2440691376 \
--hash=sha256:5a6cbf216b69c7090f0fe4669501a27326c34e119068c1494f35aaf4cc683778 \
--hash=sha256:79bca3a03a759cc773fca69e0bdeac8abd1c13c31b798d5bb3c9da4a03144a9f \
--hash=sha256:8d6ab88c81c4040a817aa432484e838aaddf8bfd7ca70e4e615482757acb64f8 \
--hash=sha256:973a0e388b7bc2e9148c7f9be8b8c6ae7471b9be37e1cc732f8f44a6f6d7720d \
--hash=sha256:b24c19e8598916d9c6f5a5437671f55ee93c212a2c4c569605dc3842b6820386 \
--hash=sha256:be90bcae57c24d9f9d023b12d627e958eb55f595428bafcb7fec0791ad25ddfc \
--hash=sha256:cfa60d23269d6e2031129b053fdb4e5a7b0637fc6c9c0586737b962b2f834493 \
--hash=sha256:e7d3f6762217c1da954de24b4a1a70515630d29f71e268ec5000afe81377642d \
--hash=sha256:f2831ec6a580a97f1ea82ea1eda0401c3cdf512cf2045fa3c85e8ef109e87de0 \
--hash=sha256:fd66469f1a18fdb9d32e22b79f486223052ddf057dc56dea0caaf1a47bdfaf4e
ruff==0.3.6 \
--hash=sha256:26071fb530038602b984e3bbe1443ef82a38450c4dcb1344a9caf67234ff9756 \
--hash=sha256:28ccf3fb6d1162a73cd286c63a5e4d885f46a1f99f0b392924bc95ccbd18ea8f \
--hash=sha256:2b0c4c70578ef1871a9ac5c85ed7a8c33470e976c73ba9211a111d2771b5f787 \
--hash=sha256:4056480f5cf38ad278667c31b0ef334c29acdfcea617cb89c4ccbc7d96f1637f \
--hash=sha256:647f1fb5128a3e24ce68878b8050bb55044c45bb3f3ae4710d4da9ca96ede5cb \
--hash=sha256:732ef99984275534f9466fbc01121523caf72aa8c2bdeb36fd2edf2bc294a992 \
--hash=sha256:7c8a2a0e0cab077a07465259ffe3b3c090e747ca8097c5dc4c36ca0fdaaac90d \
--hash=sha256:878ef1a55ce931f3ca23b690b159cd0659f495a4c231a847b00ca55e4c688baf \
--hash=sha256:93699d61116807edc5ca1cdf9d2d22cf8d93335d59e3ff0ca7aee62c1818a736 \
--hash=sha256:b11e09439d9df6cc12d9f622065834654417c40216d271f639512d80e80e3e53 \
--hash=sha256:b2e79f8e1b6bd5411d7ddad3f2abff3f9d371beda29daef86400d416dedb7e02 \
--hash=sha256:c466a52c522e6a08df0af018f550902f154f5649ad09e7f0d43da766e7399ebc \
--hash=sha256:cf48ec2c4bfae7837dc325c431a2932dc23a1485e71c59591c1df471ba234e0e \
--hash=sha256:e3da499ded004d0b956ab04248b2ae17e54a67ffc81353514ac583af5959a255 \
--hash=sha256:ecb87788284af96725643eae9ab3ac746d8cc09aad140268523b019f7ac3cd98 \
--hash=sha256:f1aa621beed533f46e9c7d6fe00e7f6e4570155b61d8f020387b72ace2b42e04 \
--hash=sha256:fc4006cbc6c11fefc25f122d2eb4731d7a3d815dc74d67c54991cc3f99c90177
# via
# ruff-lsp
# ruff-vscode (./pyproject.toml)
Expand Down
36 changes: 18 additions & 18 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -38,24 +38,24 @@ pygls==1.2.1 \
--hash=sha256:04f9b9c115b622dcc346fb390289066565343d60245a424eca77cb429b911ed8 \
--hash=sha256:7dcfcf12b6f15beb606afa46de2ed348b65a279c340ef2242a9a35c22eeafe94
# via ruff-lsp
ruff==0.3.3 \
--hash=sha256:0171aab5fecdc54383993389710a3d1227f2da124d76a2784a7098e818f92d61 \
--hash=sha256:0da458989ce0159555ef224d5b7c24d3d2e4bf4c300b85467b08c3261c6bc6a8 \
--hash=sha256:1eca7ff7a47043cf6ce5c7f45f603b09121a7cc047447744b029d1b719278eb5 \
--hash=sha256:2700a804d5336bcffe063fd789ca2c7b02b552d2e323a336700abb8ae9e6a3f8 \
--hash=sha256:352e95ead6964974b234e16ba8a66dad102ec7bf8ac064a23f95371d8b198aab \
--hash=sha256:38671be06f57a2f8aba957d9f701ea889aa5736be806f18c0cd03d6ff0cbca8d \
--hash=sha256:45817af234605525cdf6317005923bf532514e1ea3d9270acf61ca2440691376 \
--hash=sha256:5a6cbf216b69c7090f0fe4669501a27326c34e119068c1494f35aaf4cc683778 \
--hash=sha256:79bca3a03a759cc773fca69e0bdeac8abd1c13c31b798d5bb3c9da4a03144a9f \
--hash=sha256:8d6ab88c81c4040a817aa432484e838aaddf8bfd7ca70e4e615482757acb64f8 \
--hash=sha256:973a0e388b7bc2e9148c7f9be8b8c6ae7471b9be37e1cc732f8f44a6f6d7720d \
--hash=sha256:b24c19e8598916d9c6f5a5437671f55ee93c212a2c4c569605dc3842b6820386 \
--hash=sha256:be90bcae57c24d9f9d023b12d627e958eb55f595428bafcb7fec0791ad25ddfc \
--hash=sha256:cfa60d23269d6e2031129b053fdb4e5a7b0637fc6c9c0586737b962b2f834493 \
--hash=sha256:e7d3f6762217c1da954de24b4a1a70515630d29f71e268ec5000afe81377642d \
--hash=sha256:f2831ec6a580a97f1ea82ea1eda0401c3cdf512cf2045fa3c85e8ef109e87de0 \
--hash=sha256:fd66469f1a18fdb9d32e22b79f486223052ddf057dc56dea0caaf1a47bdfaf4e
ruff==0.3.6 \
--hash=sha256:26071fb530038602b984e3bbe1443ef82a38450c4dcb1344a9caf67234ff9756 \
--hash=sha256:28ccf3fb6d1162a73cd286c63a5e4d885f46a1f99f0b392924bc95ccbd18ea8f \
--hash=sha256:2b0c4c70578ef1871a9ac5c85ed7a8c33470e976c73ba9211a111d2771b5f787 \
--hash=sha256:4056480f5cf38ad278667c31b0ef334c29acdfcea617cb89c4ccbc7d96f1637f \
--hash=sha256:647f1fb5128a3e24ce68878b8050bb55044c45bb3f3ae4710d4da9ca96ede5cb \
--hash=sha256:732ef99984275534f9466fbc01121523caf72aa8c2bdeb36fd2edf2bc294a992 \
--hash=sha256:7c8a2a0e0cab077a07465259ffe3b3c090e747ca8097c5dc4c36ca0fdaaac90d \
--hash=sha256:878ef1a55ce931f3ca23b690b159cd0659f495a4c231a847b00ca55e4c688baf \
--hash=sha256:93699d61116807edc5ca1cdf9d2d22cf8d93335d59e3ff0ca7aee62c1818a736 \
--hash=sha256:b11e09439d9df6cc12d9f622065834654417c40216d271f639512d80e80e3e53 \
--hash=sha256:b2e79f8e1b6bd5411d7ddad3f2abff3f9d371beda29daef86400d416dedb7e02 \
--hash=sha256:c466a52c522e6a08df0af018f550902f154f5649ad09e7f0d43da766e7399ebc \
--hash=sha256:cf48ec2c4bfae7837dc325c431a2932dc23a1485e71c59591c1df471ba234e0e \
--hash=sha256:e3da499ded004d0b956ab04248b2ae17e54a67ffc81353514ac583af5959a255 \
--hash=sha256:ecb87788284af96725643eae9ab3ac746d8cc09aad140268523b019f7ac3cd98 \
--hash=sha256:f1aa621beed533f46e9c7d6fe00e7f6e4570155b61d8f020387b72ace2b42e04 \
--hash=sha256:fc4006cbc6c11fefc25f122d2eb4731d7a3d815dc74d67c54991cc3f99c90177
# via
# ruff-lsp
# ruff-vscode (./pyproject.toml)
Expand Down
6 changes: 5 additions & 1 deletion src/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ export const DEBUG_SERVER_SCRIPT_PATH = path.join(
"tool",
`_debug_server.py`,
);
export const RUFF_BIN_PATH = path.join(BUNDLED_PYTHON_SCRIPTS_DIR, "libs", "bin", "ruff");
export const EXPERIMENTAL_SERVER_SCRIPT_PATH = path.join(
BUNDLED_PYTHON_SCRIPTS_DIR,
"tool",
"ruff_server.py",
);
export const RUFF_SERVER_CMD = "server";
export const RUFF_SERVER_REQUIRED_ARGS = ["--preview"];
41 changes: 29 additions & 12 deletions src/common/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ import {
import {
BUNDLED_PYTHON_SCRIPTS_DIR,
DEBUG_SERVER_SCRIPT_PATH,
RUFF_BIN_PATH,
RUFF_SERVER_REQUIRED_ARGS,
RUFF_SERVER_CMD,
SERVER_SCRIPT_PATH,
EXPERIMENTAL_SERVER_SCRIPT_PATH,
} from "./constants";
import { traceError, traceInfo, traceVerbose } from "./log/logging";
import { getDebuggerPath } from "./python";
Expand All @@ -39,17 +39,34 @@ async function createExperimentalServer(
outputChannel: LogOutputChannel,
initializationOptions: IInitOptions,
): Promise<LanguageClient> {
const command = RUFF_BIN_PATH;
const cwd = settings.cwd;
const args = [RUFF_SERVER_CMD, ...RUFF_SERVER_REQUIRED_ARGS];

const serverOptions: ServerOptions = {
command,
args,
options: { cwd, env: process.env },
};

traceInfo(`Server run command: ${[command, ...args].join(" ")}`);
let serverOptions: ServerOptions;
// If the user provided a binary path, we'll try to call that path directly.
if (settings.path[0]) {
const command = settings.path[0];
const cwd = settings.cwd;
const args = [RUFF_SERVER_CMD, ...RUFF_SERVER_REQUIRED_ARGS];
serverOptions = {
command,
args,
options: { cwd, env: process.env },
};

traceInfo(`Server run command: ${[command, ...args].join(" ")}`);
} else {
// Otherwise, we'll call a Python script that tries to locate
// a binary, falling back to the bundled version if no local executable is found.
const command = settings.interpreter[0];
const cwd = settings.cwd;
const args = [EXPERIMENTAL_SERVER_SCRIPT_PATH, RUFF_SERVER_CMD, ...RUFF_SERVER_REQUIRED_ARGS];

serverOptions = {
command,
args,
options: { cwd, env: process.env },
};

traceInfo(`Server run command: ${[command, ...args].join(" ")}`);
}

const clientOptions = {
// Register the server for python documents
Expand Down

0 comments on commit a64505d

Please sign in to comment.