From 23c06cf40b0efe9f0d4e99ea1300bb6631e62064 Mon Sep 17 00:00:00 2001 From: Benjamin Schubert Date: Sat, 30 Dec 2023 10:35:37 +0000 Subject: [PATCH] Add support for the ruff linter and formatter This is a new linter that is gaining in popularity quickly, let's add a predefined step for it. This also now sets CLICOLOR_FORCE=1 when color is requested, as this is what ruff listens for, and is a standard for quite a few tools --- docs/conf.py | 1 + dwasfile.py | 26 +++++++- pyproject.toml | 6 ++ src/dwas/_config.py | 1 + src/dwas/predefined/__init__.py | 2 + src/dwas/predefined/_ruff.py | 89 +++++++++++++++++++++++++++ tests/conftest.py | 2 +- tests/predefined/mixins.py | 2 +- tests/predefined/test_black.py | 4 +- tests/predefined/test_docformatter.py | 4 +- tests/predefined/test_isort.py | 4 +- tests/predefined/test_ruff.py | 44 +++++++++++++ tests/predefined/test_unimport.py | 4 +- 13 files changed, 177 insertions(+), 12 deletions(-) create mode 100644 src/dwas/predefined/_ruff.py create mode 100644 tests/predefined/test_ruff.py diff --git a/docs/conf.py b/docs/conf.py index 0d3acb0..9874a7b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -131,6 +131,7 @@ .. _mypy: https://mypy.readthedocs.io/en/stable/ .. _pylint: https://pylint.pycqa.org/en/latest/ .. _pytest: https://docs.pytest.org/en/stable/ +.. _ruff: https://docs.astral.sh/ruff/ .. _sphinx: https://www.sphinx-doc.org/ .. _twine: https://twine.readthedocs.io/en/stable/ .. _the Unimport formatter: https://unimport.hakancelik.dev/ diff --git a/dwasfile.py b/dwasfile.py index 5925e72..d53f527 100644 --- a/dwasfile.py +++ b/dwasfile.py @@ -74,10 +74,27 @@ requires=["isort:fix", "docformatter:fix"], run_by_default=False, ) +dwas.register_managed_step( + dwas.predefined.ruff( + files=PYTHON_FILES, + additional_arguments=["check", "--fix", "--show-fixes", "--fix-only"], + ), + dependencies=["ruff"], + python=OLDEST_SUPPORTED_PYTHON, + name="ruff:fix", + requires=["black:fix"], + run_by_default=False, +) dwas.register_step_group( name="fix", description="Fix all auto-fixable issues on the project", - requires=["unimport:fix", "isort:fix", "docformatter:fix", "black:fix"], + requires=[ + "unimport:fix", + "isort:fix", + "docformatter:fix", + "black:fix", + "ruff:fix", + ], run_by_default=False, ) @@ -106,7 +123,12 @@ ], python=OLDEST_SUPPORTED_PYTHON, ) -dwas.register_step_group("lint", ["mypy", "pylint"]) +dwas.register_managed_step( + dwas.predefined.ruff(files=PYTHON_FILES), + dependencies=["ruff"], + python=OLDEST_SUPPORTED_PYTHON, +) +dwas.register_step_group("lint", ["mypy", "pylint", "ruff"]) ## # Packaging diff --git a/pyproject.toml b/pyproject.toml index 329c48b..57dcf8a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -129,6 +129,12 @@ disable = [ [tool.pylint.variables] "init-import" = true +## +# Ruff +[tool.ruff] +target-version = "py38" + + ## # Testing diff --git a/src/dwas/_config.py b/src/dwas/_config.py index 70b59d6..114d2a0 100644 --- a/src/dwas/_config.py +++ b/src/dwas/_config.py @@ -195,6 +195,7 @@ def __init__( if self.colors: self.environ["PY_COLORS"] = "1" self.environ["FORCE_COLOR"] = "1" + self.environ["CLICOLOR_FORCE"] = "1" else: self.environ["PY_COLORS"] = "0" self.environ["NO_COLOR"] = "0" diff --git a/src/dwas/predefined/__init__.py b/src/dwas/predefined/__init__.py index 1df2203..f35c8e0 100644 --- a/src/dwas/predefined/__init__.py +++ b/src/dwas/predefined/__init__.py @@ -22,6 +22,7 @@ from ._package import package from ._pylint import pylint from ._pytest import pytest +from ._ruff import ruff from ._sphinx import sphinx from ._twine import twine from ._unimport import unimport @@ -35,6 +36,7 @@ "package", "pylint", "pytest", + "ruff", "sphinx", "twine", "unimport", diff --git a/src/dwas/predefined/_ruff.py b/src/dwas/predefined/_ruff.py new file mode 100644 index 0000000..066d3b7 --- /dev/null +++ b/src/dwas/predefined/_ruff.py @@ -0,0 +1,89 @@ +from typing import List, Optional, Sequence + +# XXX: All imports here should be done from the top level. If we need it, +# users might need it +from .. import Step, StepRunner, build_parameters, set_defaults + + +@set_defaults( + { + "dependencies": ["ruff"], + "files": ["."], + "additional_arguments": ["check"], + } +) +class Ruff(Step): + def __init__(self) -> None: + self.__name__ = "ruff" + + def __call__( + self, + step: StepRunner, + files: Sequence[str], + additional_arguments: List[str], + ) -> None: + step.run( + ["ruff", *additional_arguments, *files], + env={"RUFF_CACHE_DIR": str(step.cache_path / "ruff-cache")}, + ) + + +def ruff( + *, + files: Optional[Sequence[str]] = None, + additional_arguments: Optional[List[str]] = None, +) -> Step: + """ + Run `Ruff`_ against your python source code. + + By default, it will depend on :python:`["ruff"]`, when registered with + :py:func:`dwas.register_managed_step`. + + :param files: The list of files or directories to run ``ruff`` against. + Defaults to :python:`["."]`. + :param additional_arguments: Additional arguments to pass to the ``ruff`` + invocation. Defaults to :python:`["check"]`. + Defaults to :python:`["--check", "--diff", "-W1"]`. + :return: The step so that you can add additional parameters to it if needed. + + :Examples: + + In order to verify your code but not change it, for a step + named **ruff**: + + .. code-block:: + + register_managed_step(dwas.predefined.ruff()) + + Or, in order to automatically fix your code, but only if requested: + + .. code-block:: + + register_managed_step( + dwas.predefined.ruff(additional_arguments=["check", "--fix"]), + # NOTE: this name is arbitrary, you could omit it, or specify + # something else. We suffix in our documentation all + # operations that will have destructive effect on the source + # code by ``:fix`` + name="ruff:fix", + run_by_default=False, + ) + + Similarly, if you want to use ruff to format your code you could do: + + .. code-block:: + + # To check the formatting + register_managed_step( + dwas.predefined.ruff(additional_arguments=["format", "--diff"]), + name="ruff:format-check", + ) + # To autoformat + register_managed_step( + dwas.predefined.ruff(additional_arguments=["format"]), + name="ruff:format", + ) + """ + return build_parameters( + files=files, additional_arguments=additional_arguments + )(Ruff()) diff --git a/tests/conftest.py b/tests/conftest.py index 4315d15..517f8e7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,7 +14,7 @@ # pylint: disable=wrong-import-position pytest.register_assert_rewrite("tests.predefined.mixins", "tests._utils") -from ._utils import isolated_context +from ._utils import isolated_context # noqa: E402 def pytest_collection_modifyitems(items): diff --git a/tests/predefined/mixins.py b/tests/predefined/mixins.py index b18db9f..3f95eb8 100644 --- a/tests/predefined/mixins.py +++ b/tests/predefined/mixins.py @@ -96,7 +96,7 @@ def test_respects_color_settings( assert not COLOR_ESCAPE_CODE.search(result.stdout) -class BaseFormatterTest(BaseLinterTest): +class BaseLinterWithAutofixTest(BaseLinterTest): @property @abstractmethod def autofix_step(self) -> str: diff --git a/tests/predefined/test_black.py b/tests/predefined/test_black.py index c50d334..ea3967b 100644 --- a/tests/predefined/test_black.py +++ b/tests/predefined/test_black.py @@ -1,7 +1,7 @@ -from .mixins import BaseFormatterTest +from .mixins import BaseLinterWithAutofixTest -class TestBlack(BaseFormatterTest): +class TestBlack(BaseLinterWithAutofixTest): dwasfile = """\ from dwas import register_managed_step from dwas.predefined import black diff --git a/tests/predefined/test_docformatter.py b/tests/predefined/test_docformatter.py index ec2d85f..ec6504b 100644 --- a/tests/predefined/test_docformatter.py +++ b/tests/predefined/test_docformatter.py @@ -1,9 +1,9 @@ import pytest -from .mixins import BaseFormatterTest +from .mixins import BaseLinterWithAutofixTest -class TestDocformatter(BaseFormatterTest): +class TestDocformatter(BaseLinterWithAutofixTest): dwasfile = """\ from dwas import register_managed_step from dwas.predefined import docformatter diff --git a/tests/predefined/test_isort.py b/tests/predefined/test_isort.py index 21a95eb..7458b23 100644 --- a/tests/predefined/test_isort.py +++ b/tests/predefined/test_isort.py @@ -1,7 +1,7 @@ -from .mixins import BaseFormatterTest +from .mixins import BaseLinterWithAutofixTest -class TestIsort(BaseFormatterTest): +class TestIsort(BaseLinterWithAutofixTest): dwasfile = """\ from dwas import register_managed_step from dwas.predefined import isort diff --git a/tests/predefined/test_ruff.py b/tests/predefined/test_ruff.py new file mode 100644 index 0000000..bee3520 --- /dev/null +++ b/tests/predefined/test_ruff.py @@ -0,0 +1,44 @@ +import pytest + +from .mixins import BaseLinterWithAutofixTest + + +class TestRuffCheck(BaseLinterWithAutofixTest): + dwasfile = """\ +from dwas import register_managed_step +from dwas.predefined import ruff + +register_managed_step(ruff()) +register_managed_step( + ruff(additional_arguments=["check", "--fix"]), + name="ruff:fix", + run_by_default=False, +) +""" + invalid_file = """\ +from pathlib import Path +import os +""" + valid_file = '"""This is a token file"""\n' + autofix_step = "ruff:fix" + + +class TestRuffFormat(BaseLinterWithAutofixTest): + dwasfile = """\ +from dwas import register_managed_step +from dwas.predefined import ruff + +register_managed_step(ruff(additional_arguments=["format", "--diff"])) +register_managed_step( + ruff(additional_arguments=["format"]), + name="ruff:fix", + run_by_default=False, +) +""" + autofix_step = "ruff:fix" + invalid_file = "x = 1" + valid_file = "x = 1\n" + + @pytest.mark.skip("ruff format does not support colored output") + def test_respects_color_settings(self): + pass # pragma: nocover diff --git a/tests/predefined/test_unimport.py b/tests/predefined/test_unimport.py index 20eab8e..5fd504a 100644 --- a/tests/predefined/test_unimport.py +++ b/tests/predefined/test_unimport.py @@ -1,7 +1,7 @@ -from .mixins import BaseFormatterTest +from .mixins import BaseLinterWithAutofixTest -class TestUnimport(BaseFormatterTest): +class TestUnimport(BaseLinterWithAutofixTest): dwasfile = """\ from dwas import register_managed_step from dwas.predefined import unimport