diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1197685e..f0b9553a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,23 +6,6 @@ ci: exclude: "^({{cookiecutter\\.project_name}}|hooks/pre_gen_project.py$)" repos: - - repo: https://github.com/psf/black-pre-commit-mirror - rev: "23.10.0" - hooks: - - id: black-jupyter - - - repo: https://github.com/adamchainz/blacken-docs - rev: "1.16.0" - hooks: - - id: blacken-docs - additional_dependencies: [black==23.10.0] - - - repo: https://github.com/astral-sh/ruff-pre-commit - rev: "v0.1.1" - hooks: - - id: ruff - args: ["--fix", "--show-fixes"] - - repo: https://github.com/pre-commit/pre-commit-hooks rev: "v4.5.0" hooks: @@ -39,6 +22,19 @@ repos: - id: requirements-txt-fixer - id: trailing-whitespace + - repo: https://github.com/adamchainz/blacken-docs + rev: "1.16.0" + hooks: + - id: blacken-docs + additional_dependencies: [black==23.*] + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: "v0.1.2" + hooks: + - id: ruff + args: ["--fix", "--show-fixes"] + - id: ruff-format + - repo: https://github.com/pre-commit/pygrep-hooks rev: "v1.10.0" hooks: diff --git a/README.md b/README.md index a92aaaa3..5f37a36e 100644 --- a/README.md +++ b/README.md @@ -357,7 +357,7 @@ for family, grp in itertools.groupby(collected.checks.items(), key=lambda x: x[1 ### Pre-commit - [`PC100`](https://learn.scientific-python.org/development/guides/style#PC100): Has pre-commit-hooks -- [`PC110`](https://learn.scientific-python.org/development/guides/style#PC110): Uses black +- [`PC110`](https://learn.scientific-python.org/development/guides/style#PC110): Uses black or ruff-format - [`PC111`](https://learn.scientific-python.org/development/guides/style#PC111): Uses blacken-docs - [`PC140`](https://learn.scientific-python.org/development/guides/style#PC140): Uses mypy - [`PC160`](https://learn.scientific-python.org/development/guides/style#PC160): Uses codespell diff --git a/docs/pages/guides/style.md b/docs/pages/guides/style.md index 37909611..53971c33 100644 --- a/docs/pages/guides/style.md +++ b/docs/pages/guides/style.md @@ -79,7 +79,7 @@ ci: autoupdate_commit_msg: "chore: update pre-commit hooks" ``` -## Black +## Format {% rr PC110 %} [Black](https://black.readthedocs.io/en/latest/) is a popular auto-formatter from the Python Software Foundation. One of the main features of @@ -102,6 +102,8 @@ There are a _few_ options, mostly to enable/disable certain files, remove string normalization, and to change the line length, and those go in your `pyproject.toml` file. +{% tabs %} {% tab black Black %} + Here is the snippet to add Black to your `.pre-commit-config.yml`: ```yaml @@ -124,6 +126,34 @@ Here is the snippet to add Black to your `.pre-commit-config.yml`: {% enddetails %} +{% endtab %} {% tab ruff Ruff-format %} + +Ruff, the powerful Rust-based linter, also has a formatter that is designed to +look like Black, but run 30x faster. Here is the snippet to add Black to your +`.pre-commit-config.yml` (combine with Ruff below): + +```yaml +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: "v0.1.0" + hooks: + - id: ruff-format +``` + +{% details You can add a Ruff badge to your repo as well %} + +[![Code style: Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/format.json))](https://github.com/astral-sh/ruff) + +```md +[![Code style: Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/format.json))](https://github.com/astral-sh/ruff) +``` + +```rst +.. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/format.json + :target: https://github.com/astral-sh/ruff +``` + +{% endtab %} {% endtabs %} + In _very_ specific situations, you may want to retain special formatting. After carefully deciding that it is a special use case, you can use `# fmt: on` and `# fmt: off` around a code block to have it keep custom formatting. _Always_ @@ -179,15 +209,13 @@ src = ["src"] exclude = [] [tool.ruff.lint] -select = [ - "E", "F", "W", # flake8 +extend-select = [ "B", # flake8-bugbear "I", # isort "ARG", # flake8-unused-arguments "C4", # flake8-comprehensions "EM", # flake8-errmsg "ICN", # flake8-import-conventions - "ISC", # flake8-implicit-str-concat "G", # flake8-logging-format "PGH", # pygrep-hooks "PIE", # flake8-pie @@ -203,6 +231,8 @@ select = [ "EXE", # flake8-executable "NPY", # NumPy specific rules "PD", # pandas-vet + "FURB", # referb + "PYI", # flake8-pyi ] ignore = [ "PLR", # Design related pylint codes @@ -255,7 +285,8 @@ without this). Here are some good error codes to enable on most (but not all!) projects: - `E`, `F`, `W`: These are the standard flake8 checks, classic checks that have - stood the test of time. + stood the test of time. Not required if you use `extend-select` (`W` not + needed if you use a formatter) - `B`: This finds patterns that are very bug-prone. {% rr RF101 %} - `I`: This sorts your includes. There are multiple benefits, such as smaller diffs, fewer conflicts, a way to auto-inject `__future__` imports, and easier @@ -270,7 +301,7 @@ Here are some good error codes to enable on most (but not all!) projects: error string directly in the exception you are throwing, producing a cleaner traceback without duplicating the error string. - `ISC`: Checks for implicit string concatenation, which can help catch mistakes - with missing commas. + with missing commas. (May collide with formatter) - `PGH`: Checks for patterns, such as type ignores or noqa's without a specific error code. - `PL`: A set of four code groups that cover some (200 or so out of 600 rules) @@ -284,9 +315,28 @@ Here are some good error codes to enable on most (but not all!) projects: - `T20`: Disallow `print` in your code (built on the assumption that it's a common debugging tool). - `UP`: Upgrade old Python syntax to your `target-version`. {% rr RF103 %} +- `FURB`: From the refurb tool, a collection of helpful cleanups. +- `PYI`: Typing related checks A few others small ones are included above, and there are even more available in -Ruff. +Ruff. You can use `ALL` to get them all, then ignore the ones you want to +ignore. New checks go into `--preview` before being activated in a minor +release. + +{% details You can add a Ruff badge to your repo as well %} + +[![Code style: Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json))](https://github.com/astral-sh/ruff) + +```md +[![Code style: Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json))](https://github.com/astral-sh/ruff) +``` + +```rst +.. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json + :target: https://github.com/astral-sh/ruff +``` + +{% enddetails %} {% details Separate tools that Ruff replaces %} diff --git a/pyproject.toml b/pyproject.toml index 3fbb6346..eb2e97cf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -138,15 +138,13 @@ src = ["src"] exclude = [] [tool.ruff.lint] -select = [ - "E", "F", "W", # flake8 +extend-select = [ "B", # flake8-bugbear "I", # isort "ARG", # flake8-unused-arguments "C4", # flake8-comprehensions "EM", # flake8-errmsg "ICN", # flake8-import-conventions - "ISC", # flake8-implicit-str-concat "PGH", # pygrep-hooks "PIE", # flake8-pie "PL", # pylint diff --git a/src/sp_repo_review/checks/general.py b/src/sp_repo_review/checks/general.py index 6525d0d0..c7b7674b 100644 --- a/src/sp_repo_review/checks/general.py +++ b/src/sp_repo_review/checks/general.py @@ -12,7 +12,9 @@ class General: class PY001(General): + "Has a pyproject.toml" + url = mk_url("packaging-simple") @staticmethod @@ -26,6 +28,7 @@ def check(package: Traversable) -> bool: class PY002(General): "Has a README.(md|rst) file" + url = mk_url("packaging-simple") @staticmethod @@ -39,6 +42,7 @@ def check(root: Traversable) -> bool: class PY003(General): "Has a LICENSE* file" + url = mk_url("packaging-simple") @staticmethod @@ -49,6 +53,7 @@ def check(package: Traversable) -> bool: class PY004(General): "Has docs folder" + url = mk_url("packaging-simple") @staticmethod @@ -59,6 +64,7 @@ def check(package: Traversable) -> bool: class PY005(General): "Has tests folder" + url = mk_url("packaging-simple") @staticmethod @@ -79,6 +85,7 @@ def check(package: Traversable) -> bool: class PY006(General): "Has pre-commit config" + url = mk_url("style") @staticmethod @@ -89,6 +96,7 @@ def check(root: Traversable) -> bool: class PY007(General): "Supports an easy task runner (nox or tox)" + url = mk_url("tasks") @staticmethod diff --git a/src/sp_repo_review/checks/github.py b/src/sp_repo_review/checks/github.py index 37302c35..b09edcf5 100644 --- a/src/sp_repo_review/checks/github.py +++ b/src/sp_repo_review/checks/github.py @@ -40,6 +40,7 @@ class GitHub: class GH100(GitHub): "Has GitHub Actions config" + url = mk_url("gha-basic") @staticmethod @@ -54,6 +55,7 @@ def check(workflows: dict[str, Any]) -> bool: class GH101(GitHub): "Has nice names" + requires = {"GH100"} url = mk_url("gha-basic") @@ -68,6 +70,7 @@ def check(workflows: dict[str, Any]) -> bool: class GH102(GitHub): "Auto-cancel on repeated PRs" + requires = {"GH100"} url = mk_url("gha-basic") @@ -87,6 +90,7 @@ def check(workflows: dict[str, Any]) -> bool: class GH103(GitHub): "At least one workflow with manual dispatch trigger" + requires = {"GH100"} url = mk_url("gha-basic") @@ -105,6 +109,7 @@ def check(workflows: dict[str, Any]) -> bool: class GH200(GitHub): "Maintained by Dependabot" + url = mk_url("gha-basic") @staticmethod @@ -128,6 +133,7 @@ def check(dependabot: dict[str, Any]) -> bool: class GH210(GitHub): "Maintains the GitHub action versions with Dependabot" + requires = {"GH200"} url = mk_url("gha-basic") @@ -154,6 +160,7 @@ def check(dependabot: dict[str, Any]) -> bool: class GH211(GitHub): "Do not pin core actions as major versions" + requires = {"GH200", "GH210"} # Currently listing both helps - TODO: remove GH200 url = mk_url("gha-basic") diff --git a/src/sp_repo_review/checks/precommit.py b/src/sp_repo_review/checks/precommit.py index edb5a9e0..c9ff8592 100644 --- a/src/sp_repo_review/checks/precommit.py +++ b/src/sp_repo_review/checks/precommit.py @@ -41,17 +41,32 @@ def check(cls, precommit: dict[str, Any]) -> bool | None | str: class PC100(PreCommit): "Has pre-commit-hooks" + repo = "https://github.com/pre-commit/pre-commit-hooks" class PC110(PreCommit): - "Uses black" + "Uses black or ruff-format" + repo = "https://github.com/psf/black-pre-commit-mirror" renamed = "https://github.com/psf/black" + alternate = "https://github.com/astral-sh/ruff-pre-commit" + + @classmethod + def check(cls, precommit: dict[str, Any]) -> bool | None | str: + "Must have `{self.repo}` or `{self.alternate}` + `id: ruff-format` in `.pre-commit-config.yaml`" + for repo in precommit.get("repos", {}): + if repo.get("repo", "").lower() == cls.alternate and any( + hook.get("id", "") == "ruff-format" for hook in repo.get("hooks", {}) + ): + return True + + return super().check(precommit) class PC111(PreCommit): "Uses blacken-docs" + requires = {"PY006", "PC110"} repo = "https://github.com/adamchainz/blacken-docs" renamed = "https://github.com/asottile/blacken-docs" @@ -59,32 +74,38 @@ class PC111(PreCommit): class PC190(PreCommit): "Uses Ruff" + repo = "https://github.com/astral-sh/ruff-pre-commit" renamed = "https://github.com/charliermarsh/ruff-pre-commit" class PC140(PreCommit): "Uses mypy" + repo = "https://github.com/pre-commit/mirrors-mypy" class PC160(PreCommit): "Uses codespell" + repo = "https://github.com/codespell-project/codespell" class PC170(PreCommit): "Uses PyGrep hooks (only needed if RST present)" + repo = "https://github.com/pre-commit/pygrep-hooks" class PC180(PreCommit): "Uses prettier" + repo = "https://github.com/pre-commit/mirrors-prettier" class PC191(PreCommit): "Ruff show fixes if fixes enabled" + requires = {"PC190"} repo = "https://github.com/astral-sh/ruff-pre-commit" diff --git a/src/sp_repo_review/checks/pyproject.py b/src/sp_repo_review/checks/pyproject.py index db139c25..003976c7 100644 --- a/src/sp_repo_review/checks/pyproject.py +++ b/src/sp_repo_review/checks/pyproject.py @@ -73,6 +73,7 @@ def check(pyproject: dict[str, Any]) -> bool: class PP302(PyProject): "Sets a minimum pytest to at least 6" + requires = {"PP301"} url = mk_url("pytest") @@ -93,6 +94,7 @@ def check(pyproject: dict[str, Any]) -> bool: class PP303(PyProject): "Sets the test paths" + requires = {"PP301"} url = mk_url("pytest") @@ -112,6 +114,7 @@ def check(pyproject: dict[str, Any]) -> bool: class PP304(PyProject): "Sets the log level in pytest" + requires = {"PP301"} url = mk_url("pytest") @@ -132,6 +135,7 @@ def check(pyproject: dict[str, Any]) -> bool: class PP305(PyProject): "Specifies xfail_strict" + requires = {"PP301"} url = mk_url("pytest") @@ -152,6 +156,7 @@ def check(pyproject: dict[str, Any]) -> bool: class PP306(PyProject): "Specifies strict config" + requires = {"PP301"} url = mk_url("pytest") @@ -172,6 +177,7 @@ def check(pyproject: dict[str, Any]) -> bool: class PP307(PyProject): "Specifies strict markers" + requires = {"PP301"} url = mk_url("pytest") @@ -192,6 +198,7 @@ def check(pyproject: dict[str, Any]) -> bool: class PP308(PyProject): "Specifies useful pytest summary" + requires = {"PP301"} url = mk_url("pytest") @@ -211,6 +218,7 @@ def check(pyproject: dict[str, Any]) -> bool: class PP309(PyProject): "Filter warnings specified" + requires = {"PP301"} url = mk_url("pytest") diff --git a/src/sp_repo_review/checks/ruff.py b/src/sp_repo_review/checks/ruff.py index d3a40103..2b57fff0 100644 --- a/src/sp_repo_review/checks/ruff.py +++ b/src/sp_repo_review/checks/ruff.py @@ -34,6 +34,7 @@ class Ruff: class RF001(Ruff): "Has Ruff config" + requires = {"PY001"} @staticmethod @@ -109,9 +110,11 @@ def check(cls: type[RF1xxMixin], ruff: dict[str, Any]) -> bool: """ match ruff: - case {"lint": {"select": list(x)} | {"extend-select": list(x)}} | { - "select": list(x) - } | {"extend-select": list(x)}: + 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 @@ -119,18 +122,21 @@ def check(cls: type[RF1xxMixin], ruff: dict[str, Any]) -> bool: class RF101(RF1xx): "Bugbear must be selected" + code = "B" name = "flake8-bugbear" class RF102(RF1xx): "isort must be selected" + code = "I" name = "isort" class RF103(RF1xx): "pyupgrade must be selected" + code = "UP" name = "pyupgrade" @@ -155,9 +161,10 @@ class RF201(RF2xx): @staticmethod def iter_check(ruff: dict[str, Any]) -> Generator[str, None, None]: match ruff: - case {"extend-unfixable": object()} | { - "lint": {"extend-unfixable": object()} - }: + 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)" diff --git a/{{cookiecutter.project_name}}/pyproject.toml b/{{cookiecutter.project_name}}/pyproject.toml index a0257377..792496ef 100644 --- a/{{cookiecutter.project_name}}/pyproject.toml +++ b/{{cookiecutter.project_name}}/pyproject.toml @@ -344,15 +344,13 @@ target-version = "py38" {%- endif %} [tool.ruff.lint] -select = [ - "E", "F", "W", # flake8 +extend-select = [ "B", # flake8-bugbear "I", # isort "ARG", # flake8-unused-arguments "C4", # flake8-comprehensions "EM", # flake8-errmsg "ICN", # flake8-import-conventions - "ISC", # flake8-implicit-str-concat "G", # flake8-logging-format "PGH", # pygrep-hooks "PIE", # flake8-pie