Skip to content

Commit

Permalink
feat: check for deprecated Ruff codes (#297)
Browse files Browse the repository at this point in the history
* feat: check for deprecated Ruff codes

Signed-off-by: Henry Schreiner <[email protected]>

* feat: check for lint section in Ruff

Signed-off-by: Henry Schreiner <[email protected]>

---------

Signed-off-by: Henry Schreiner <[email protected]>
  • Loading branch information
henryiii authored Oct 25, 2023
1 parent 5ea2c07 commit d152f42
Show file tree
Hide file tree
Showing 5 changed files with 150 additions and 46 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,8 @@ for family, grp in itertools.groupby(collected.checks.items(), key=lambda x: x[1
- [`RF101`](https://learn.scientific-python.org/development/guides/style#RF101): Bugbear must be selected
- [`RF102`](https://learn.scientific-python.org/development/guides/style#RF102): isort must be selected
- [`RF103`](https://learn.scientific-python.org/development/guides/style#RF103): pyupgrade must be selected
- `RF201`: Avoid using deprecated config settings
- `RF202`: Use (new) lint config section

<!-- [[[end]]] -->

Expand Down
14 changes: 8 additions & 6 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 All @@ -200,22 +204,20 @@ select = [
"NPY", # NumPy specific rules
"PD", # pandas-vet
]
extend-ignore = [
ignore = [
"PLR", # Design related pylint codes
"E501", # Line too long
"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 All @@ -224,8 +226,8 @@ select what you want from these. Like Flake8, plugins match by whole letter
sequences (with the special exception of pylint's "PL" shortcut), then you can
also include leading or whole error codes. Codes starting with 9 must be
selected explicitly, with at least the letters followed by a 9. You can also
ignore certain error codes via `extend-ignore`. You can also set codes per paths
to ignore in `per-file-ignores`. If you don't like certain auto-fixes, you can
ignore certain error codes via `ignore`. You can also set codes per paths to
ignore in `per-file-ignores`. If you don't like certain auto-fixes, you can
disable auto-fixing for specific error codes via `unfixable`.

There are other configuration options, such as the `src` list which tells it
Expand Down
13 changes: 8 additions & 5 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 All @@ -155,21 +160,19 @@ select = [
"UP", # pyupgrade
"YTT", # flake8-2020
]
extend-ignore = [
ignore = [
"PLR", # Design related pylint codes
"E501", # Line too long
"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
152 changes: 124 additions & 28 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,24 +82,21 @@ def check(pyproject: dict[str, Any], package: Traversable) -> bool | None:
if not package.joinpath("src").is_dir():
return None

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


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 @@ -99,10 +108,10 @@ def check(cls: type[RuffMixin], pyproject: dict[str, Any]) -> bool:
```
"""

match pyproject:
case {"tool": {"ruff": {"select": list(x)}}} | {
"tool": {"ruff": {"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 @@ -126,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}
15 changes: 8 additions & 7 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 All @@ -363,25 +369,20 @@ select = [
"NPY", # NumPy specific rules
"PD", # pandas-vet
]
extend-ignore = [
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 d152f42

Please sign in to comment.