From ce637aecf46df5a40fee1e5301b0618ff58d2ba5 Mon Sep 17 00:00:00 2001 From: Matt McShane Date: Fri, 10 Nov 2023 09:59:55 -0500 Subject: [PATCH 01/12] Remove python v3.7 testing from CI (#58) * Remove python 3.7 testing and add 3.11 and 3.12 * Update action versions --- .github/workflows/python.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index d42c02d..06a37bb 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -10,16 +10,16 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.7, 3.8, 3.9, "3.10"] + python-version: [3.8, 3.9, "3.10", 3.11, 3.12] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: python -m pip install -e .[dev] - name: pre-commit checks - uses: pre-commit/action@v2.0.2 + uses: pre-commit/action@v3.0.0 - name: Tests run: pytest -v tests/ From 9ba6e5117afec1913e2b39278554fcef5749d595 Mon Sep 17 00:00:00 2001 From: Julian Hossbach Date: Sat, 25 Nov 2023 12:57:57 +0100 Subject: [PATCH 02/12] Tighten cattrs dep (#63) --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index cfdf720..6fb0695 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,7 @@ license = {text = "MIT"} dependencies = [ "ruff>=0.1.0, <0.2.0", "python-lsp-server", + "cattrs!=23.2.1", "lsprotocol>=2022.0.0a1", "tomli>=1.1.0; python_version < '3.11'", ] From eff74e17eeb63104eceecdf11119c99d48dca1e6 Mon Sep 17 00:00:00 2001 From: Julian Hossbach Date: Sat, 25 Nov 2023 13:14:51 +0100 Subject: [PATCH 03/12] Update 'unsafe' code actions (#55) (#62) - Update unsafe code actions to contain `(unsafe)` at the end of the message - Prevent the `Fix All` code action from applying unsafe codeactions - Update the README to mention this behaviour --- README.md | 6 ++++++ pylsp_ruff/plugin.py | 10 +++++++--- tests/test_code_actions.py | 14 ++++++++------ 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 71f02ea..ec7a5bc 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,12 @@ lspconfig.pylsp.setup { } ``` +## Code actions + +`python-lsp-ruff` supports code actions as given by possible fixes by `ruff`. `python-lsp-ruff` also supports [unsafe fixes](https://docs.astral.sh/ruff/linter/#fix-safety). +Fixes considered unsafe by `ruff` are marked `(unsafe)` in the code action. +The `Fix all` code action *only* consideres safe fixes. + ## Configuration Configuration options can be passed to the python-language-server. If a `pyproject.toml` diff --git a/pylsp_ruff/plugin.py b/pylsp_ruff/plugin.py index a36babf..16a9c8f 100644 --- a/pylsp_ruff/plugin.py +++ b/pylsp_ruff/plugin.py @@ -238,9 +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 fix.applicability == "unsafe": + if not settings.unsafe_fixes: + continue + fix.message += " (unsafe)" if diagnostic.code == "I001": code_actions.append( @@ -359,6 +360,9 @@ def create_fix_all_code_action( title = "Ruff: Fix All" kind = CodeActionKind.SourceFixAll + # No unsafe fixes for 'Fix all', see https://github.com/python-lsp/python-lsp-ruff/issues/55 + settings.unsafe_fixes = False + new_text = run_ruff_fix(document=document, settings=settings) range = Range( start=Position(line=0, character=0), diff --git a/tests/test_code_actions.py b/tests/test_code_actions.py index 063b536..acfb160 100644 --- a/tests/test_code_actions.py +++ b/tests/test_code_actions.py @@ -45,7 +45,7 @@ def f(): codeactions = [ "Ruff (F401): Remove unused import: `os`", "Ruff (F401): Disable for this line", - "Ruff (F841): Remove assignment to unused variable `a`", + "Ruff (F841): Remove assignment to unused variable `a` (unsafe)", "Ruff (F841): Disable for this line", "Ruff: Fix All", ] @@ -70,7 +70,9 @@ def temp_document(doc_text, workspace): def test_ruff_code_actions(workspace): _, doc = temp_document(codeaction_str, workspace) - workspace._config.update({"plugins": {"ruff": {"select": ["F"]}}}) + workspace._config.update( + {"plugins": {"ruff": {"select": ["F"], "unsafeFixes": True}}} + ) diags = ruff_lint.pylsp_lint(workspace, doc) range_ = cattrs.unstructure( Range(start=Position(line=0, character=0), end=Position(line=0, character=0)) @@ -79,8 +81,8 @@ def test_ruff_code_actions(workspace): workspace._config, workspace, doc, range=range_, context={"diagnostics": diags} ) actions = converter.structure(actions, List[CodeAction]) - for action in actions: - assert action.title in codeactions + action_titles = list(map(lambda action: action.title, actions)) + assert sorted(codeactions) == sorted(action_titles) def test_import_action(workspace): @@ -104,8 +106,8 @@ def test_import_action(workspace): workspace._config, workspace, doc, range=range_, context={"diagnostics": diags} ) actions = converter.structure(actions, List[CodeAction]) - for action in actions: - assert action.title in codeactions_import + action_titles = list(map(lambda action: action.title, actions)) + assert sorted(codeactions_import) == sorted(action_titles) def test_fix_all(workspace): From 9ff905cbd2f3b2670e65df3d18ee940948223bb9 Mon Sep 17 00:00:00 2001 From: Matt McShane Date: Sat, 25 Nov 2023 07:43:56 -0500 Subject: [PATCH 04/12] Run `ruff format` when lsp formatting is invoked (#57) * 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. * Preserve compatibility with `format` settings Codes listed in this setting should be included in fixes performed as part of a formatting pass. * Make import sorting opt-in --- README.md | 2 +- pylsp_ruff/plugin.py | 108 ++++++++++++++++++++++++++++++----- tests/test_code_actions.py | 34 ----------- tests/test_ruff_format.py | 114 +++++++++++++++++++++++++++++++++++++ tests/test_ruff_lint.py | 1 + 5 files changed, 209 insertions(+), 50 deletions(-) create mode 100644 tests/test_ruff_format.py diff --git a/README.md b/README.md index ec7a5bc..80cd40a 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ the valid configuration keys: - `pylsp.plugins.ruff.perFileIgnores`: File-specific error codes to be ignored. - `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.format`: List of error codes to fix during formatting. Empty by default, use `["I"]` here to get import sorting as part of formatting. - `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). diff --git a/pylsp_ruff/plugin.py b/pylsp_ruff/plugin.py index 16a9c8f..8698a6d 100644 --- a/pylsp_ruff/plugin.py +++ b/pylsp_ruff/plugin.py @@ -1,3 +1,4 @@ +import enum import json import logging import re @@ -45,6 +46,7 @@ r"(?::\s?(?P([A-Z]+[0-9]+(?:[,\s]+)?)+))?" ) + UNNECESSITY_CODES = { "F401", # `module` imported but unused "F504", # % format unused named arguments @@ -61,6 +63,29 @@ } +class Subcommand(str, enum.Enum): + CHECK = "check" + FORMAT = "format" + + def __str__(self) -> str: + return self.value + + def build_args( + self, + document_path: str, + settings: PluginSettings, + fix: bool = False, + extra_arguments: Optional[List[str]] = None, + ) -> List[str]: + if self == Subcommand.CHECK: + return build_check_arguments(document_path, settings, fix, extra_arguments) + elif self == Subcommand.FORMAT: + return build_format_arguments(document_path, settings, extra_arguments) + else: + logging.warn(f"subcommand without argument builder '{self}'") + return [] + + @hookimpl def pylsp_settings(): log.debug("Initializing pylsp_ruff") @@ -103,8 +128,19 @@ def pylsp_format_document(workspace: Workspace, document: Document) -> Generator settings=settings, document_path=document.path, document_source=source ) + if settings.format: + # A second pass through the document with `ruff check` and only the rules + # enabled via the format config property. This allows for things like + # specifying `format = ["I"]` to get import sorting as part of formatting. + new_text = run_ruff( + settings=PluginSettings(ignore=["ALL"], select=settings.format), + document_path=document.path, + document_source=new_text, + fix=True, + ) + # Avoid applying empty text edit - if new_text == source: + if not new_text or new_text == source: return range = Range( @@ -399,6 +435,7 @@ def run_ruff_check(document: Document, settings: PluginSettings) -> List[RuffChe document_path=document.path, document_source=document.source, settings=settings, + subcommand=Subcommand.CHECK, ) try: result = json.loads(result) @@ -422,26 +459,19 @@ def run_ruff_format( document_path: str, document_source: str, ) -> str: - fixable_codes = ["I"] - if settings.format: - fixable_codes.extend(settings.format) - extra_arguments = [ - f"--fixable={','.join(fixable_codes)}", - ] - result = run_ruff( + return run_ruff( settings=settings, document_path=document_path, document_source=document_source, - fix=True, - extra_arguments=extra_arguments, + subcommand=Subcommand.FORMAT, ) - return result def run_ruff( settings: PluginSettings, document_path: str, document_source: str, + subcommand: Subcommand = Subcommand.CHECK, fix: bool = False, extra_arguments: Optional[List[str]] = None, ) -> str: @@ -457,6 +487,8 @@ def run_ruff( document_source : str Document source or to apply ruff on. Needed when the source differs from the file source, e.g. during formatting. + subcommand: Subcommand + The ruff subcommand to run. Default = Subcommand.CHECK. fix : bool Whether to run fix or no-fix. extra_arguments : List[str] @@ -467,7 +499,8 @@ def run_ruff( String containing the result in json format. """ executable = settings.executable - arguments = build_arguments(document_path, settings, fix, extra_arguments) + + arguments = subcommand.build_args(document_path, settings, fix, extra_arguments) if executable is not None: log.debug(f"Calling {executable} with args: {arguments} on '{document_path}'") @@ -478,7 +511,7 @@ def run_ruff( except Exception: log.error(f"Can't execute ruff with given executable '{executable}'.") else: - cmd = [sys.executable, "-m", "ruff"] + cmd = [sys.executable, "-m", "ruff", str(subcommand)] cmd.extend(arguments) p = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE) (stdout, stderr) = p.communicate(document_source.encode()) @@ -489,14 +522,14 @@ def run_ruff( return stdout.decode() -def build_arguments( +def build_check_arguments( document_path: str, settings: PluginSettings, fix: bool = False, extra_arguments: Optional[List[str]] = None, ) -> List[str]: """ - Build arguments for ruff. + Build arguments for ruff check. Parameters ---------- @@ -569,6 +602,51 @@ def build_arguments( return args +def build_format_arguments( + document_path: str, + settings: PluginSettings, + extra_arguments: Optional[List[str]] = None, +) -> List[str]: + """ + Build arguments for ruff format. + + Parameters + ---------- + document : pylsp.workspace.Document + Document to apply ruff on. + settings : PluginSettings + Settings to use for arguments to pass to ruff. + extra_arguments : List[str] + Extra arguments to pass to ruff. + + Returns + ------- + List containing the arguments. + """ + args = [] + # Suppress update announcements + args.append("--quiet") + + # Always force excludes + args.append("--force-exclude") + # Pass filename to ruff for per-file-ignores, catch unsaved + if document_path != "": + args.append(f"--stdin-filename={document_path}") + + if settings.config: + args.append(f"--config={settings.config}") + + if settings.exclude: + args.append(f"--exclude={','.join(settings.exclude)}") + + if extra_arguments: + args.extend(extra_arguments) + + args.extend(["--", "-"]) + + return args + + def load_settings(workspace: Workspace, document_path: str) -> PluginSettings: """ Load settings from pyproject.toml file in the project path. diff --git a/tests/test_code_actions.py b/tests/test_code_actions.py index acfb160..f304e7a 100644 --- a/tests/test_code_actions.py +++ b/tests/test_code_actions.py @@ -150,37 +150,3 @@ def f(): 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) - settings = ruff_lint.load_settings(workspace, doc.path) - formatted_str = ruff_lint.run_ruff_format( - settings, document_path=doc.path, document_source=doc.source - ) - assert formatted_str == import_str - - -def test_format_document_settings(workspace): - expected_str = dedent( - """ - import os - import pathlib - """ - ) - workspace._config.update( - { - "plugins": { - "ruff": { - "select": ["I"], - "format": ["I001"], - } - } - } - ) - _, doc = temp_document(import_str, workspace) - settings = ruff_lint.load_settings(workspace, doc.path) - formatted_str = ruff_lint.run_ruff_format( - settings, document_path=doc.path, document_source=doc.source - ) - assert formatted_str == expected_str diff --git a/tests/test_ruff_format.py b/tests/test_ruff_format.py new file mode 100644 index 0000000..817d7be --- /dev/null +++ b/tests/test_ruff_format.py @@ -0,0 +1,114 @@ +import contextlib +import tempfile +import textwrap as tw +from typing import Any, List, Mapping, Optional +from unittest.mock import Mock + +import pytest +from pylsp import uris +from pylsp.config.config import Config +from pylsp.workspace import Document, Workspace + +import pylsp_ruff.plugin as plugin + +_UNSORTED_IMPORTS = tw.dedent( + """ + from thirdparty import x + import io + import asyncio + """ +).strip() + +_SORTED_IMPORTS = tw.dedent( + """ + import asyncio + import io + + from thirdparty import x + """ +).strip() + +_UNFORMATTED_CODE = tw.dedent( + """ + def foo(): pass + def bar(): pass + """ +).strip() + +_FORMATTED_CODE = tw.dedent( + """ + def foo(): + pass + + + def bar(): + pass + """ +).strip() + + +@pytest.fixture() +def workspace(tmp_path): + """Return a workspace.""" + ws = Workspace(tmp_path.absolute().as_uri(), Mock()) + ws._config = Config(ws.root_uri, {}, 0, {}) + return ws + + +def temp_document(doc_text, workspace): + with tempfile.NamedTemporaryFile( + mode="w", dir=workspace.root_path, delete=False + ) as temp_file: + name = temp_file.name + temp_file.write(doc_text) + doc = Document(uris.from_fs_path(name), workspace) + return name, doc + + +def run_plugin_format(workspace: Workspace, doc: Document) -> str: + class TestResult: + result: Optional[List[Mapping[str, Any]]] + + def __init__(self): + self.result = None + + def get_result(self): + return self.result + + def force_result(self, r): + self.result = r + + generator = plugin.pylsp_format_document(workspace, doc) + result = TestResult() + with contextlib.suppress(StopIteration): + generator.send(None) + generator.send(result) + + if result.result: + return result.result[0]["newText"] + return pytest.fail() + + +def test_ruff_format_only(workspace): + txt = f"{_UNSORTED_IMPORTS}\n{_UNFORMATTED_CODE}" + want = f"{_UNSORTED_IMPORTS}\n\n\n{_FORMATTED_CODE}\n" + _, doc = temp_document(txt, workspace) + got = run_plugin_format(workspace, doc) + assert want == got + + +def test_ruff_format_and_sort_imports(workspace): + txt = f"{_UNSORTED_IMPORTS}\n{_UNFORMATTED_CODE}" + want = f"{_SORTED_IMPORTS}\n\n\n{_FORMATTED_CODE}\n" + _, doc = temp_document(txt, workspace) + workspace._config.update( + { + "plugins": { + "ruff": { + "format": ["I001"], + } + } + } + ) + got = run_plugin_format(workspace, doc) + assert want == got diff --git a/tests/test_ruff_lint.py b/tests/test_ruff_lint.py index 41026fb..ac60ccb 100644 --- a/tests/test_ruff_lint.py +++ b/tests/test_ruff_lint.py @@ -178,6 +178,7 @@ def f(): str(sys.executable), "-m", "ruff", + "check", "--quiet", "--exit-zero", "--output-format=json", From 42095eff4780f4cbbab0219d7acaa15910f10163 Mon Sep 17 00:00:00 2001 From: Felix Williams <87987318+felix-cw@users.noreply.github.com> Date: Sat, 25 Nov 2023 14:00:27 +0000 Subject: [PATCH 05/12] Add support for ruff `extension` option to enable `jupyterlab-lsp` (#59) --- pylsp_ruff/plugin.py | 1 + pyproject.toml | 2 +- tests/test_ruff_lint.py | 21 +++++++++++++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/pylsp_ruff/plugin.py b/pylsp_ruff/plugin.py index 8698a6d..a70298e 100644 --- a/pylsp_ruff/plugin.py +++ b/pylsp_ruff/plugin.py @@ -553,6 +553,7 @@ def build_check_arguments( args.append("--exit-zero") # Use the json formatting for easier evaluation args.append("--output-format=json") + args.append("--extension=ipynb:python") if fix: args.append("--fix") else: diff --git a/pyproject.toml b/pyproject.toml index 6fb0695..4098c34 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ readme = "README.md" requires-python = ">=3.7" license = {text = "MIT"} dependencies = [ - "ruff>=0.1.0, <0.2.0", + "ruff>=0.1.5, <0.2.0", "python-lsp-server", "cattrs!=23.2.1", "lsprotocol>=2022.0.0a1", diff --git a/tests/test_ruff_lint.py b/tests/test_ruff_lint.py index ac60ccb..c88c559 100644 --- a/tests/test_ruff_lint.py +++ b/tests/test_ruff_lint.py @@ -182,6 +182,7 @@ def f(): "--quiet", "--exit-zero", "--output-format=json", + "--extension=ipynb:python", "--no-fix", "--force-exclude", f"--stdin-filename={os.path.join(workspace.root_path, '__init__.py')}", @@ -243,3 +244,23 @@ def f(): assert diag["code"] != "F401" os.unlink(os.path.join(workspace.root_path, "pyproject.toml")) + + +def test_notebook_input(workspace): + doc_str = r""" +print('hi') +import os +def f(): + a = 2 +""" + # attribute the python code to a notebook file name per jupyterlab-lsp + doc_uri = uris.from_fs_path(os.path.join(workspace.root_path, "Untitled.ipynb")) + workspace.put_document(doc_uri, doc_str) + doc = workspace.get_document(doc_uri) + + diags = ruff_lint.pylsp_lint(workspace, doc) + diag_codes = [diag["code"] for diag in diags] + assert "E999" not in diag_codes + assert "E402" in diag_codes + assert "F401" in diag_codes + assert "F841" in diag_codes From e5091cb686075505dfad901837c420415843cc7a Mon Sep 17 00:00:00 2001 From: Jochen Sprickerhof Date: Sat, 25 Nov 2023 15:01:20 +0100 Subject: [PATCH 06/12] Support setting target-version (#60) --- README.md | 1 + pylsp_ruff/plugin.py | 3 +++ pylsp_ruff/settings.py | 2 ++ 3 files changed, 6 insertions(+) diff --git a/README.md b/README.md index 80cd40a..288bb40 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,7 @@ the valid configuration keys: - `pylsp.plugins.ruff.format`: List of error codes to fix during formatting. Empty by default, use `["I"]` here to get import sorting as part of formatting. - `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). + - `pylsp.plugins.ruff.targetVersion`: The minimum Python version to target. For more information on the configuration visit [Ruff's homepage](https://beta.ruff.rs/docs/configuration/). diff --git a/pylsp_ruff/plugin.py b/pylsp_ruff/plugin.py index a70298e..04782c5 100644 --- a/pylsp_ruff/plugin.py +++ b/pylsp_ruff/plugin.py @@ -589,6 +589,9 @@ def build_check_arguments( if settings.extend_ignore: args.append(f"--extend-ignore={','.join(settings.extend_ignore)}") + if settings.target_version: + args.append(f"--target-version={settings.target_version}") + if settings.per_file_ignores: for path, errors in settings.per_file_ignores.items(): if not PurePath(document_path).match(path): diff --git a/pylsp_ruff/settings.py b/pylsp_ruff/settings.py index 0ccda7e..b5bc24d 100644 --- a/pylsp_ruff/settings.py +++ b/pylsp_ruff/settings.py @@ -27,6 +27,8 @@ class PluginSettings: severities: Optional[Dict[str, str]] = None + target_version: Optional[str] = None + def to_camel_case(snake_str: str) -> str: components = snake_str.split("_") From 5d7338d44ed8dc58bd0d72de664072f5495e433d Mon Sep 17 00:00:00 2001 From: Magnus Larsen Date: Sat, 25 Nov 2023 14:05:52 +0000 Subject: [PATCH 07/12] Add setting for enabling/disabling Preview rules (#54) --- README.md | 5 +++-- pylsp_ruff/plugin.py | 3 +++ pylsp_ruff/settings.py | 1 + 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 288bb40..5c2a5da 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ The plugin follows [python-lsp-server's configuration](https://github.com/python-lsp/python-lsp-server/#configuration). These are the valid configuration keys: - - `pylsp.plugins.ruff.enabled`: boolean to enable/disable the plugin. `true` by default. + - `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. Uses `os.executable -m "ruff"` by default. @@ -79,7 +79,8 @@ 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. Empty by default, use `["I"]` here to get import sorting as part of formatting. - - `pylsp.plugins.ruff.unsafeFixes`: boolean that enables/disables fixes that are marked "unsafe" by `ruff`. `false` by default. + - `pylsp.plugins.ruff.unsafeFixes`: Boolean that enables/disables fixes that are marked "unsafe" by `ruff`. `false` by default. + - `pylsp.plugins.ruff.preview`: Boolean that enables/disables rules & fixes that are marked "preview" by `ruff`. `false` by default. - `pylsp.plugins.ruff.severities`: Dictionary of custom severity levels for specific codes, see [below](#custom-severities). - `pylsp.plugins.ruff.targetVersion`: The minimum Python version to target. diff --git a/pylsp_ruff/plugin.py b/pylsp_ruff/plugin.py index 04782c5..6ef9cdf 100644 --- a/pylsp_ruff/plugin.py +++ b/pylsp_ruff/plugin.py @@ -571,6 +571,9 @@ def build_check_arguments( if settings.line_length: args.append(f"--line-length={settings.line_length}") + if settings.preview: + args.append("--preview") + if settings.unsafe_fixes: args.append("--unsafe-fixes") diff --git a/pylsp_ruff/settings.py b/pylsp_ruff/settings.py index b5bc24d..2345ce1 100644 --- a/pylsp_ruff/settings.py +++ b/pylsp_ruff/settings.py @@ -23,6 +23,7 @@ class PluginSettings: format: Optional[List[str]] = None + preview: bool = False unsafe_fixes: bool = False severities: Optional[Dict[str, str]] = None From 8a0270cbc58bd94695b84ed7d0018565e19df963 Mon Sep 17 00:00:00 2001 From: Julian Hossbach Date: Sat, 25 Nov 2023 15:29:43 +0100 Subject: [PATCH 08/12] Add target version and preview to formatter (#64) --- pylsp_ruff/plugin.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pylsp_ruff/plugin.py b/pylsp_ruff/plugin.py index 6ef9cdf..15b6dfb 100644 --- a/pylsp_ruff/plugin.py +++ b/pylsp_ruff/plugin.py @@ -646,6 +646,15 @@ def build_format_arguments( if settings.exclude: args.append(f"--exclude={','.join(settings.exclude)}") + if settings.preview: + args.append("--preview") + + if settings.line_length: + args.append(f"--line-length={settings.line_length}") + + if settings.target_version: + args.append(f"--target-version={settings.target_version}") + if extra_arguments: args.extend(extra_arguments) From f81071038ae74ded1e0a0504d7c1971297ed0b6c Mon Sep 17 00:00:00 2001 From: Julian Hossbach Date: Sat, 25 Nov 2023 19:09:12 +0100 Subject: [PATCH 09/12] add pattern matching to severities (#66) --- README.md | 3 +-- pylsp_ruff/plugin.py | 14 +++++++++++--- tests/test_ruff_lint.py | 4 ++-- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 5c2a5da..f696448 100644 --- a/README.md +++ b/README.md @@ -94,5 +94,4 @@ This default can be changed through the `pylsp.plugins.ruff.severities` option, For more information on the diagnostic severities please refer to [the official LSP reference](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#diagnosticSeverity). -Note that `python-lsp-ruff` does *not* accept regex, and it will *not* check whether the error code exists. If the custom severity level is not displayed, -please check first that the error code is correct and that the given value is one of the possible keys from above. +With `v2.0.0` it is also possible to use patterns to match codes. Rules match if the error code starts with the given pattern. If multiple patterns match the error code, `python-lsp-ruff` chooses the one with the most amount of matching characters. diff --git a/pylsp_ruff/plugin.py b/pylsp_ruff/plugin.py index 15b6dfb..1df0461 100644 --- a/pylsp_ruff/plugin.py +++ b/pylsp_ruff/plugin.py @@ -207,10 +207,18 @@ def create_diagnostic(check: RuffCheck, settings: PluginSettings) -> Diagnostic: if check.code == "E999" or check.code[0] == "F": severity = DiagnosticSeverity.Error - # Override severity with custom severity if possible, use default otherwise + # Check if check.code starts contained in given severities if settings.severities is not None: - custom_sev = settings.severities.get(check.code, None) - if custom_sev is not None: + _custom_sev = [ + sev + for pat, sev in sorted( + settings.severities.items(), key=lambda key: (len(key), key) + ) + if check.code.startswith(pat) + ] + + if _custom_sev: + custom_sev = _custom_sev[-1] severity = DIAGNOSTIC_SEVERITIES.get(custom_sev, severity) tags = [] diff --git a/tests/test_ruff_lint.py b/tests/test_ruff_lint.py index c88c559..2933d26 100644 --- a/tests/test_ruff_lint.py +++ b/tests/test_ruff_lint.py @@ -195,7 +195,7 @@ def f(): "plugins": { "ruff": { "extendIgnore": ["D104"], - "severities": {"E402": "E", "D103": "I"}, + "severities": {"E402": "E", "D": "I", "D1": "H"}, } } } @@ -217,7 +217,7 @@ def f(): if diag["code"] == "E402": assert diag["severity"] == 1 if diag["code"] == "D103": - assert diag["severity"] == 3 + assert diag["severity"] == 4 # Should take "D1" over "D" # Excludes doc_uri = uris.from_fs_path(os.path.join(workspace.root_path, "blah/__init__.py")) From 2d41c6d2b8a04a92dcd2c02bf4fd06ea0a50a010 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20Ho=C3=9Fbach?= Date: Sat, 25 Nov 2023 14:57:32 +0100 Subject: [PATCH 10/12] Update README --- README.md | 94 +++++++++++++++++++++++++++---------------------------- 1 file changed, 46 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index f696448..8a560ea 100644 --- a/README.md +++ b/README.md @@ -28,61 +28,43 @@ pip install "ruff<0.1.0" "python-lsp-ruff==1.5.3" 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`. -Sorting of the imports through `ruff` when formatting is enabled by default. -The list of code fixes can be changed via the `pylsp.plugins.ruff.format` option. Any codes given in the `format` option will only be marked as `fixable` for ruff during the formatting operation, the user has to make sure that these codes are also in the list of codes that ruff checks! -This example configuration for `neovim` shows how to always sort imports when running `textDocument/formatting`: - -```lua -lspconfig.pylsp.setup { - settings = { - pylsp = { - plugins = { - ruff = { - enabled = true, - extendSelect = { "I" }, - }, - } - } - } -} -``` - -## Code actions - -`python-lsp-ruff` supports code actions as given by possible fixes by `ruff`. `python-lsp-ruff` also supports [unsafe fixes](https://docs.astral.sh/ruff/linter/#fix-safety). -Fixes considered unsafe by `ruff` are marked `(unsafe)` in the code action. -The `Fix all` code action *only* consideres safe fixes. ## 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. -Note that any configuration options (except for `extendIgnore` and `extendSelect`, see -[this issue](https://github.com/python-lsp/python-lsp-ruff/issues/19)) passed to ruff via -`pylsp` are ignored if the project has a `pyproject.toml`. - -The plugin follows [python-lsp-server's -configuration](https://github.com/python-lsp/python-lsp-server/#configuration). These are -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. 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. - - `pylsp.plugins.ruff.perFileIgnores`: File-specific error codes to be ignored. - - `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. Empty by default, use `["I"]` here to get import sorting as part of formatting. - - `pylsp.plugins.ruff.unsafeFixes`: Boolean that enables/disables fixes that are marked "unsafe" by `ruff`. `false` by default. - - `pylsp.plugins.ruff.preview`: Boolean that enables/disables rules & fixes that are marked "preview" by `ruff`. `false` by default. - - `pylsp.plugins.ruff.severities`: Dictionary of custom severity levels for specific codes, see [below](#custom-severities). - - `pylsp.plugins.ruff.targetVersion`: The minimum Python version to target. +file is present in the project, `python-lsp-ruff` will ignore specific options (see below). + +The plugin follows [python-lsp-server's configuration](https://github.com/python-lsp/python-lsp-server/#configuration). +This example configuration using for `neovim` shows the possible optionsL + +```lua +pylsp = { + plugins = { + ruff = { + enabled = true, -- Enable the plugin + executable = "", -- Custom path to ruff + path = "", -- Custom config for ruff to use + extendSelect = { "I" }, -- Rules that are additionally used by ruff + extendIgnore = { "C90" }, -- Rules that are additionally ignored by ruff + format = { "I" }, -- Rules that are marked as fixable by ruff that should be fixed when running textDocument/formatting + severities = { ["D212"] = "I" }, -- Optional table of rules where a custom severity is desired + unsafeFixes = false, -- Whether or not to offer unsafe fixes as code actions. Ignored with the "Fix All" action + + -- Rules that are ignored when a pyproject.toml or ruff.toml is present: + lineLength = 88, -- Line length to pass to ruff checking and formatting + exclude = { "__about__.py" }, -- Files to be excluded by ruff checking + select = { "F" }, -- Rules to be enabled by ruff + ignore = { "D210" }, -- Rules to be ignored by ruff + perFileIgnores = { ["__init__.py"] = "CPY001" }, -- Rules that should be ignored for specific files + preview = false, -- Whether to enable the preview style linting and formatting. + targetVersion = "py310", -- The minimum python version to target (applies for both linting and formatting). + }, + } +} +``` For more information on the configuration visit [Ruff's homepage](https://beta.ruff.rs/docs/configuration/). @@ -95,3 +77,19 @@ For more information on the diagnostic severities please refer to [the official LSP reference](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#diagnosticSeverity). With `v2.0.0` it is also possible to use patterns to match codes. Rules match if the error code starts with the given pattern. If multiple patterns match the error code, `python-lsp-ruff` chooses the one with the most amount of matching characters. + + +## Code formatting + +With `python-lsp-ruff>1.6.0` formatting is done using [ruffs own formatter](https://docs.astral.sh/ruff/formatter/). +In addition, rules that should be fixed during the `textDocument/formatting` request can be added with the `format` option. + +Coming from previous versions the only change is that `isort` rules are **not** applied by default. +To enable sorting of imports using ruff's isort functionality, add `"I"` to the list of `format` rules. + + +## Code actions + +`python-lsp-ruff` supports code actions as given by possible fixes by `ruff`. `python-lsp-ruff` also supports [unsafe fixes](https://docs.astral.sh/ruff/linter/#fix-safety). +Fixes considered unsafe by `ruff` are marked `(unsafe)` in the code action. +The `Fix all` code action *only* consideres safe fixes. From d50b8f5430e2591b5fbeb417b86c5fddb7232c24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20Ho=C3=9Fbach?= Date: Sun, 26 Nov 2023 15:54:34 +0100 Subject: [PATCH 11/12] Update required python version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4098c34..a5c982c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ authors = [ version = "1.6.0" description = "Ruff linting plugin for pylsp" readme = "README.md" -requires-python = ">=3.7" +requires-python = ">=3.8" license = {text = "MIT"} dependencies = [ "ruff>=0.1.5, <0.2.0", From a4fac0281546d7d189211c01da6342037de6fa24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20Ho=C3=9Fbach?= Date: Sun, 26 Nov 2023 15:54:52 +0100 Subject: [PATCH 12/12] Bump version in pyproject --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a5c982c..85fa3e4 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.6.0" +version = "2.0.0" description = "Ruff linting plugin for pylsp" readme = "README.md" requires-python = ">=3.8"