Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support new ruff version, fix wrong call to ruff through PATH #52

Merged
merged 4 commits into from
Oct 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading