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

Add Ruff as supported tool #694

Merged
merged 1 commit into from
Oct 25, 2024
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
9 changes: 9 additions & 0 deletions .prospector.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,12 @@ bandit:
run: true
disable:
- B101 # Use of assert detected.

ruff:
run: true
options:
fix: true
disable:
- E501 # line too long
- S101 # Use of assert detected
- SIM105 # suppressible-exception (slow code)
13 changes: 11 additions & 2 deletions docs/profiles.rst
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ Individual Configuration Options

Each tool can be individually configured with a section beginning with the tool name
(in lowercase). Valid values are ``bandit``, ``dodgy``, ``frosted``, ``mccabe``, ``mypy``, ``pydocstyle``, ``pycodestyle``,
``pyflakes``, ``pylint``, ``pyright``, ``pyroma`` and ``vulture``.
``pyflakes``, ``pylint``, ``pyright``, ``pyroma``, ``vulture`` and ``ruff``.

Enabling and Disabling Tools
............................
Expand Down Expand Up @@ -416,17 +416,26 @@ The available options are:
+----------------+------------------------+----------------------------------------------+
| pyright | venv-path | Directory that contains virtual environments |
+----------------+------------------------+----------------------------------------------+
| ruff | -anything- | Options pass to ruff as argument |
| | | `True` => --<option> |
| | | `False` => ignoring |
| | | <string> => --<option>=<value> |
| | | <list> => --<option>=<comma separated value> |
| | | <dict> => --<option>=<comma separated key> |
| | | if sub value is true |
+----------------+------------------------+----------------------------------------------+

See `bandit options`_ for more details

See `pyright options`_ for more details


See `ruff options`_ for more details

.. _pylint options: https://pylint.readthedocs.io/en/latest/user_guide/run.html
.. _bandit options: https://bandit.readthedocs.io/en/latest/config.html
.. _mypy options: https://mypy.readthedocs.io/en/stable/command_line.html
.. _pyright options: https://microsoft.github.io/pyright/#/command-line
.. _ruff options: https://docs.astral.sh/ruff/configuration/#command-line-interface



Expand Down
11 changes: 11 additions & 0 deletions docs/supported_tools.rst
Original file line number Diff line number Diff line change
Expand Up @@ -172,3 +172,14 @@ To install and use::

pip install prospector[with_pyright]
prospector --with-tool pyright


`Ruff <https://docs.astral.sh/ruff/>`_
``````````````````````````````````````

An extremely fast Python linter, written in Rust.

To install and use::

pip install prospector[with_ruff]
prospector --with-tool ruff
32 changes: 30 additions & 2 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions prospector/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,9 @@ def exit_with_zero_on_success(self) -> bool:
def get_disabled_messages(self, tool_name: str) -> list[str]:
return self.profile.get_disabled_messages(tool_name)

def get_enabled_messages(self, tool_name: str) -> list[str]:
return self.profile.get_enabled_messages(tool_name)

def use_external_config(self, _: Any) -> bool:
# Currently there is only one single global setting for whether to use
# global config, but this could be extended in the future
Expand Down
14 changes: 13 additions & 1 deletion prospector/formatters/pylint.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,24 @@ def render_messages(self) -> list[str]:
# prospector/configuration.py:65: [missing-docstring(missing-docstring), build_default_sources] \
# Missing function docstring

template = "%(path)s:%(line)s: [%(code)s(%(source)s), %(function)s] %(message)s"
template_location = (
"%(path)s"
if message.location.line is None
else "%(path)s:%(line)s"
if message.location.character is None
else "%(path)s:%(line)s:%(character)s"
)
template_code = (
"%(code)s(%(source)s)" if message.location.function is None else "[%(code)s(%(source)s), %(function)s]"
)
template = f"{template_location}: {template_code}: %(message)s"

output.append(
template
% {
"path": self._make_path(message.location),
"line": message.location.line,
"character": message.location.character,
"source": message.source,
"code": message.code,
"function": message.location.function,
Expand Down
20 changes: 11 additions & 9 deletions prospector/profiles/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ def get_disabled_messages(self, tool_name: str) -> list[str]:
enable = getattr(self, tool_name)["enable"]
return list(set(disable) - set(enable))

def get_enabled_messages(self, tool_name: str) -> list[str]:
disable = getattr(self, tool_name)["disable"]
enable = getattr(self, tool_name)["enable"]
return list(set(enable) - set(disable))

def is_tool_enabled(self, name: str) -> bool:
enabled: Optional[bool] = getattr(self, name).get("run")
if enabled is not None:
Expand Down Expand Up @@ -189,24 +194,21 @@ def _ensure_list(value: Any) -> list[Any]:


def _simple_merge_dict(priority: dict[str, Any], base: dict[str, Any]) -> dict[str, Any]:
out = dict(base.items())
out.update(dict(priority.items()))
out = {**base, **priority}
keys = set(priority.keys()) | set(base.keys())
for key in keys:
if isinstance(base.get(key), dict) and isinstance(priority.get(key), dict):
out[key] = _simple_merge_dict(priority[key], base[key])
return out


def _merge_tool_config(priority: dict[str, Any], base: dict[str, Any]) -> dict[str, Any]:
out = dict(base.items())
out = {**base, **priority}

# add options that are missing, but keep existing options from the priority dictionary
# TODO: write a unit test for this :-|
out["options"] = _simple_merge_dict(priority.get("options", {}), base.get("options", {}))

# copy in some basic pieces
for key in ("run", "load-plugins"):
value = priority.get(key, base.get(key))
if value is not None:
out[key] = value

# anything enabled in the 'priority' dict is removed
# from 'disabled' in the base dict and vice versa
base_disabled: list[Any] = base.get("disable") or []
Expand Down
1 change: 1 addition & 0 deletions prospector/tools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ def _optional_tool(
"pyright": _optional_tool("pyright"),
"mypy": _optional_tool("mypy"),
"bandit": _optional_tool("bandit"),
"ruff": _optional_tool("ruff"),
}


Expand Down
75 changes: 75 additions & 0 deletions prospector/tools/ruff/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import json
import subprocess # nosec
from typing import TYPE_CHECKING, Any

from ruff.__main__ import find_ruff_bin

from prospector.finder import FileFinder
from prospector.message import Location, Message
from prospector.tools.base import ToolBase

if TYPE_CHECKING:
from prospector.config import ProspectorConfig


class RuffTool(ToolBase):
def configure(self, prospector_config: "ProspectorConfig", _: Any) -> None:
self.ruff_bin = find_ruff_bin()
self.ruff_args = ["check", "--output-format=json"]

enabled = prospector_config.get_enabled_messages("ruff")
if enabled:
enabled_arg_value = ",".join(enabled)
self.ruff_args.append(f"--select={enabled_arg_value}")
disabled = prospector_config.get_disabled_messages("ruff")
if disabled:
disabled_arg_value = ",".join(disabled)
self.ruff_args.append(f"--ignore={disabled_arg_value}")

options = prospector_config.tool_options("ruff")
for key, value in options.items():
if value is True:
self.ruff_args.append(f"--{key}")
elif value is False:
pass
elif isinstance(value, list):
arg_value = ",".join(value)
self.ruff_args.append(f"--{key}={arg_value}")
# dict is like array but with a dict with true/false value to be able to merge profiles
elif isinstance(value, dict):
arg_value = ",".join(k for k, v in value.items() if v)
self.ruff_args.append(f"--{key}={arg_value}")
else:
self.ruff_args.append(f"--{key}={value}")

def run(self, found_files: FileFinder) -> list[Message]:
print([self.ruff_bin, *self.ruff_args])
messages = []
completed_process = subprocess.run( # noqa: S603
[self.ruff_bin, *self.ruff_args, *found_files.python_modules], capture_output=True
)
for message in json.loads(completed_process.stdout):
sub_message = {}
if message.get("url"):
sub_message["See"] = message["url"]
if message.get("fix") and message["fix"].get("applicability"):
sub_message["Fix applicability"] = message["fix"]["applicability"]
message_str = message.get("message", "")
if sub_message:
message_str += f" [{', '.join(f'{k}: {v}' for k, v in sub_message.items())}]"

messages.append(
Message(
"ruff",
message.get("code") or "unknown",
Location(
message.get("filename") or "unknown",
None,
None,
line=message.get("location", {}).get("row"),
character=message.get("location", {}).get("column"),
),
message_str,
)
)
return messages
4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -59,14 +59,16 @@ vulture = {version = ">=1.5", optional = true}
mypy = {version = ">=0.600", optional = true}
pyright = {version = ">=1.1.3", optional = true}
pyroma = {version = ">=2.4", optional = true}
ruff = {version = "*", optional = true}

[tool.poetry.extras]
with_bandit = ["bandit"]
with_mypy = ["mypy"]
with_pyright = ["pyright"]
with_pyroma = ["pyroma"]
with_vulture = ["vulture"]
with_everything = ["bandit", "mypy", "pyright", "pyroma", "vulture"]
with_ruff = ["ruff"]
with_everything = ["bandit", "mypy", "pyright", "pyroma", "vulture", "ruff"]

[tool.poetry.dev-dependencies]
coveralls = "^3.3.1"
Expand Down
18 changes: 18 additions & 0 deletions tests/tools/bandit/test_ruff_tool.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from pathlib import Path
from unittest import TestCase
from unittest.mock import patch

from prospector.config import ProspectorConfig
from prospector.finder import FileFinder
from prospector.tools.ruff import RuffTool


class test_ruff_tool:
with patch("sys.argv", []):
config = ProspectorConfig()
ruff_tool = RuffTool()

found_files = FileFinder(Path(__file__).parent / "testpath/testfile.py")
ruff_tool.configure(config, found_files)
messages = ruff_tool.run(found_files)
assert {"S105", "S106", "S107"} == {message.code for message in messages}
Loading