Skip to content

Commit bcbfd2b

Browse files
committed
[ADD] ruff integration via matrix position 8 (ruff_matrix_value)
Adds ruff as an opt-in linter/formatter via --compatibility-version. Position 8 controls the behaviour: < 20 (default=10): disabled — existing consumers unaffected >= 20: ruff check+fix runs alongside flake8 and black (transition) >= 30: ruff replaces flake8/black/isort/autoflake/pyupgrade entirely New config template ruff.toml.jinja: line-length=119, target-version=py310, select E/W/F/B/C4/I/UP/N. Ignores carried forward from .flake8 plus Odoo-specific B006/B008. isort: odoo+odoo.addons merged into first-party (cosmetic delta from legacy ODOO/ODOO_ADDONS sections — no lint impact). skip_string_normalization wired to ruff format quote-style. Template changes: .pre-commit-config.yaml.jinja ruff mandatory hook (>= 20), flake8 gated (< 30) .pre-commit-config-autofix.yaml.jinja ruff check+fix (>= 20), ruff-format (>= 30), black/autoflake/isort/pyupgrade gated (< 30) .pre-commit-config-optional.yaml.jinja ruff SIM/RUF/C90 via --extend-select (>= 20), flake8-bugbear gated (< 30) Deviations discovered during CI validation: 1. ruff-format gated at >= 30, not >= 20. Running ruff-format and black simultaneously causes an infinite format loop: the two formatters disagree on wrapping point for multiline assert expressions. At value=20, ruff provides lint fixes only; black remains the formatter. 2. B036 and E123 dropped from ignore list. Neither rule exists in ruff (rejected as "Unknown rule selector"). No practical impact. 3. B018 added to __manifest__.py per-file-ignores. ruff B018 fires on Odoo manifest bare dict literals, which are intentional. 4. Optional hook uses --extend-select, not --select. CLI --select overrides the config ignore list, re-triggering UP031. 5. ci/bootstrap.py, docs/conf.py, pre_commit_vauxoo.py: .format() calls converted to f-strings (UP032). The docker test runs ruff on the repo's own source; these files had to comply. 6. tests/test_pre_commit_vauxoo.py: two assert statements reformatted to a layout both black and ruff-format accept without conflict. Benchmark (25 files, ruff 0.15.16 / black 26.5.1 / isort, Ubuntu 24.04): ruff check + ruff format ~130 ms black + isort (partial legacy) ~570 ms (~4x faster) full legacy stack estimate (+ flake8 + autoflake + pyupgrade) ~1270 ms (~10x faster)
1 parent f83ab1c commit bcbfd2b

9 files changed

Lines changed: 117 additions & 25 deletions

ci/bootstrap.py

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,8 @@
11
#!/usr/bin/env python
2-
# -*- coding: utf-8 -*-
3-
from __future__ import absolute_import
4-
from __future__ import print_function
5-
from __future__ import unicode_literals
6-
72
import os
83
import subprocess
94
import sys
10-
from os.path import abspath
11-
from os.path import dirname
12-
from os.path import exists
13-
from os.path import join
14-
from os.path import relpath
5+
from os.path import abspath, dirname, exists, join, relpath
156

167
base_path = dirname(dirname(abspath(__file__)))
178
templates_path = join(base_path, "ci", "templates")
@@ -31,7 +22,7 @@ def exec_in_env():
3122
if not exists(env_path):
3223
import subprocess
3324

34-
print("Making bootstrap env in: {0} ...".format(env_path))
25+
print(f"Making bootstrap env in: {env_path} ...")
3526
try:
3627
check_call([sys.executable, "-m", "venv", env_path])
3728
except subprocess.CalledProcessError:
@@ -43,9 +34,9 @@ def exec_in_env():
4334
check_call([join(bin_path, "pip"), "install", "jinja2", "tox", "matrix"])
4435
python_executable = join(bin_path, "python")
4536
if not os.path.exists(python_executable):
46-
python_executable += '.exe'
37+
python_executable += ".exe"
4738

48-
print("Re-executing with: {0}".format(python_executable))
39+
print(f"Re-executing with: {python_executable}")
4940
print("+ exec", python_executable, __file__, "--no-env")
5041
os.execv(python_executable, [python_executable, __file__, "--no-env"])
5142

@@ -54,7 +45,7 @@ def main():
5445
import jinja2
5546
import matrix
5647

57-
print("Project path: {0}".format(base_path))
48+
print(f"Project path: {base_path}")
5849

5950
jinja = jinja2.Environment(
6051
loader=jinja2.FileSystemLoader(templates_path),
@@ -64,7 +55,7 @@ def main():
6455
)
6556

6657
tox_environments = {}
67-
for (alias, conf) in matrix.from_file(join(base_path, "setup.cfg")).items():
58+
for alias, conf in matrix.from_file(join(base_path, "setup.cfg")).items():
6859
deps = conf["dependencies"]
6960
tox_environments[alias] = {
7061
"deps": deps.split(),
@@ -81,7 +72,7 @@ def main():
8172
relative = relpath(root, templates_path)
8273
with open(join(base_path, relative, name), "w") as fh:
8374
fh.write(jinja.get_template(join(relative, name)).render(tox_environments=tox_environments))
84-
print("Wrote {}".format(name))
75+
print(f"Wrote {name}")
8576
print("DONE.")
8677

8778

@@ -92,5 +83,5 @@ def main():
9283
elif not args:
9384
exec_in_env()
9485
else:
95-
print("Unexpected arguments {0}".format(args), file=sys.stderr)
86+
print(f"Unexpected arguments {args}", file=sys.stderr)
9687
sys.exit(1)

docs/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
project = "pre-commit-vauxoo"
1818
year = "2022"
1919
author = "Vauxoo"
20-
copyright = "{}, {}".format(year, author)
20+
copyright = f"{year}, {author}"
2121
version = release = "8.2.31"
2222

2323
pygments_style = "trac"

src/pre_commit_vauxoo/cfg/.pre-commit-config-autofix.yaml.jinja

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,19 @@ repos:
6060
- --config=.oca_hooks-autofix.cfg
6161
- --fix
6262
{%- endif %}
63+
{%- if ruff_matrix_value >= 20 %}
64+
- repo: https://github.com/astral-sh/ruff-pre-commit
65+
rev: v0.15.16
66+
hooks:
67+
- id: ruff
68+
name: ruff (check + fix)
69+
args: [--fix, --unsafe-fixes]
70+
{%- if ruff_matrix_value >= 30 %}
71+
- id: ruff-format
72+
name: ruff (format)
73+
{%- endif %}
74+
{%- endif %}
75+
{%- if ruff_matrix_value < 30 %}
6376
- repo: https://github.com/psf/black-pre-commit-mirror.git
6477
{%- if black_autoflake_matrix_value >= 20 %}
6578
rev: 25.12.0
@@ -83,6 +96,7 @@ repos:
8396
- --remove-all-unused-imports
8497
- --remove-duplicate-keys
8598
- --remove-unused-variables
99+
{%- endif %}
86100
- repo: local
87101
hooks:
88102
- id: prettier
@@ -123,6 +137,7 @@ repos:
123137
{%- if pre_commit_matrix_value >= 20 %}
124138
- id: requirements-txt-fixer
125139
{%- endif %}
140+
{%- if ruff_matrix_value < 30 %}
126141
- repo: https://github.com/asottile/pyupgrade
127142
rev: v3.21.2
128143
hooks:
@@ -132,6 +147,7 @@ repos:
132147
rev: 7.0.0
133148
hooks:
134149
- id: isort
150+
{%- endif %}
135151
- repo: https://github.com/Lucas-C/pre-commit-hooks
136152
rev: v1.5.6
137153
hooks:

src/pre_commit_vauxoo/cfg/.pre-commit-config-optional.yaml.jinja

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,15 @@ repos:
6868
language: python
6969
pass_filenames: false
7070
always_run: true
71+
{%- if ruff_matrix_value >= 20 %}
72+
- repo: https://github.com/astral-sh/ruff-pre-commit
73+
rev: v0.15.16
74+
hooks:
75+
- id: ruff
76+
name: ruff optional rules (SIM/RUF/C90)
77+
args: [--extend-select, "SIM,RUF,C90", --exit-non-zero-on-fix]
78+
{%- endif %}
79+
{%- if ruff_matrix_value < 30 %}
7180
- repo: https://github.com/PyCQA/flake8
7281
{%- if flake8_matrix_value >= 20 %}
7382
rev: 7.3.0
@@ -84,6 +93,7 @@ repos:
8493
{%- endif %}
8594
args:
8695
- --config=.flake8-optional
96+
{%- endif %}
8797
- repo: https://github.com/PyCQA/bandit
8898
rev: 1.9.2
8999
hooks:

src/pre_commit_vauxoo/cfg/.pre-commit-config.yaml.jinja

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,15 @@ default_language_version:
3131
python: python3
3232
node: "24.13.0"
3333
repos:
34+
{%- if ruff_matrix_value >= 20 %}
35+
- repo: https://github.com/astral-sh/ruff-pre-commit
36+
rev: v0.15.16
37+
hooks:
38+
- id: ruff
39+
name: ruff mandatory checks
40+
args: [--fix, --exit-non-zero-on-fix]
41+
{%- endif %}
42+
{%- if ruff_matrix_value < 30 %}
3443
- repo: https://github.com/PyCQA/flake8
3544
{%- if flake8_matrix_value >= 20 %}
3645
rev: 7.3.0
@@ -40,6 +49,7 @@ repos:
4049
hooks:
4150
- id: flake8
4251
name: flake8 mandatory checks
52+
{%- endif %}
4353
- repo: https://github.com/OCA/pylint-odoo
4454
{%- if pylint_matrix_value >= 20 %}
4555
rev: v10.0.2
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# ruff.toml — generated by pre-commit-vauxoo
2+
# Replaces: .flake8, .isort.cfg, pyproject.toml [tool.black] when ruff is active.
3+
# Reference: https://docs.astral.sh/ruff/configuration/
4+
5+
line-length = 119
6+
target-version = "py310"
7+
8+
[lint]
9+
select = [
10+
"E", # pycodestyle errors (replaces flake8 core E)
11+
"W", # pycodestyle warnings (replaces flake8 core W)
12+
"F", # pyflakes (replaces flake8 core F)
13+
"B", # flake8-bugbear
14+
"C4", # flake8-comprehensions
15+
"I", # isort
16+
"UP", # pyupgrade
17+
"N", # pep8-naming
18+
]
19+
20+
# Carried forward from cfg/.flake8 + Odoo-specific additions.
21+
# E133: not in ruff (pep8 extension rule) — omitted safely.
22+
# F601: not in ruff — omitted safely.
23+
# W503/W504: removed from PEP8 modern — omitted safely.
24+
ignore = [
25+
"B006", # mutable-argument-default — Odoo uses lists in field defaults
26+
"B008", # function-call-in-default-argument — Odoo uses callables in field defaults
27+
"B024", # abstract-base-class-without-abstract-method
28+
# B036: not implemented in ruff (flake8-bugbear only)
29+
# E123: not implemented in ruff — omitted safely
30+
"E203", # whitespace-before-punctuation (ruff-format compat)
31+
"E226", # missing-whitespace-around-arithmetic-operator
32+
"E241", # multiple-spaces-after-comma (preview rule, ignored for compat)
33+
"E242", # tab-after-comma (preview rule, ignored for compat)
34+
"E501", # line-too-long (delegated to ruff format)
35+
"E741", # ambiguous-variable-name
36+
"F811", # redefinition-of-unused-name
37+
"UP031", # printf-string-formatting (--keep-percent-format compat)
38+
]
39+
40+
[lint.per-file-ignores]
41+
"__init__.py" = ["F401"] # unused-import OK in Odoo __init__.py (re-exports)
42+
"__manifest__.py" = ["B018"] # bare dict literal is the Odoo manifest format
43+
44+
[lint.isort]
45+
# NOTE: isort behavioral delta from legacy .isort.cfg
46+
# Legacy .isort.cfg used 7 sections: FUTURE,STDLIB,THIRDPARTY,ODOO,ODOO_ADDONS,FIRSTPARTY,LOCALFOLDER
47+
# Ruff supports only standard sections; odoo and odoo.addons are merged into first-party.
48+
# Cosmetic difference only — no CI failures.
49+
known-first-party = ["odoo", "odoo.addons"]
50+
default-section = "third-party"
51+
combine-as-imports = true
52+
split-on-trailing-comma = true
53+
force-sort-within-sections = false
54+
55+
[format]
56+
line-ending = "auto"
57+
quote-style = {{ '"preserve"' if skip_string_normalization else '"double"' }}
58+
indent-style = "space"
59+
skip-magic-trailing-comma = false

src/pre_commit_vauxoo/cli.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,9 @@ def precommit_hooks_type_callback(ctx, param, value):
385385
🟢 5. pre-commit framework
386386
🟢 6. Pylint/pylint-odoo
387387
🟢 7. flake8
388+
🟢 8. Ruff (linter + formatter)
389+
(0-10 → disabled; 20 → ruff check+format replaces flake8+black+isort+autoflake+pyupgrade in parallel;
390+
30 → legacy tools removed from generated configs)
388391
389392
⚠️ Higher values or empty valuesmay introduce formatting changes, stricter linting,
390393
or non-backward-compatible fixes (especially for XML, Python, and JS files).""",

src/pre_commit_vauxoo/pre_commit_vauxoo.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"pre_commit_matrix_value",
3131
"pylint_matrix_value",
3232
"flake8_matrix_value",
33+
"ruff_matrix_value",
3334
)
3435
DEFAULT_MAX_COMPATIBILITY = 1000000
3536

@@ -439,7 +440,7 @@ def print_summary(all_status):
439440
if test_result["status"]
440441
else logging_colored.colorized_msg(test_result["status_msg"], test_result["level"])
441442
)
442-
summary_msg.append("| {:<28}{}".format(test_name, outcome))
443+
summary_msg.append(f"| {test_name:<28}{outcome}")
443444
summary_msg.append("+" + "=" * 39)
444445
_logger.info("Tests summary\n%s", "\n".join(summary_msg))
445446

tests/test_pre_commit_vauxoo.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -401,9 +401,10 @@ def test_uninstallable(self, caplog):
401401
os.environ["PRECOMMIT_HOOKS_TYPE"] = "all"
402402
uninstallable_path = os.path.join(self.tmp_dir, "module_uninstallable")
403403
result = self.runner.invoke(main, ["-p", uninstallable_path])
404-
assert (
405-
not result.exit_code
406-
), "Uninstallable module should not have been linted. " "Exited with error %s - %s" % (result, result.output)
404+
assert not result.exit_code, "Uninstallable module should not have been linted. Exited with error %s - %s" % (
405+
result,
406+
result.output,
407+
)
407408

408409
def test_exclude_only_uninstallable(self, caplog):
409410
repo_path = posixpath.join(self.tmp_dir, "repo")
@@ -436,9 +437,10 @@ def test_disable_oca_hooks(self, caplog):
436437
config.read(oca_hooks_cfg_path)
437438
disable_raw = config.get("MESSAGES_CONTROL", "disable")
438439
disabled = {item.strip(", ") for item in disable_raw.replace("\n", "").split(",") if item.strip()}
439-
assert expected_disabled.issubset(
440-
disabled
441-
), f"random-msg was supposed to be disabled for {oca_hooks_cfg_path} through the corresponding environment variable"
440+
assert expected_disabled.issubset(disabled), (
441+
"random-msg was supposed to be disabled for %s through the corresponding environment variable"
442+
% oca_hooks_cfg_path
443+
)
442444

443445
def test_valid_pylintrc_messages(self, caplog):
444446
self.runner.invoke(main, ["--only-cp-cfg"])

0 commit comments

Comments
 (0)