Skip to content

Commit

Permalink
feat: check for lint section in Ruff
Browse files Browse the repository at this point in the history
Signed-off-by: Henry Schreiner <[email protected]>
  • Loading branch information
henryiii committed Oct 24, 2023
1 parent bc9c30c commit 2e82c63
Show file tree
Hide file tree
Showing 4 changed files with 142 additions and 55 deletions.
8 changes: 5 additions & 3 deletions docs/pages/guides/style.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,10 @@ inspect and undo changes in git.

```toml
[tool.ruff]
src = ["src"]
exclude = []
[tool.ruff.lint]
select = [
"E", "F", "W", # flake8
"B", # flake8-bugbear
Expand Down Expand Up @@ -206,16 +210,14 @@ ignore = [
"PT004", # Use underscore for non-returning fixture (use usefixture instead)
]
typing-modules = ["mypackage._compat.typing"]
src = ["src"]
unfixable = [
"T20", # Removes print statements
"F841", # Removes unused variables
]
exclude = []
flake8-unused-arguments.ignore-variadic-names = true
isort.required-imports = ["from __future__ import annotations"]
[tool.ruff.per-file-ignores]
[tool.ruff.lint.per-file-ignores]
"tests/**" = ["T20"]
```

Expand Down
11 changes: 7 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ workflows = "sp_repo_review.checks.github:workflows"
dependabot = "sp_repo_review.checks.github:dependabot"
precommit = "sp_repo_review.checks.precommit:precommit"
readthedocs = "sp_repo_review.checks.readthedocs:readthedocs"
ruff = "sp_repo_review.checks.ruff:ruff"

[project.entry-points."repo_review.families"]
scikit-hep = "sp_repo_review.families:get_families"
Expand Down Expand Up @@ -133,6 +134,10 @@ messages_control.disable = [


[tool.ruff]
src = ["src"]
exclude = []

[tool.ruff.lint]
select = [
"E", "F", "W", # flake8
"B", # flake8-bugbear
Expand Down Expand Up @@ -161,15 +166,13 @@ ignore = [
"PT004", # Incorrect check, usefixtures is the correct way to do this
"RUF012", # Would require a lot of ClassVar's
]
src = ["src"]
unfixable = [
"T20", # Removes print statements
"F841", # Removes unused variables
]
exclude = []
flake8-unused-arguments.ignore-variadic-names = true

[tool.ruff.flake8-tidy-imports.banned-api]
[tool.ruff.lint.flake8-tidy-imports.banned-api]
"typing.Callable".msg = "Use collections.abc.Callable instead."
"typing.Iterator".msg = "Use collections.abc.Iterator instead."
"typing.Mapping".msg = "Use collections.abc.Mapping instead."
Expand All @@ -178,7 +181,7 @@ flake8-unused-arguments.ignore-variadic-names = true
"importlib.abc".msg = "Use sp_repo_review._compat.importlib.resources.abc instead."
"importlib.resources.abc".msg = "Use sp_repo_review._compat.importlib.resources.abc instead."

[tool.ruff.per-file-ignores]
[tool.ruff.lint.per-file-ignores]
"src/sp_repo_review/_compat/**.py" = ["TID251"]

[tool.repo-review]
Expand Down
165 changes: 123 additions & 42 deletions src/sp_repo_review/checks/ruff.py
Original file line number Diff line number Diff line change
@@ -1,51 +1,65 @@
from __future__ import annotations

from collections.abc import Generator
from typing import Any, ClassVar, Protocol

from .._compat import tomllib
from .._compat.importlib.resources.abc import Traversable
from . import mk_url

## R0xx: Ruff general
## R1xx: Ruff checks
## R2xx: Ruff deprecations


def ruff(pyproject: dict[str, Any], root: Traversable) -> dict[str, Any] | None:
"""
Returns the ruff configuration, or None if the configuration doesn't exist.
Respects ``ruff.toml`` and ``.ruff.toml`` in addition to
``pyproject.toml``.
"""
paths = [root.joinpath(".ruff.toml"), root.joinpath("ruff.toml")]
for path in paths:
if path.is_file():
with path.open("rb") as f:
return tomllib.load(f)
return pyproject.get("tool", {}).get("ruff", None) # type: ignore[no-any-return]


class Ruff:
family = "ruff"
url = mk_url("style")
requires = {"RF001"}


class RF001(Ruff):
"Has Ruff config"
requires = {"PY001"}

@staticmethod
def check(pyproject: dict[str, Any]) -> bool:
def check(ruff: dict[str, Any] | None) -> bool:
"""
Must have `[tool.ruff]` section in `pyproject.toml`. Other forms of
configuration are not supported by this check.
"""

match pyproject:
case {"tool": {"ruff": object()}}:
return True
case _:
return False
return ruff is not None


class RF002(Ruff):
"Target version must be set"
requires = {"RF001"}

@staticmethod
def check(pyproject: dict[str, Any]) -> bool:
def check(pyproject: dict[str, Any], ruff: dict[str, Any]) -> bool:
"""
Must select a minimum version to target. Affects pyupgrade, isort, and
others. Will be inferred from `project.requires-python`.
"""

if "target-version" in ruff:
return True

match pyproject:
case {"tool": {"ruff": {"target-version": str()}}}:
return True
case {"project": {"requires-python": str()}}:
return True
case _:
Expand All @@ -55,10 +69,8 @@ def check(pyproject: dict[str, Any]) -> bool:
class RF003(Ruff):
"src directory specified if used"

requires = {"RF001"}

@staticmethod
def check(pyproject: dict[str, Any], package: Traversable) -> bool | None:
def check(ruff: dict[str, Any], package: Traversable) -> bool | None:
"""
Must specify `src` directory if it exists.
Expand All @@ -70,41 +82,21 @@ def check(pyproject: dict[str, Any], package: Traversable) -> bool | None:
if not package.joinpath("src").is_dir():
return None

match pyproject["tool"]["ruff"]:
match ruff:
case {"src": list(x)}:
return "src" in x
case _:
return False


class RF004(Ruff):
"Deprecated config options should be avoided"

requires = {"RF001"}
url = ""

@staticmethod
def check(pyproject: dict[str, Any]) -> str:
match pyproject["tool"]["ruff"]:
case {"extend-unfixable": object()}:
return "`extend-unfixable` deprecated, use `unfixable` (identical)"
case {"extend-ignore": object()}:
return "`extend-ignore` deprecated, use `ignore` (identical)"
case _:
return ""


class RuffMixin(Protocol):
class RF1xxMixin(Protocol):
code: ClassVar[str]
name: ClassVar[str]


class RF1xx(Ruff):
family = "ruff"
requires = {"RF001"}

@classmethod
def check(cls: type[RuffMixin], pyproject: dict[str, Any]) -> bool:
def check(cls: type[RF1xxMixin], ruff: dict[str, Any]) -> bool:
"""
Must select the {self.name} `{self.code}` checks. Recommended:
Expand All @@ -116,8 +108,10 @@ def check(cls: type[RuffMixin], pyproject: dict[str, Any]) -> bool:
```
"""

match pyproject["tool"]["ruff"]:
case {"select": list(x)} | {"extend-select": list(x)}:
match ruff:
case {"lint": {"select": list(x)} | {"extend-select": list(x)}} | {
"select": list(x)
} | {"extend-select": list(x)}:
return cls.code in x or "ALL" in x
case _:
return False
Expand All @@ -141,8 +135,95 @@ class RF103(RF1xx):
name = "pyupgrade"


class RF2xxMixin(Protocol):
@staticmethod
def iter_check(ruff: dict[str, Any]) -> Generator[str, None, None]:
...


class RF2xx(Ruff):
url = ""

@classmethod
def check(cls: type[RF2xxMixin], ruff: dict[str, Any]) -> str:
return "\n\n".join(cls.iter_check(ruff))


class RF201(RF2xx):
"Avoid using deprecated config settings"

@staticmethod
def iter_check(ruff: dict[str, Any]) -> Generator[str, None, None]:
match ruff:
case {"extend-unfixable": object()} | {
"lint": {"extend-unfixable": object()}
}:
yield "`extend-unfixable` deprecated, use `unfixable` instead (identical)"
case {"extend-ignore": object()} | {"lint": {"extend-ignore": object()}}:
yield "`extend-ignore` deprecated, use `ignore` instead (identical)"
case _:
pass


RUFF_LINT = {
"allowed-confusables",
"dummy-variable-rgx",
"explicit-preview-rules",
"extend-fixable",
"extend-ignore",
"extend-per-file-ignores",
"extend-safe-fixes",
"extend-select",
"extend-unfixable",
"extend-unsafe-fixes",
"external",
"fixable",
"flake8-annotations",
"flake8-bandit",
"flake8-bugbear",
"flake8-builtins",
"flake8-comprehensions",
"flake8-copyright",
"flake8-errmsg",
"flake8-gettext",
"flake8-implicit-str-concat",
"flake8-import-conventions",
"flake8-pytest-style",
"flake8-quotes",
"flake8-self",
"flake8-tidy-imports",
"flake8-type-checking",
"flake8-unused-arguments",
"ignore",
"ignore-init-module-imports",
"isort",
"logger-objects",
"mccabe",
"pep8-naming",
"per-file-ignores",
"pycodestyle",
"pydocstyle",
"pyflakes",
"pylint",
"pyupgrade",
"select",
"task-tags",
"typing-modules",
"unfixable",
}


class RF202(RF2xx):
"Use (new) lint config section"

@staticmethod
def iter_check(ruff: dict[str, Any]) -> Generator[str, None, None]:
for item in sorted(set(ruff) & RUFF_LINT):
yield f"`{item}` should be set as `lint.{item}` instead"


def repo_review_checks() -> dict[str, Ruff]:
base_classes = set(Ruff.__subclasses__()) - {RF1xx}
rf1xx_classes = set(RF1xx.__subclasses__())
repo_review_checks = base_classes | rf1xx_classes
return {p.__name__: p() for p in repo_review_checks}
classes = set(Ruff.__subclasses__()) - {RF1xx, RF2xx}
classes |= set(RF1xx.__subclasses__())
classes |= set(RF2xx.__subclasses__())
return {p.__name__: p() for p in classes}
13 changes: 7 additions & 6 deletions {{cookiecutter.project_name}}/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,12 @@ disallow_incomplete_defs = true


[tool.ruff]
src = ["src"]
{%- if cookiecutter.backend in ["setuptools", "pybind11", "poetry"] %}
target-version = "py38"
{%- endif %}

[tool.ruff.lint]
select = [
"E", "F", "W", # flake8
"B", # flake8-bugbear
Expand Down Expand Up @@ -367,21 +373,16 @@ ignore = [
"PLR", # Design related pylint codes
"E501", # Line too long
]
{%- if cookiecutter.backend in ["setuptools", "pybind11", "poetry"] %}
target-version = "py38"
{%- endif %}
src = ["src"]
unfixable = [
"T20", # Removes print statements
"F841", # Removes unused variables
]
exclude = []
flake8-unused-arguments.ignore-variadic-names = true
isort.required-imports = ["from __future__ import annotations"]
# Uncomment if using a _compat.typing backport
# typing-modules = ["{{ cookiecutter.__project_slug }}._compat.typing"]

[tool.ruff.per-file-ignores]
[tool.ruff.lint.per-file-ignores]
"tests/**" = ["T20"]
"noxfile.py" = ["T20"]

Expand Down

0 comments on commit 2e82c63

Please sign in to comment.