Skip to content

Commit

Permalink
Merge pull request #52 from python-lsp/pr_v160
Browse files Browse the repository at this point in the history
Support new ruff version, fix wrong call to ruff through PATH
  • Loading branch information
jhossbach committed Oct 20, 2023
2 parents b71fcd3 + 3f17ceb commit be27747
Show file tree
Hide file tree
Showing 7 changed files with 89 additions and 20 deletions.
16 changes: 12 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand All @@ -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.
Expand All @@ -58,19 +65,20 @@ 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.
- `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.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
Expand Down
47 changes: 37 additions & 10 deletions pylsp_ruff/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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)
Expand All @@ -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(
Expand All @@ -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"]
Expand Down Expand Up @@ -446,19 +465,21 @@ 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)
(stdout, stderr) = p.communicate(document_source.encode())

if stderr:
if p.returncode != 0:
log.error(f"Error running ruff: {stderr.decode()}")

return stdout.decode()
Expand Down Expand Up @@ -491,8 +512,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:
Expand All @@ -510,6 +533,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)}")

Expand Down Expand Up @@ -583,6 +609,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,
Expand Down
1 change: 1 addition & 0 deletions pylsp_ruff/ruff.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class Edit:
class Fix:
edits: List[Edit]
message: str
applicability: str


@dataclass
Expand Down
5 changes: 3 additions & 2 deletions pylsp_ruff/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@
@dataclass
class PluginSettings:
enabled: bool = True
executable: str = "ruff"

executable: Optional[str] = None
config: Optional[str] = None
line_length: Optional[int] = None

Expand All @@ -24,6 +23,8 @@ class PluginSettings:

format: Optional[List[str]] = None

unsafe_fixes: bool = False

severities: Optional[Dict[str, str]] = None


Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ name = "python-lsp-ruff"
authors = [
{name = "Julian Hossbach", email = "[email protected]"}
]
version = "1.5.3"
version = "1.6.0"
description = "Ruff linting plugin for pylsp"
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'",
Expand Down
29 changes: 29 additions & 0 deletions tests/test_code_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
7 changes: 5 additions & 2 deletions tests/test_ruff_lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# Copyright 2021- Python Language Server Contributors.

import os
import sys
import tempfile
from unittest.mock import Mock, patch

Expand Down Expand Up @@ -154,7 +155,6 @@ def f():
)

# Check that user config is ignored
assert ruff_settings.executable == "ruff"
empty_keys = [
"config",
"line_length",
Expand All @@ -175,9 +175,12 @@ def f():

call_args = popen_mock.call_args[0][0]
assert call_args == [
str(sys.executable),
"-m",
"ruff",
"--quiet",
"--format=json",
"--exit-zero",
"--output-format=json",
"--no-fix",
"--force-exclude",
f"--stdin-filename={os.path.join(workspace.root_path, '__init__.py')}",
Expand Down

0 comments on commit be27747

Please sign in to comment.