From b183eb119d6df7dc27ef22af3c2810aae0814cd9 Mon Sep 17 00:00:00 2001 From: kstrauser Date: Fri, 20 Oct 2023 09:44:07 +0200 Subject: [PATCH 1/4] Add ruff 0.1.0 capabilities. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Does the following: - Updates the pyproject.toml file to only support ruff>=0.1.0 - Add the `--unsafe-fixes` option to the LSP config Co-authored-by: Julian Hoßbach --- README.md | 14 +++++++++++--- pylsp_ruff/plugin.py | 31 ++++++++++++++++++++++++++++--- pylsp_ruff/ruff.py | 1 + pylsp_ruff/settings.py | 1 + pyproject.toml | 2 +- tests/test_code_actions.py | 29 +++++++++++++++++++++++++++++ tests/test_ruff_lint.py | 3 ++- 7 files changed, 73 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 2b393b5..a5e3cb2 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,14 @@ pip install python-lsp-ruff There also exists an [AUR package](https://aur.archlinux.org/packages/python-lsp-ruff). -# Usage +### When using ruff before version 0.1.0 +Ruff version `0.1.0` introduced API changes that are fixed in Python LSP Ruff `v1.6.0`. To continue with `ruff<0.1.0` please use `v1.5.3`, e.g. using `pip`: + +```sh +pip install "ruff<0.1.0" "python-lsp-ruff==1.5.3" +``` + +## Usage This plugin will disable `pycodestyle`, `pyflakes`, `mccabe` and `pyls_isort` by default, unless they are explicitly enabled in the client configuration. When enabled, all linting diagnostics will be provided by `ruff`. @@ -43,7 +50,7 @@ lspconfig.pylsp.setup { } ``` -# Configuration +## Configuration Configuration options can be passed to the python-language-server. If a `pyproject.toml` file is present in the project, `python-lsp-ruff` will use these configuration options. @@ -66,11 +73,12 @@ the valid configuration keys: - `pylsp.plugins.ruff.select`: List of error codes to enable. - `pylsp.plugins.ruff.extendSelect`: Same as select, but append to existing error codes. - `pylsp.plugins.ruff.format`: List of error codes to fix during formatting. The default is `["I"]`, any additional codes are appended to this list. + - `pylsp.plugins.ruff.unsafeFixes`: boolean that enables/disables fixes that are marked "unsafe" by `ruff`. `false` by default. - `pylsp.plugins.ruff.severities`: Dictionary of custom severity levels for specific codes, see [below](#custom-severities). For more information on the configuration visit [Ruff's homepage](https://beta.ruff.rs/docs/configuration/). -## Custom severities +### Custom severities By default, all diagnostics are marked as warning, except for `"E999"` and all error codes starting with `"F"`, which are displayed as errors. This default can be changed through the `pylsp.plugins.ruff.severities` option, which takes the error code as a key and any of diff --git a/pylsp_ruff/plugin.py b/pylsp_ruff/plugin.py index 2342b99..e74b166 100644 --- a/pylsp_ruff/plugin.py +++ b/pylsp_ruff/plugin.py @@ -139,6 +139,20 @@ def pylsp_lint(workspace: Workspace, document: Document) -> List[Dict]: def create_diagnostic(check: RuffCheck, settings: PluginSettings) -> Diagnostic: + """ + Create a LSP diagnostic based on the given RuffCheck object. + + Parameters + ---------- + check : RuffCheck + RuffCheck object to convert. + settings : PluginSettings + Current settings. + + Returns + ------- + Diagnostic + """ # Adapt range to LSP specification (zero-based) range = Range( start=Position( @@ -214,6 +228,8 @@ def pylsp_code_actions( code_actions = [] has_organize_imports = False + settings = load_settings(workspace=workspace, document_path=document.path) + for diagnostic in diagnostics: code_actions.append( create_disable_code_action(document=document, diagnostic=diagnostic) @@ -222,6 +238,10 @@ def pylsp_code_actions( if diagnostic.data: # Has fix fix = converter.structure(diagnostic.data, RuffFix) + # Ignore fix if marked as unsafe and unsafe_fixes are disabled + if fix.applicability != "safe" and not settings.unsafe_fixes: + continue + if diagnostic.code == "I001": code_actions.append( create_organize_imports_code_action( @@ -236,7 +256,6 @@ def pylsp_code_actions( ), ) - settings = load_settings(workspace=workspace, document_path=document.path) checks = run_ruff_check(document=document, settings=settings) checks_with_fixes = [c for c in checks if c.fix] checks_organize_imports = [c for c in checks_with_fixes if c.code == "I001"] @@ -458,7 +477,7 @@ def run_ruff( p = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE) (stdout, stderr) = p.communicate(document_source.encode()) - if stderr: + if p.returncode != 0: log.error(f"Error running ruff: {stderr.decode()}") return stdout.decode() @@ -491,8 +510,10 @@ def build_arguments( args = [] # Suppress update announcements args.append("--quiet") + # Suppress exit 1 when violations were found + args.append("--exit-zero") # Use the json formatting for easier evaluation - args.append("--format=json") + args.append("--output-format=json") if fix: args.append("--fix") else: @@ -510,6 +531,9 @@ def build_arguments( if settings.line_length: args.append(f"--line-length={settings.line_length}") + if settings.unsafe_fixes: + args.append("--unsafe-fixes") + if settings.exclude: args.append(f"--exclude={','.join(settings.exclude)}") @@ -583,6 +607,7 @@ def load_settings(workspace: Workspace, document_path: str) -> PluginSettings: return PluginSettings( enabled=plugin_settings.enabled, executable=plugin_settings.executable, + unsafe_fixes=plugin_settings.unsafe_fixes, extend_ignore=plugin_settings.extend_ignore, extend_select=plugin_settings.extend_select, format=plugin_settings.format, diff --git a/pylsp_ruff/ruff.py b/pylsp_ruff/ruff.py index 2f83f9b..efda324 100644 --- a/pylsp_ruff/ruff.py +++ b/pylsp_ruff/ruff.py @@ -19,6 +19,7 @@ class Edit: class Fix: edits: List[Edit] message: str + applicability: str @dataclass diff --git a/pylsp_ruff/settings.py b/pylsp_ruff/settings.py index 7993183..5585e83 100644 --- a/pylsp_ruff/settings.py +++ b/pylsp_ruff/settings.py @@ -9,6 +9,7 @@ class PluginSettings: enabled: bool = True executable: str = "ruff" + unsafe_fixes: bool = False config: Optional[str] = None line_length: Optional[int] = None diff --git a/pyproject.toml b/pyproject.toml index f96f12e..1d374e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ readme = "README.md" requires-python = ">=3.7" license = {text = "MIT"} dependencies = [ - "ruff>=0.0.267,<0.1.0", + "ruff>=0.1.0, <0.2.0", "python-lsp-server", "lsprotocol>=2022.0.0a1", "tomli>=1.1.0; python_version < '3.11'", diff --git a/tests/test_code_actions.py b/tests/test_code_actions.py index 7e15de9..063b536 100644 --- a/tests/test_code_actions.py +++ b/tests/test_code_actions.py @@ -115,11 +115,40 @@ def f(): pass """ ) + expected_str_safe = dedent( + """ + def f(): + a = 2 + """ + ) + workspace._config.update( + { + "plugins": { + "ruff": { + "unsafeFixes": True, + } + } + } + ) _, doc = temp_document(codeaction_str, workspace) settings = ruff_lint.load_settings(workspace, doc.path) fixed_str = ruff_lint.run_ruff_fix(doc, settings) assert fixed_str == expected_str + workspace._config.update( + { + "plugins": { + "ruff": { + "unsafeFixes": False, + } + } + } + ) + _, doc = temp_document(codeaction_str, workspace) + settings = ruff_lint.load_settings(workspace, doc.path) + fixed_str = ruff_lint.run_ruff_fix(doc, settings) + assert fixed_str == expected_str_safe + def test_format_document_default_settings(workspace): _, doc = temp_document(import_str, workspace) diff --git a/tests/test_ruff_lint.py b/tests/test_ruff_lint.py index 9f6bac0..b1439ee 100644 --- a/tests/test_ruff_lint.py +++ b/tests/test_ruff_lint.py @@ -177,7 +177,8 @@ def f(): assert call_args == [ "ruff", "--quiet", - "--format=json", + "--exit-zero", + "--output-format=json", "--no-fix", "--force-exclude", f"--stdin-filename={os.path.join(workspace.root_path, '__init__.py')}", From ea5f874a4268edcdbde3c0f8a922aa54b9e29b0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20Ho=C3=9Fbach?= Date: Fri, 20 Oct 2023 09:57:39 +0200 Subject: [PATCH 2/4] make executable optional, invoke ruff with sys --- pylsp_ruff/plugin.py | 16 +++++++++------- pylsp_ruff/settings.py | 6 +++--- tests/test_ruff_lint.py | 4 +++- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/pylsp_ruff/plugin.py b/pylsp_ruff/plugin.py index e74b166..a36babf 100644 --- a/pylsp_ruff/plugin.py +++ b/pylsp_ruff/plugin.py @@ -465,13 +465,15 @@ def run_ruff( executable = settings.executable arguments = build_arguments(document_path, settings, fix, extra_arguments) - log.debug(f"Calling {executable} with args: {arguments} on '{document_path}'") - try: - cmd = [executable] - cmd.extend(arguments) - p = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE) - except Exception: - log.debug(f"Can't execute {executable}. Trying with '{sys.executable} -m ruff'") + if executable is not None: + log.debug(f"Calling {executable} with args: {arguments} on '{document_path}'") + try: + cmd = [executable] + cmd.extend(arguments) + p = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE) + except Exception: + log.error(f"Can't execute ruff with given executable '{executable}'.") + else: cmd = [sys.executable, "-m", "ruff"] cmd.extend(arguments) p = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE) diff --git a/pylsp_ruff/settings.py b/pylsp_ruff/settings.py index 5585e83..0ccda7e 100644 --- a/pylsp_ruff/settings.py +++ b/pylsp_ruff/settings.py @@ -8,9 +8,7 @@ @dataclass class PluginSettings: enabled: bool = True - executable: str = "ruff" - unsafe_fixes: bool = False - + executable: Optional[str] = None config: Optional[str] = None line_length: Optional[int] = None @@ -25,6 +23,8 @@ class PluginSettings: format: Optional[List[str]] = None + unsafe_fixes: bool = False + severities: Optional[Dict[str, str]] = None diff --git a/tests/test_ruff_lint.py b/tests/test_ruff_lint.py index b1439ee..41026fb 100644 --- a/tests/test_ruff_lint.py +++ b/tests/test_ruff_lint.py @@ -2,6 +2,7 @@ # Copyright 2021- Python Language Server Contributors. import os +import sys import tempfile from unittest.mock import Mock, patch @@ -154,7 +155,6 @@ def f(): ) # Check that user config is ignored - assert ruff_settings.executable == "ruff" empty_keys = [ "config", "line_length", @@ -175,6 +175,8 @@ def f(): call_args = popen_mock.call_args[0][0] assert call_args == [ + str(sys.executable), + "-m", "ruff", "--quiet", "--exit-zero", From 438d8fd7568fcdb1e03414f0a6ef0dd40a8fd739 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20Ho=C3=9Fbach?= Date: Fri, 20 Oct 2023 10:10:21 +0200 Subject: [PATCH 3/4] update README with new executable default --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a5e3cb2..71f02ea 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ the valid configuration keys: - `pylsp.plugins.ruff.enabled`: boolean to enable/disable the plugin. `true` by default. - `pylsp.plugins.ruff.config`: Path to optional `pyproject.toml` file. - `pylsp.plugins.ruff.exclude`: Exclude files from being checked by `ruff`. - - `pylsp.plugins.ruff.executable`: Path to the `ruff` executable. Assumed to be in PATH by default. + - `pylsp.plugins.ruff.executable`: Path to the `ruff` executable. Uses `os.executable -m "ruff"` by default. - `pylsp.plugins.ruff.ignore`: Error codes to ignore. - `pylsp.plugins.ruff.extendIgnore`: Same as ignore, but append to existing ignores. - `pylsp.plugins.ruff.lineLength`: Set the line-length for length checks. From 3f17ceb164c90c2785f6496cfdbb0e17558a150f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20Ho=C3=9Fbach?= Date: Fri, 20 Oct 2023 10:10:32 +0200 Subject: [PATCH 4/4] bump to v1.6.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1d374e3..cfdf720 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ name = "python-lsp-ruff" authors = [ {name = "Julian Hossbach", email = "julian.hossbach@gmx.de"} ] -version = "1.5.3" +version = "1.6.0" description = "Ruff linting plugin for pylsp" readme = "README.md" requires-python = ">=3.7"