Skip to content

Commit 735b2d0

Browse files
committed
Run ruff format when lsp formatting is invoked
Adds the Subcommand enum to indicate which `ruff` subcommand should be executed by `run_ruff`. At this time, only `check` and `format` are supported. As different subcommands support different parameters, argument generation is delegated based on the specific subcommand value. The `ruff format` subcommand does not currently organize imports and there does not appear to be a way to convince it to do so. Until a unified command exists the approach taken here is to format and then make a second run of `ruff check` that _only_ performs import formatting.
1 parent be27747 commit 735b2d0

File tree

4 files changed

+186
-49
lines changed

4 files changed

+186
-49
lines changed

pylsp_ruff/plugin.py

Lines changed: 92 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import enum
12
import json
23
import logging
34
import re
@@ -45,6 +46,7 @@
4546
r"(?::\s?(?P<codes>([A-Z]+[0-9]+(?:[,\s]+)?)+))?"
4647
)
4748

49+
4850
UNNECESSITY_CODES = {
4951
"F401", # `module` imported but unused
5052
"F504", # % format unused named arguments
@@ -60,6 +62,31 @@
6062
"H": DiagnosticSeverity.Hint,
6163
}
6264

65+
ISORT_FIXES = "I"
66+
67+
68+
class Subcommand(str, enum.Enum):
69+
CHECK = "check"
70+
FORMAT = "format"
71+
72+
def __str__(self) -> str:
73+
return self.value
74+
75+
def build_args(
76+
self,
77+
document_path: str,
78+
settings: PluginSettings,
79+
fix: bool = False,
80+
extra_arguments: Optional[List[str]] = None,
81+
) -> List[str]:
82+
if self == Subcommand.CHECK:
83+
return build_check_arguments(document_path, settings, fix, extra_arguments)
84+
elif self == Subcommand.FORMAT:
85+
return build_format_arguments(document_path, settings, extra_arguments)
86+
else:
87+
logging.warn(f"subcommand without argument builder '{self}'")
88+
return []
89+
6390

6491
@hookimpl
6592
def pylsp_settings():
@@ -103,8 +130,16 @@ def pylsp_format_document(workspace: Workspace, document: Document) -> Generator
103130
settings=settings, document_path=document.path, document_source=source
104131
)
105132

133+
settings.select = [ISORT_FIXES] # clobber to just run import sorting
134+
new_text = run_ruff(
135+
settings=settings,
136+
document_path=document.path,
137+
document_source=new_text,
138+
fix=True,
139+
)
140+
106141
# Avoid applying empty text edit
107-
if new_text == source:
142+
if not new_text or new_text == source:
108143
return
109144

110145
range = Range(
@@ -395,6 +430,7 @@ def run_ruff_check(document: Document, settings: PluginSettings) -> List[RuffChe
395430
document_path=document.path,
396431
document_source=document.source,
397432
settings=settings,
433+
subcommand=Subcommand.CHECK,
398434
)
399435
try:
400436
result = json.loads(result)
@@ -418,26 +454,19 @@ def run_ruff_format(
418454
document_path: str,
419455
document_source: str,
420456
) -> str:
421-
fixable_codes = ["I"]
422-
if settings.format:
423-
fixable_codes.extend(settings.format)
424-
extra_arguments = [
425-
f"--fixable={','.join(fixable_codes)}",
426-
]
427-
result = run_ruff(
457+
return run_ruff(
428458
settings=settings,
429459
document_path=document_path,
430460
document_source=document_source,
431-
fix=True,
432-
extra_arguments=extra_arguments,
461+
subcommand=Subcommand.FORMAT,
433462
)
434-
return result
435463

436464

437465
def run_ruff(
438466
settings: PluginSettings,
439467
document_path: str,
440468
document_source: str,
469+
subcommand: Subcommand = Subcommand.CHECK,
441470
fix: bool = False,
442471
extra_arguments: Optional[List[str]] = None,
443472
) -> str:
@@ -453,6 +482,8 @@ def run_ruff(
453482
document_source : str
454483
Document source or to apply ruff on.
455484
Needed when the source differs from the file source, e.g. during formatting.
485+
subcommand: Subcommand
486+
The ruff subcommand to run. Default = Subcommand.CHECK.
456487
fix : bool
457488
Whether to run fix or no-fix.
458489
extra_arguments : List[str]
@@ -463,7 +494,8 @@ def run_ruff(
463494
String containing the result in json format.
464495
"""
465496
executable = settings.executable
466-
arguments = build_arguments(document_path, settings, fix, extra_arguments)
497+
498+
arguments = subcommand.build_args(document_path, settings, fix, extra_arguments)
467499

468500
if executable is not None:
469501
log.debug(f"Calling {executable} with args: {arguments} on '{document_path}'")
@@ -474,7 +506,7 @@ def run_ruff(
474506
except Exception:
475507
log.error(f"Can't execute ruff with given executable '{executable}'.")
476508
else:
477-
cmd = [sys.executable, "-m", "ruff"]
509+
cmd = [sys.executable, "-m", "ruff", str(subcommand)]
478510
cmd.extend(arguments)
479511
p = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)
480512
(stdout, stderr) = p.communicate(document_source.encode())
@@ -485,14 +517,14 @@ def run_ruff(
485517
return stdout.decode()
486518

487519

488-
def build_arguments(
520+
def build_check_arguments(
489521
document_path: str,
490522
settings: PluginSettings,
491523
fix: bool = False,
492524
extra_arguments: Optional[List[str]] = None,
493525
) -> List[str]:
494526
"""
495-
Build arguments for ruff.
527+
Build arguments for ruff check.
496528
497529
Parameters
498530
----------
@@ -565,6 +597,51 @@ def build_arguments(
565597
return args
566598

567599

600+
def build_format_arguments(
601+
document_path: str,
602+
settings: PluginSettings,
603+
extra_arguments: Optional[List[str]] = None,
604+
) -> List[str]:
605+
"""
606+
Build arguments for ruff format.
607+
608+
Parameters
609+
----------
610+
document : pylsp.workspace.Document
611+
Document to apply ruff on.
612+
settings : PluginSettings
613+
Settings to use for arguments to pass to ruff.
614+
extra_arguments : List[str]
615+
Extra arguments to pass to ruff.
616+
617+
Returns
618+
-------
619+
List containing the arguments.
620+
"""
621+
args = []
622+
# Suppress update announcements
623+
args.append("--quiet")
624+
625+
# Always force excludes
626+
args.append("--force-exclude")
627+
# Pass filename to ruff for per-file-ignores, catch unsaved
628+
if document_path != "":
629+
args.append(f"--stdin-filename={document_path}")
630+
631+
if settings.config:
632+
args.append(f"--config={settings.config}")
633+
634+
if settings.exclude:
635+
args.append(f"--exclude={','.join(settings.exclude)}")
636+
637+
if extra_arguments:
638+
args.extend(extra_arguments)
639+
640+
args.extend(["--", "-"])
641+
642+
return args
643+
644+
568645
def load_settings(workspace: Workspace, document_path: str) -> PluginSettings:
569646
"""
570647
Load settings from pyproject.toml file in the project path.

tests/test_code_actions.py

Lines changed: 0 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -148,37 +148,3 @@ def f():
148148
settings = ruff_lint.load_settings(workspace, doc.path)
149149
fixed_str = ruff_lint.run_ruff_fix(doc, settings)
150150
assert fixed_str == expected_str_safe
151-
152-
153-
def test_format_document_default_settings(workspace):
154-
_, doc = temp_document(import_str, workspace)
155-
settings = ruff_lint.load_settings(workspace, doc.path)
156-
formatted_str = ruff_lint.run_ruff_format(
157-
settings, document_path=doc.path, document_source=doc.source
158-
)
159-
assert formatted_str == import_str
160-
161-
162-
def test_format_document_settings(workspace):
163-
expected_str = dedent(
164-
"""
165-
import os
166-
import pathlib
167-
"""
168-
)
169-
workspace._config.update(
170-
{
171-
"plugins": {
172-
"ruff": {
173-
"select": ["I"],
174-
"format": ["I001"],
175-
}
176-
}
177-
}
178-
)
179-
_, doc = temp_document(import_str, workspace)
180-
settings = ruff_lint.load_settings(workspace, doc.path)
181-
formatted_str = ruff_lint.run_ruff_format(
182-
settings, document_path=doc.path, document_source=doc.source
183-
)
184-
assert formatted_str == expected_str

tests/test_ruff_format.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import contextlib
2+
import tempfile
3+
import textwrap as tw
4+
from typing import Any, List, Mapping, Optional
5+
from unittest.mock import Mock
6+
7+
import pytest
8+
from pylsp import uris
9+
from pylsp.config.config import Config
10+
from pylsp.workspace import Document, Workspace
11+
12+
import pylsp_ruff.plugin as plugin
13+
14+
15+
@pytest.fixture()
16+
def workspace(tmp_path):
17+
"""Return a workspace."""
18+
ws = Workspace(tmp_path.absolute().as_uri(), Mock())
19+
ws._config = Config(ws.root_uri, {}, 0, {})
20+
return ws
21+
22+
23+
def temp_document(doc_text, workspace):
24+
with tempfile.NamedTemporaryFile(
25+
mode="w", dir=workspace.root_path, delete=False
26+
) as temp_file:
27+
name = temp_file.name
28+
temp_file.write(doc_text)
29+
doc = Document(uris.from_fs_path(name), workspace)
30+
return name, doc
31+
32+
33+
def run_plugin_format(workspace: Workspace, doc: Document) -> str:
34+
class TestResult:
35+
result: Optional[List[Mapping[str, Any]]]
36+
37+
def __init__(self):
38+
self.result = None
39+
40+
def get_result(self):
41+
return self.result
42+
43+
def force_result(self, r):
44+
self.result = r
45+
46+
generator = plugin.pylsp_format_document(workspace, doc)
47+
result = TestResult()
48+
with contextlib.suppress(StopIteration):
49+
generator.send(None)
50+
generator.send(result)
51+
52+
if result.result:
53+
return result.result[0]["newText"]
54+
return pytest.fail()
55+
56+
57+
def test_ruff_format(workspace):
58+
# imports incorrectly ordered,
59+
# body of foo has a line that's too long
60+
# def bar() line missing whitespace above
61+
txt = tw.dedent(
62+
"""
63+
from thirdparty import x
64+
import io
65+
import asyncio
66+
67+
def foo():
68+
print("this is a looooooooooooooooooooooooooooooooooooooooooooong line that should exceed the usual line-length limit which is normally eighty-eight columns")
69+
def bar():
70+
pass
71+
""" # noqa: E501
72+
).lstrip()
73+
want = tw.dedent(
74+
"""
75+
import asyncio
76+
import io
77+
78+
from thirdparty import x
79+
80+
81+
def foo():
82+
print(
83+
"this is a looooooooooooooooooooooooooooooooooooooooooooong line that should exceed the usual line-length limit which is normally eighty-eight columns"
84+
)
85+
86+
87+
def bar():
88+
pass
89+
""" # noqa: E501
90+
).lstrip()
91+
_, doc = temp_document(txt, workspace)
92+
got = run_plugin_format(workspace, doc)
93+
assert want == got, f"want:\n{want}\n\ngot:\n{got}"

tests/test_ruff_lint.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ def f():
178178
str(sys.executable),
179179
"-m",
180180
"ruff",
181+
"check",
181182
"--quiet",
182183
"--exit-zero",
183184
"--output-format=json",

0 commit comments

Comments
 (0)