8000 refactor: use dataclass for plugin settings · kstrauser/python-lsp-ruff@a1b21f2 · GitHub
[go: up one dir, main page]

Skip to content

Commit a1b21f2

Browse files
betaboonjhossbach
authored andcommitted
refactor: use dataclass for plugin settings
1 parent 81ce593 commit a1b21f2

File tree

3 files changed

+129
-87
lines changed

3 files changed

+129
-87
lines changed

pylsp_ruff/plugin.py

Lines changed: 59 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
from subprocess import PIPE, Popen
77
from typing import Dict, List
88

9-
from lsprotocol.converters import get_converter
109
from lsprotocol.types import (
1110
CodeAction,
1211
CodeActionContext,
@@ -26,6 +25,7 @@
2625

2726
from pylsp_ruff.ruff import Check as RuffCheck
2827
from pylsp_ruff.ruff import Fix as 8000 RuffFix
28+
from pylsp_ruff.settings import PluginSettings, get_converter
2929

3030
log = logging.getLogger(__name__)
3131
converter = get_converter()
@@ -52,26 +52,16 @@
5252
def pylsp_settings():
5353
log.debug("Initializing pylsp_ruff")
5454
# this plugin disables flake8, mccabe, and pycodestyle by default
55-
return {
55+
settings = {
5656
"plugins": {
57-
"ruff": {
58-
"enabled": True,
59-
"config": None,
60-
"exclude": None,
61-
"executable": "ruff",
62-
"ignore": None,
63-
"extendIgnore": None,
64-
"lineLength": None,
65-
"perFileIgnores": None,
66-
"select": None,
67-
"extendSelect": None,
68-
},
57+
"ruff": PluginSettings(),
6958
"pyflakes": {"enabled": False},
7059
"flake8": {"enabled": False},
7160
"mccabe": {"enabled": False},
7261
"pycodestyle": {"enabled": False},
7362
}
7463
}
64+
return converter.unstructure(settings)
7565

7666

7767
@hookimpl
@@ -342,9 +332,9 @@ def run_ruff(workspace: Workspace, document: Document, fix: bool = False) -> str
342332
-------
343333
String containing the result in json format.
344334
"""
345-
config = load_config(workspace, document)
346-
executable = config.pop("executable")
347-
arguments = build_arguments(document, config, fix)
335+
settings = load_settings(workspace, document)
336+
executable = settings.executable
337+
arguments = build_arguments(document, settings, fix)
348338

349339
log.debug(f"Calling {executable} with args: {arguments} on '{document.path}'")
350340
try:
@@ -364,61 +354,74 @@ def run_ruff(workspace: Workspace, document: Document, fix: bool = False) -> str
364354
return stdout.decode()
365355

366356

367-
def build_arguments(document: Document, options: Dict, fix: bool = False) -> List[str]:
357+
def build_arguments(
358+
document: Document,
359+
settings: PluginSettings,
360+
fix: bool = False,
361+
) -> List[str]:
368362
"""
369363
Build arguments for ruff.
370364
371365
Parameters
372366
----------
373367
document : pylsp.workspace.Document
374368
Document to apply ruff on.
375-
options : Dict
376-
Dict of arguments to pass to ruff.
369+
settings : PluginSettings
370+
Settings to use for arguments to pass to ruff.
377371
378372
Returns
379373
-------
380374
List containing the arguments.
381375
"""
376+
args = []
382377
# Suppress update announcements
383-
args = ["--quiet"]
378+
args.append("--quiet")
384379
# Use the json formatting for easier evaluation
385-
args.extend(["--format=json"])
380+
args.append("--format=json")
386381
if fix:
387-
args.extend(["--fix"])
382+
args.append("--fix")
388383
else:
389384
# Do not attempt to fix -> returns file instead of diagnostics
390-
args.extend(["--no-fix"])
385+
args.append("--no-fix")
391386
# Always force excludes
392-
args.extend(["--force-exclude"])
387+
args.append("--force-exclude")
393388
# Pass filename to ruff for per-file-ignores, catch unsaved
394389
if document.path != "":
395-
args.extend(["--stdin-filename", document.path])
396-
397-
# Convert per-file-ignores dict to right format
398-
per_file_ignores = options.pop("per-file-ignores")
399-
400-
if per_file_ignores:
401-
for path, errors in per_file_ignores.items():
402-
errors = (",").join(errors)
403-
if PurePath(document.path).match(path):
404-
args.extend([f"--ignore={errors}"])
405-
406-
for arg_name, arg_val in options.items():
407-
if arg_val is None:
408-
continue
409-
arg = None
410-
if isinstance(arg_val, list):
411-
arg = "--{}={}".format(arg_name, ",".join(arg_val))
412-
else:
413-
arg = "--{}={}".format(arg_name, arg_val)
414-
args.append(arg)
390+
args.append(f"--stdin-filename={document.path}")
391+
392+
if settings.config:
393+
args.append(f"--config={settings.config}")
394+
395+
if settings.line_length:
396+
args.append(f"--line-length={settings.line_length}")
397+
398+
if settings.exclude:
399+
args.append(f"--exclude={','.join(settings.exclude)}")
400+
401+
if settings.select:
402+
args.append(f"--select={','.join(settings.select)}")
403+
404+
if settings.extend_select:
405+
args.append(f"--extend-select={','.join(settings.extend_select)}")
406+
407+
if settings.ignore:
408+
args.append(f"--ignore={','.join(settings.ignore)}")
409+
410+
if settings.extend_ignore:
411+
args.append(f"--extend-ignore={','.join(settings.extend_ignore)}")
412+
413+
if settings.per_file_ignores:
414+
for path, errors in settings.per_file_ignores.items():
415+
if not PurePath(document.path).match(path):
416+
continue
417+
args.append(f"--ignore={','.join(errors)}")
415418

416419
args.extend(["--", "-"])
417420

418421
return args
419422

420423

421-
def load_config(workspace: Workspace, document: Document) -> Dict:
424+
def load_settings(workspace: Workspace, document: Document) -> PluginSettings:
422425
"""
423426
Load settings from pyproject.toml file in the project path.
424427
@@ -431,10 +434,11 @@ def load_config(workspace: Workspace, document: Document) -> Dict:
431434
432435
Returns
433436
-------
434-
Dictionary containing the settings to use when calling ruff.
437+
PluginSettings read via lsp.
435438
"""
436439
config = workspace._config
437-
_settings = config.plugin_settings("ruff", document_path=document.path)
440+
_plugin_settings = config.plugin_settings("ruff", document_path=document.path)
441+
plugin_settings = converter.structure(_plugin_settings, PluginSettings)
438442

439443
pyproject_file = find_parents(
440444
workspace.root_path, document.path, ["pyproject.toml"]
@@ -446,32 +450,12 @@ def load_config(workspace: Workspace, document: Document) -> Dict:
446450
f"Found pyproject file: {str(pyproject_file[0])}, "
447451
+ "skipping pylsp config."
448452
)
449-
450453
# Leave config to pyproject.toml
451-
settings = {
452-
"config": None,
453-
"exclude": None,
454-
"executable": _settings.get("executable", "ruff"),
455-
"ignore": None,
456-
"extend-ignore": _settings.get("extendIgnore", None),
457-
"line-length": None,
458-
"per-file-ignores": None,
459-
"select": None,
460-
"extend-select": _settings.get("extendSelect", None),
461-
}
462-
463-
else:
464-
# Default values are given by ruff
465-
settings = {
466-
"config": _settings.get("config", None),
467-
"exclude": _settings.get("exclude", None),
468-
"executable": _settings.get("executable", "ruff"),
469-
"ignore": _settings.get("ignore", None),
470-
"extend-ignore": _settings.get("extendIgnore", None),
471-
"line-length": _settings.get("lineLength", None),
472-
"per-file-ignores": _settings.get("perFileIgnores", None),
473-
"select": _settings.get("select", None),
474-
"extend-select": _settings.get("extendSelect", None),
475-
}
454+
return PluginSettings(
455+
enabled=plugin_settings.executable,
456+
executable=plugin_settings.executable,
457+
extend_ignore=plugin_settings.extend_ignore,
458+
extend_select=plugin_settings.extend_select,
459+
)
476460

477-
return settings
461+
return plugin_settings

pylsp_ruff/settings.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
from dataclasses import dataclass, fields
2+
from typing import Dict, List, Optional
3+
4+
import lsprotocol.converters
5+
from cattrs.gen import make_dict_structure_fn, make_dict_unstructure_fn, override
6+
7+
8+
@dataclass
9+
class PluginSettings:
10+
enabled: bool = True
11+
executable: str = "ruff"
12+
13+
config: Optional[str] = None
14+
line_length: Optional[int] = None
15+
16+
exclude: Optional[List[str]] = None
17+
18+
select: Optional[List[str]] = None
19+
extend_select: Optional[List[str]] = None
20+
21+
ignore: Optional[List[str]] = None
22+
extend_ignore: Optional[List[str]] = None
23+
per_file_ignores: Optional[Dict[str, List[str]]] = None
24+
25+
26+
def to_camel_case(snake_str: str) -> str:
27+
components = snake_str.split("_")
28+
return components[0] + "".join(x.title() for x in components[1:])
29+
30+
31+
def to_camel_case_unstructure(converter, klass):
32+
return make_dict_unstructure_fn(
33+
klass,
34+
converter,
35+
**{a.name: override(rename=to_camel_case(a.name)) for a in fields(klass)},
36+
)
37+
38+
39+
def to_camel_case_structure(converter, klass):
40+
return make_dict_structure_fn(
41+
klass,
42+
converter,
43+
**{a.name: override(rename=to_camel_case(a.name)) for a in fields(klass)},
44+
)
45+
46+
47+
def get_converter():
48+
converter = lsprotocol.converters.get_converter()
49+
unstructure_hook = to_camel_case_unstructure(converter, PluginSettings)
50+
structure_hook = to_camel_case_structure(converter, PluginSettings)
51+
converter.register_unstructure_hook(PluginSettings, unstructure_hook)
52+
converter.register_structure_hook(PluginSettings, structure_hook)
53+
return converter

tests/test_ruff_lint.py

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ def test_ruff_executable_param(workspace):
112112
assert ruff_executable in call_args
113113

114114

115-
def get_ruff_cfg_settings(workspace, doc, config_str):
115+
def get_ruff_settings(workspace, doc, config_str):
116116
"""Write a ``pyproject.toml``, load it in the workspace, and return the ruff
117117
settings.
118118
@@ -124,10 +124,10 @@ def get_ruff_cfg_settings(workspace, doc, config_str):
124124
) as f:
125125
f.write(config_str)
126126

127-
return ruff_lint.load_config(workspace, doc)
127+
return ruff_lint.load_settings(workspace, doc)
128128

129129

130-
def test_ruff_config(workspace):
130+
def test_ruff_settings(workspace):
131131
config_str = r"""[tool.ruff]
132132
ignore = ["F841"]
133133
exclude = [
@@ -149,16 +149,22 @@ def f():
149149
doc_uri = uris.from_fs_path(os.path.join(workspace.root_path, "__init__.py"))
150150
workspace.put_document(doc_uri, doc_str)
151151

152-
ruff_settings = get_ruff_cfg_settings(
152+
ruff_settings = get_ruff_settings(
153153
workspace, workspace.get_document(doc_uri), config_str
154154
)
155155

156156
# Check that user config is ignored
157-
for key, value in ruff_settings.items():
158-
if key == "executable":
159-
assert value == "ruff"
160-
continue
161-
assert value is None
157+
assert ruff_settings.executable == "ruff"
158+
empty_keys = [
159+
"config",
160+
"line_length",
161+
"exclude",
162+
"select",
163+
"ignore",
164+
"per_file_ignores",
165+
]
166+
for k in empty_keys:
167+
assert getattr(ruff_settings, k) is None
162168

163169
with patch("pylsp_ruff.plugin.Popen") as popen_mock:
164170
mock_instance = popen_mock.return_value
@@ -174,8 +180,7 @@ def f():
174180
"--format=json",
175181
"--no-fix",
176182
"--force-exclude",
177-
"--stdin-filename",
178-
os.path.join(workspace.root_path, "__init__.py"),
183+
f"--stdin-filename={os.path.join(workspace.root_path, '__init__.py')}",
179184
"--",
180185
"-",
181186
]
@@ -205,7 +210,7 @@ def f():
205210
doc_uri = uris.from_fs_path(os.path.join(workspace.root_path, "blah/__init__.py"))
206211
workspace.put_document(doc_uri, doc_str)
207212

208-
ruff_settings = get_ruff_cfg_settings(
213+
ruff_settings = get_ruff_settings(
209214
workspace, workspace.get_document(doc_uri), config_str
210215
)
211216

0 commit comments

Comments
 (0)
0