lint-roller 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lint_roller-0.1.0/LICENSE +17 -0
- lint_roller-0.1.0/PKG-INFO +31 -0
- lint_roller-0.1.0/README.rst +13 -0
- lint_roller-0.1.0/pyproject.toml +194 -0
- lint_roller-0.1.0/setup.cfg +4 -0
- lint_roller-0.1.0/src/lint_roller/__init__.py +0 -0
- lint_roller-0.1.0/src/lint_roller/cli.py +334 -0
- lint_roller-0.1.0/src/lint_roller/diff.py +83 -0
- lint_roller-0.1.0/src/lint_roller/flake8.py +240 -0
- lint_roller-0.1.0/src/lint_roller/git.py +162 -0
- lint_roller-0.1.0/src/lint_roller/lint.py +325 -0
- lint_roller-0.1.0/src/lint_roller/py.typed +0 -0
- lint_roller-0.1.0/src/lint_roller/ruff.py +616 -0
- lint_roller-0.1.0/src/lint_roller/tests/__init__.py +0 -0
- lint_roller-0.1.0/src/lint_roller/tests/conftest.py +78 -0
- lint_roller-0.1.0/src/lint_roller/tests/helpers.py +78 -0
- lint_roller-0.1.0/src/lint_roller/tests/test_cli.py +180 -0
- lint_roller-0.1.0/src/lint_roller/tests/test_diff.py +325 -0
- lint_roller-0.1.0/src/lint_roller/tests/test_flake8.py +108 -0
- lint_roller-0.1.0/src/lint_roller/tests/test_git.py +42 -0
- lint_roller-0.1.0/src/lint_roller/tests/test_lint.py +22 -0
- lint_roller-0.1.0/src/lint_roller/tests/test_ruff_check.py +523 -0
- lint_roller-0.1.0/src/lint_roller/tests/test_ruff_format.py +187 -0
- lint_roller-0.1.0/src/lint_roller/tests/test_toml.py +35 -0
- lint_roller-0.1.0/src/lint_roller/tests/test_workspace.py +22 -0
- lint_roller-0.1.0/src/lint_roller/toml.py +72 -0
- lint_roller-0.1.0/src/lint_roller/workspace.py +183 -0
- lint_roller-0.1.0/src/lint_roller.egg-info/PKG-INFO +31 -0
- lint_roller-0.1.0/src/lint_roller.egg-info/SOURCES.txt +31 -0
- lint_roller-0.1.0/src/lint_roller.egg-info/dependency_links.txt +1 -0
- lint_roller-0.1.0/src/lint_roller.egg-info/entry_points.txt +2 -0
- lint_roller-0.1.0/src/lint_roller.egg-info/requires.txt +11 -0
- lint_roller-0.1.0/src/lint_roller.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
2
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
3
|
+
the Software without restriction, including without limitation the rights to
|
|
4
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
5
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
|
6
|
+
so, subject to the following conditions:
|
|
7
|
+
|
|
8
|
+
The above copyright notice and this permission notice shall be included in all
|
|
9
|
+
copies or substantial portions of the Software.
|
|
10
|
+
|
|
11
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
12
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
13
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
14
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
15
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
16
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
17
|
+
SOFTWARE.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lint-roller
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Check changes in a code repository for linting violations to iteratively improve them.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Requires-Python: >=3.14
|
|
7
|
+
Description-Content-Type: text/x-rst
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: attrs
|
|
10
|
+
Requires-Dist: click
|
|
11
|
+
Requires-Dist: colorama; platform_system == "Windows"
|
|
12
|
+
Requires-Dist: match_diff_lines>=0.3.0
|
|
13
|
+
Requires-Dist: structlog
|
|
14
|
+
Requires-Dist: wcmatch
|
|
15
|
+
Provides-Extra: tracebacks
|
|
16
|
+
Requires-Dist: rich; extra == "tracebacks"
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
===========
|
|
20
|
+
lint-roller
|
|
21
|
+
===========
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
Changelog
|
|
25
|
+
=========
|
|
26
|
+
|
|
27
|
+
0.1.0 - 2026-03-31
|
|
28
|
+
------------------
|
|
29
|
+
|
|
30
|
+
* Initial release.
|
|
31
|
+
[fschulze]
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "lint-roller"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Check changes in a code repository for linting violations to iteratively improve them."
|
|
5
|
+
readme = "README.rst"
|
|
6
|
+
requires-python = ">=3.14"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
dependencies = [
|
|
10
|
+
"attrs",
|
|
11
|
+
"click",
|
|
12
|
+
"colorama;platform_system=='Windows'",
|
|
13
|
+
"match_diff_lines>=0.3.0",
|
|
14
|
+
"structlog",
|
|
15
|
+
"wcmatch",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.scripts]
|
|
19
|
+
lint-roller = "lint_roller.cli:cli"
|
|
20
|
+
|
|
21
|
+
[project.optional-dependencies]
|
|
22
|
+
tracebacks = ["rich"]
|
|
23
|
+
|
|
24
|
+
[dependency-groups]
|
|
25
|
+
dev = [
|
|
26
|
+
"PdbEditorSupport",
|
|
27
|
+
"rich",
|
|
28
|
+
{ include-group = "coverage" },
|
|
29
|
+
{ include-group = "flake8" },
|
|
30
|
+
{ include-group = "mypy" },
|
|
31
|
+
{ include-group = "pytest" },
|
|
32
|
+
{ include-group = "tombi" },
|
|
33
|
+
{ include-group = "tox" },
|
|
34
|
+
]
|
|
35
|
+
coverage = [
|
|
36
|
+
"coverage",
|
|
37
|
+
]
|
|
38
|
+
flake8 = [
|
|
39
|
+
"flake8",
|
|
40
|
+
]
|
|
41
|
+
mypy = [
|
|
42
|
+
"mypy",
|
|
43
|
+
{ include-group = "pytest" },
|
|
44
|
+
]
|
|
45
|
+
pytest = [
|
|
46
|
+
"pytest",
|
|
47
|
+
"pytest-asyncio",
|
|
48
|
+
"tomli-w",
|
|
49
|
+
]
|
|
50
|
+
ruff = [
|
|
51
|
+
"ruff",
|
|
52
|
+
]
|
|
53
|
+
tombi = [
|
|
54
|
+
"tombi",
|
|
55
|
+
]
|
|
56
|
+
tox = [
|
|
57
|
+
"tox",
|
|
58
|
+
"tox-uv",
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
[build-system]
|
|
62
|
+
requires = ["setuptools"]
|
|
63
|
+
|
|
64
|
+
[tool.coverage.run]
|
|
65
|
+
branch = true
|
|
66
|
+
parallel = true
|
|
67
|
+
relative_files = true
|
|
68
|
+
source_pkgs = ["lint_roller"]
|
|
69
|
+
source = [
|
|
70
|
+
"src/lint_roller/tests",
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
[tool.lint-roller]
|
|
74
|
+
start-rev = "main"
|
|
75
|
+
|
|
76
|
+
[tool.pytest]
|
|
77
|
+
addopts = [
|
|
78
|
+
"-W error::DeprecationWarning",
|
|
79
|
+
"-W once::ResourceWarning",
|
|
80
|
+
"-r a",
|
|
81
|
+
]
|
|
82
|
+
asyncio_mode = "auto"
|
|
83
|
+
|
|
84
|
+
[tool.ruff.lint]
|
|
85
|
+
select = ["ALL"]
|
|
86
|
+
ignore = [
|
|
87
|
+
"COM812", # missing-trailing-comma - disabled for ruff format
|
|
88
|
+
"D1", # undocumented-*
|
|
89
|
+
"D203", # incorrect-blank-line-before-class
|
|
90
|
+
"D213", # multi-line-summary-second-line
|
|
91
|
+
"E501", # line-too-long
|
|
92
|
+
"PLR2004", # magic-value-comparison
|
|
93
|
+
"SLF001", # private-member-access
|
|
94
|
+
"TID252", # flake8-tidy-imports.ban-relative-imports
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
[tool.ruff.lint.per-file-ignores]
|
|
98
|
+
"src/lint_roller/tests/*.py" = [
|
|
99
|
+
"INP001", # implicit-namespace-package
|
|
100
|
+
"PLC0415", # import-outside-top-level
|
|
101
|
+
"PLR0913", # too-many-arguments
|
|
102
|
+
"PLR0915", # too-many-statements
|
|
103
|
+
"S101", # assert
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
[tool.ruff.lint.isort]
|
|
107
|
+
case-sensitive = true
|
|
108
|
+
force-single-line = true
|
|
109
|
+
from-first = true
|
|
110
|
+
lines-after-imports = 2
|
|
111
|
+
no-sections = true
|
|
112
|
+
order-by-type = false
|
|
113
|
+
|
|
114
|
+
[tool.setuptools]
|
|
115
|
+
package-dir = { "" = "src" }
|
|
116
|
+
|
|
117
|
+
[tool.setuptools.packages.find]
|
|
118
|
+
where = ["src"]
|
|
119
|
+
|
|
120
|
+
[tool.tombi.format.rules]
|
|
121
|
+
indent-width = 4
|
|
122
|
+
|
|
123
|
+
[tool.tombi.lint.rules]
|
|
124
|
+
key-empty = "off"
|
|
125
|
+
|
|
126
|
+
[tool.tox]
|
|
127
|
+
env_list = [
|
|
128
|
+
"clean",
|
|
129
|
+
"coverage_combine",
|
|
130
|
+
"coverage_report",
|
|
131
|
+
"flake8",
|
|
132
|
+
"format",
|
|
133
|
+
"mypy",
|
|
134
|
+
{ product = [{ prefix = "py3", start = 14 }] },
|
|
135
|
+
"ruff",
|
|
136
|
+
"tombi-check",
|
|
137
|
+
"tombi-format",
|
|
138
|
+
]
|
|
139
|
+
|
|
140
|
+
[tool.tox.env_run_base]
|
|
141
|
+
base_python = ["python3.14"]
|
|
142
|
+
dependency_groups = ["coverage", "pytest"]
|
|
143
|
+
commands = [["coverage", "run", "-m", "pytest"]]
|
|
144
|
+
depends = ["clean"]
|
|
145
|
+
|
|
146
|
+
[tool.tox.env.clean]
|
|
147
|
+
dependency_groups = ["coverage"]
|
|
148
|
+
commands = [["coverage", "erase"]]
|
|
149
|
+
skip_install = true
|
|
150
|
+
|
|
151
|
+
[tool.tox.env.coverage_combine]
|
|
152
|
+
dependency_groups = ["coverage"]
|
|
153
|
+
commands = [["coverage", "combine"]]
|
|
154
|
+
depends = ["py3*"]
|
|
155
|
+
skip_install = true
|
|
156
|
+
|
|
157
|
+
[tool.tox.env.coverage_report]
|
|
158
|
+
dependency_groups = ["coverage"]
|
|
159
|
+
commands = [
|
|
160
|
+
["coverage", "html"],
|
|
161
|
+
["coverage", "report", "--fail-under=100"],
|
|
162
|
+
]
|
|
163
|
+
depends = ["coverage_combine"]
|
|
164
|
+
parallel_show_output = true
|
|
165
|
+
skip_install = true
|
|
166
|
+
|
|
167
|
+
[tool.tox.env.flake8]
|
|
168
|
+
dependency_groups = ["flake8"]
|
|
169
|
+
commands = [["flake8"]]
|
|
170
|
+
skip_install = true
|
|
171
|
+
|
|
172
|
+
[tool.tox.env.format]
|
|
173
|
+
dependency_groups = ["ruff"]
|
|
174
|
+
commands = [["ruff", "format", "--check"]]
|
|
175
|
+
skip_install = true
|
|
176
|
+
|
|
177
|
+
[tool.tox.env.mypy]
|
|
178
|
+
dependency_groups = ["mypy"]
|
|
179
|
+
commands = [["mypy"]]
|
|
180
|
+
|
|
181
|
+
[tool.tox.env.ruff]
|
|
182
|
+
dependency_groups = ["ruff"]
|
|
183
|
+
commands = [["ruff", "check"]]
|
|
184
|
+
skip_install = true
|
|
185
|
+
|
|
186
|
+
[tool.tox.env.tombi-check]
|
|
187
|
+
dependency_groups = ["tombi"]
|
|
188
|
+
commands = [["tombi", "check", "--error-on-warnings", "--quiet"]]
|
|
189
|
+
skip_install = true
|
|
190
|
+
|
|
191
|
+
[tool.tox.env.tombi-format]
|
|
192
|
+
dependency_groups = ["tombi"]
|
|
193
|
+
commands = [["tombi", "format", "--check", "--diff", "--quiet"]]
|
|
194
|
+
skip_install = true
|
|
File without changes
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .lint import BaseOutput
|
|
4
|
+
from .lint import LintResults
|
|
5
|
+
from .lint import LintResultsCoroutine
|
|
6
|
+
from .workspace import Workspace
|
|
7
|
+
from abc import abstractmethod
|
|
8
|
+
from attrs import define
|
|
9
|
+
from functools import cached_property
|
|
10
|
+
from functools import wraps
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from subprocess import CalledProcessError
|
|
13
|
+
from traceback import format_exception_only
|
|
14
|
+
from typing import TYPE_CHECKING
|
|
15
|
+
import asyncio
|
|
16
|
+
import click
|
|
17
|
+
import logging
|
|
18
|
+
import structlog
|
|
19
|
+
import sys
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from collections.abc import Awaitable
|
|
24
|
+
from collections.abc import Callable
|
|
25
|
+
from collections.abc import Iterator
|
|
26
|
+
from typing import Any
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
log = structlog.stdlib.get_logger()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def checkers_options[**P, R](f: Callable[P, R]) -> Callable[P, R]:
|
|
33
|
+
@click.option(
|
|
34
|
+
"--default-checks/--no-default-checks",
|
|
35
|
+
"run_default_checks",
|
|
36
|
+
default=None,
|
|
37
|
+
help="Run/omit default checks.",
|
|
38
|
+
)
|
|
39
|
+
@click.option(
|
|
40
|
+
"--flake8/--no-flake8",
|
|
41
|
+
"run_flake8",
|
|
42
|
+
default=None,
|
|
43
|
+
help="Run/omit flake8 checks.",
|
|
44
|
+
)
|
|
45
|
+
@click.option(
|
|
46
|
+
"--ruff-check/--no-ruff-check",
|
|
47
|
+
"run_ruff_check",
|
|
48
|
+
default=None,
|
|
49
|
+
help="Run/omit ruff check checks.",
|
|
50
|
+
)
|
|
51
|
+
@click.option(
|
|
52
|
+
"--ruff-format/--no-ruff-format",
|
|
53
|
+
"run_ruff_format",
|
|
54
|
+
default=None,
|
|
55
|
+
help="Run/omit ruff format checks.",
|
|
56
|
+
)
|
|
57
|
+
@wraps(f)
|
|
58
|
+
def wrapper_checkers_options(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
59
|
+
return f(*args, **kwargs)
|
|
60
|
+
|
|
61
|
+
return wrapper_checkers_options
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def common_output_options[**P, R](f: Callable[P, R]) -> Callable[P, R]:
|
|
65
|
+
@click.option(
|
|
66
|
+
"--show-full-diff/--hide-full-diff",
|
|
67
|
+
default=False,
|
|
68
|
+
show_default=True,
|
|
69
|
+
help="Show the full diff",
|
|
70
|
+
)
|
|
71
|
+
@click.option(
|
|
72
|
+
"--show-full-diff-ranges/--hide-full-diff-ranges",
|
|
73
|
+
default=False,
|
|
74
|
+
show_default=True,
|
|
75
|
+
help="Show line ranges of diff",
|
|
76
|
+
)
|
|
77
|
+
@click.option(
|
|
78
|
+
"--show-start-rev/--hide-start-rev",
|
|
79
|
+
default=True,
|
|
80
|
+
show_default=True,
|
|
81
|
+
help="Show determined start rev",
|
|
82
|
+
)
|
|
83
|
+
@click.option(
|
|
84
|
+
"--show-tool-versions/--hide-tool-versions",
|
|
85
|
+
default=False,
|
|
86
|
+
show_default=True,
|
|
87
|
+
help="Show versions of used tools",
|
|
88
|
+
)
|
|
89
|
+
@click.option("-v", "--verbose", count=True, help="Enables verbose mode")
|
|
90
|
+
@wraps(f)
|
|
91
|
+
def wrapper_common_output_options(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
92
|
+
return f(*args, **kwargs)
|
|
93
|
+
|
|
94
|
+
return wrapper_common_output_options
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def start_rev_option[**P, R](f: Callable[P, R]) -> Callable[P, R]:
|
|
98
|
+
@click.option(
|
|
99
|
+
"--start-rev",
|
|
100
|
+
metavar="REV",
|
|
101
|
+
help="The commit or symbolic name to check against. [default: main]",
|
|
102
|
+
)
|
|
103
|
+
@wraps(f)
|
|
104
|
+
def wrapper_start_rev_option(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
105
|
+
return f(*args, **kwargs)
|
|
106
|
+
|
|
107
|
+
return wrapper_start_rev_option
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@click.group()
|
|
111
|
+
def cli() -> None: ...
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@define(kw_only=True)
|
|
115
|
+
class Output(BaseOutput):
|
|
116
|
+
cli_show_full_diff: bool
|
|
117
|
+
cli_show_full_diff_ranges: bool
|
|
118
|
+
cli_show_start_rev: bool
|
|
119
|
+
cli_show_tool_versions: bool
|
|
120
|
+
verbose: int
|
|
121
|
+
|
|
122
|
+
def _iter_exceptions(self, e: BaseException) -> Iterator[BaseException]:
|
|
123
|
+
if isinstance(e, (BaseExceptionGroup, ExceptionGroup)):
|
|
124
|
+
for se in e.exceptions:
|
|
125
|
+
yield from self._iter_exceptions(se)
|
|
126
|
+
else:
|
|
127
|
+
yield e
|
|
128
|
+
|
|
129
|
+
def _log_exception(self, msg: str, e: BaseException) -> None:
|
|
130
|
+
if self.log_for(logging.INFO):
|
|
131
|
+
log.exception("%s:", msg, exc_info=e)
|
|
132
|
+
else:
|
|
133
|
+
log.error(
|
|
134
|
+
"%s (use -v for more details): %s",
|
|
135
|
+
msg,
|
|
136
|
+
"".join(format_exception_only(e)).rstrip(),
|
|
137
|
+
)
|
|
138
|
+
if isinstance(e, CalledProcessError):
|
|
139
|
+
if e.stdout: # pragma: no cover
|
|
140
|
+
click.echo(e.stdout)
|
|
141
|
+
if e.stderr: # pragma: no cover
|
|
142
|
+
click.echo(e.stderr)
|
|
143
|
+
|
|
144
|
+
def log_exception(self, e: BaseException) -> None:
|
|
145
|
+
for se in self._iter_exceptions(e):
|
|
146
|
+
msg = (
|
|
147
|
+
"A sub-process call failed"
|
|
148
|
+
if isinstance(se, CalledProcessError)
|
|
149
|
+
else "Exception"
|
|
150
|
+
)
|
|
151
|
+
self._log_exception(msg, se)
|
|
152
|
+
|
|
153
|
+
def log_for(self, level: int) -> bool:
|
|
154
|
+
return self.log_level <= level
|
|
155
|
+
|
|
156
|
+
@cached_property
|
|
157
|
+
def log_level(self) -> int:
|
|
158
|
+
if not self.verbose:
|
|
159
|
+
return logging.WARNING
|
|
160
|
+
if self.verbose > 1:
|
|
161
|
+
return logging.DEBUG
|
|
162
|
+
return logging.INFO
|
|
163
|
+
|
|
164
|
+
def log_start_rev(self, workspace: Workspace) -> None:
|
|
165
|
+
if self.show_start_rev():
|
|
166
|
+
click.echo(
|
|
167
|
+
f"Checking commits from {workspace.commit_hash} ({workspace.commit_names}) in {Path().absolute()}"
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
def log_versions(self, workspace: Workspace) -> None:
|
|
171
|
+
if self.show_tool_versions():
|
|
172
|
+
click.echo(f"git {workspace.git.version}")
|
|
173
|
+
|
|
174
|
+
async def log_full_python_diff(self, workspace: Workspace) -> None:
|
|
175
|
+
if self.show_full_diff():
|
|
176
|
+
click.echo((await workspace.full_python_diff_task).colored())
|
|
177
|
+
elif self.show_full_diff_ranges():
|
|
178
|
+
click.echo((await workspace.full_python_diff_task).format_ranges())
|
|
179
|
+
|
|
180
|
+
def show_fixes_for(self, _kind: str, /) -> bool:
|
|
181
|
+
return True
|
|
182
|
+
|
|
183
|
+
@property
|
|
184
|
+
def show_format_trigger_lines(self) -> bool:
|
|
185
|
+
return bool(self.verbose)
|
|
186
|
+
|
|
187
|
+
def show_full_diff(self) -> bool:
|
|
188
|
+
return self.cli_show_full_diff or self.log_for(logging.DEBUG)
|
|
189
|
+
|
|
190
|
+
def show_full_diff_ranges(self) -> bool:
|
|
191
|
+
return self.cli_show_full_diff_ranges or self.log_for(logging.INFO)
|
|
192
|
+
|
|
193
|
+
def show_start_rev(self) -> bool:
|
|
194
|
+
return self.cli_show_start_rev or self.log_for(logging.INFO)
|
|
195
|
+
|
|
196
|
+
def show_tool_versions(self) -> bool:
|
|
197
|
+
return self.cli_show_tool_versions or self.log_for(logging.INFO)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
@define(kw_only=True)
|
|
201
|
+
class BaseCommand[C: Awaitable[Any], R]:
|
|
202
|
+
output: Output
|
|
203
|
+
flake8: bool
|
|
204
|
+
ruff_check: bool
|
|
205
|
+
ruff_format: bool
|
|
206
|
+
workspace: Workspace
|
|
207
|
+
|
|
208
|
+
@abstractmethod
|
|
209
|
+
def finish(self) -> None: ...
|
|
210
|
+
|
|
211
|
+
@abstractmethod
|
|
212
|
+
def get_tasks(self) -> list[C]: ...
|
|
213
|
+
|
|
214
|
+
@abstractmethod
|
|
215
|
+
def process_results(self, results: R) -> None: ...
|
|
216
|
+
|
|
217
|
+
async def run_tasks(self, tasks: list[C]) -> bool:
|
|
218
|
+
exceptions = []
|
|
219
|
+
task_failed = False
|
|
220
|
+
output = self.output
|
|
221
|
+
for task in tasks:
|
|
222
|
+
try:
|
|
223
|
+
results = await task
|
|
224
|
+
except CalledProcessError as e:
|
|
225
|
+
output._log_exception("Task failed", e)
|
|
226
|
+
task_failed = True
|
|
227
|
+
except BaseException as e: # noqa: BLE001
|
|
228
|
+
exceptions.append(e)
|
|
229
|
+
else:
|
|
230
|
+
self.process_results(results)
|
|
231
|
+
if exceptions:
|
|
232
|
+
msg = "Unhandled exceptions in run_tasks"
|
|
233
|
+
raise BaseExceptionGroup(msg, exceptions)
|
|
234
|
+
return task_failed
|
|
235
|
+
|
|
236
|
+
async def run(self) -> None:
|
|
237
|
+
workspace = self.workspace
|
|
238
|
+
output = self.output
|
|
239
|
+
await output.log_full_python_diff(workspace)
|
|
240
|
+
tasks = self.get_tasks()
|
|
241
|
+
if not tasks:
|
|
242
|
+
log.error("No tasks selected.")
|
|
243
|
+
sys.exit(5)
|
|
244
|
+
task_failed = await self.run_tasks(tasks)
|
|
245
|
+
if task_failed:
|
|
246
|
+
sys.exit(6)
|
|
247
|
+
self.finish()
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def run_linter(*, linter: bool | None, default: bool | None) -> bool:
|
|
251
|
+
if (default or default is None) and linter is None:
|
|
252
|
+
return True
|
|
253
|
+
return bool(linter)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
class CheckCommand(BaseCommand[LintResultsCoroutine, LintResults]):
|
|
257
|
+
failed = False
|
|
258
|
+
|
|
259
|
+
def finish(self) -> None:
|
|
260
|
+
if self.failed:
|
|
261
|
+
sys.exit(4)
|
|
262
|
+
|
|
263
|
+
def get_tasks(self) -> list[LintResultsCoroutine]:
|
|
264
|
+
tasks: list[LintResultsCoroutine] = []
|
|
265
|
+
if self.ruff_format:
|
|
266
|
+
tasks.extend(self.workspace.ruff_format.check_tasks())
|
|
267
|
+
if self.ruff_check:
|
|
268
|
+
tasks.extend(self.workspace.ruff_check.check_tasks())
|
|
269
|
+
if self.flake8:
|
|
270
|
+
tasks.extend(self.workspace.flake8.check_tasks())
|
|
271
|
+
return tasks
|
|
272
|
+
|
|
273
|
+
def process_results(self, results: LintResults) -> None:
|
|
274
|
+
self.failed = self.failed or results.failed
|
|
275
|
+
for result in results:
|
|
276
|
+
click.echo(result.format(self.output))
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
@cli.command()
|
|
280
|
+
@click.argument("rev", envvar="LINT_ROLLER_START_REV", required=False)
|
|
281
|
+
@checkers_options
|
|
282
|
+
@common_output_options
|
|
283
|
+
@start_rev_option
|
|
284
|
+
def check( # noqa: PLR0913
|
|
285
|
+
*,
|
|
286
|
+
rev: str | None,
|
|
287
|
+
show_full_diff: bool,
|
|
288
|
+
show_full_diff_ranges: bool,
|
|
289
|
+
show_start_rev: bool,
|
|
290
|
+
show_tool_versions: bool,
|
|
291
|
+
start_rev: str | None,
|
|
292
|
+
verbose: int,
|
|
293
|
+
run_default_checks: bool | None,
|
|
294
|
+
run_ruff_check: bool | None,
|
|
295
|
+
run_flake8: bool | None,
|
|
296
|
+
run_ruff_format: bool | None,
|
|
297
|
+
) -> None:
|
|
298
|
+
"""Check for any violations.
|
|
299
|
+
|
|
300
|
+
The optional REV argument is the commit hash or symbolic name from which the git diff is taken.
|
|
301
|
+
"""
|
|
302
|
+
output = Output(
|
|
303
|
+
cli_show_full_diff=show_full_diff,
|
|
304
|
+
cli_show_full_diff_ranges=show_full_diff_ranges,
|
|
305
|
+
cli_show_start_rev=show_start_rev,
|
|
306
|
+
cli_show_tool_versions=show_tool_versions,
|
|
307
|
+
verbose=verbose,
|
|
308
|
+
)
|
|
309
|
+
workspace = Workspace(cli_rev=rev, cli_start_rev=start_rev)
|
|
310
|
+
structlog.configure(
|
|
311
|
+
wrapper_class=structlog.make_filtering_bound_logger(output.log_level),
|
|
312
|
+
)
|
|
313
|
+
try:
|
|
314
|
+
output.log_versions(workspace)
|
|
315
|
+
output.log_start_rev(workspace)
|
|
316
|
+
asyncio.run(
|
|
317
|
+
CheckCommand(
|
|
318
|
+
flake8=run_linter(linter=run_flake8, default=run_default_checks),
|
|
319
|
+
output=output,
|
|
320
|
+
ruff_check=run_linter(
|
|
321
|
+
linter=run_ruff_check, default=run_default_checks
|
|
322
|
+
),
|
|
323
|
+
ruff_format=run_linter(
|
|
324
|
+
linter=run_ruff_format, default=run_default_checks
|
|
325
|
+
),
|
|
326
|
+
workspace=workspace,
|
|
327
|
+
).run()
|
|
328
|
+
)
|
|
329
|
+
except (CalledProcessError, ExceptionGroup) as e:
|
|
330
|
+
(se, *_oe) = output._iter_exceptions(e)
|
|
331
|
+
output.log_exception(se)
|
|
332
|
+
if isinstance(se, CalledProcessError):
|
|
333
|
+
sys.exit(3)
|
|
334
|
+
raise se from e
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from attrs import define
|
|
4
|
+
from click import style
|
|
5
|
+
from click import unstyle
|
|
6
|
+
from functools import cached_property
|
|
7
|
+
from match_diff_lines import parse_unified_diff as _parse_unified_diff
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
import itertools
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from collections.abc import Iterable
|
|
14
|
+
from collections.abc import Iterator
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@define(frozen=True)
|
|
19
|
+
class LineRange:
|
|
20
|
+
start: int
|
|
21
|
+
stop: int
|
|
22
|
+
|
|
23
|
+
def __iter__(self) -> Iterator[int]:
|
|
24
|
+
yield self.start
|
|
25
|
+
yield self.stop
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def ranges(numbers: Iterable[int]) -> Iterator[LineRange]:
|
|
29
|
+
for _a, _b in itertools.groupby(
|
|
30
|
+
enumerate(sorted(numbers)), lambda pair: pair[1] - pair[0]
|
|
31
|
+
):
|
|
32
|
+
b = list(_b)
|
|
33
|
+
yield LineRange(b[0][1], b[-1][1])
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@define
|
|
37
|
+
class Diff:
|
|
38
|
+
base: Path
|
|
39
|
+
raw: str
|
|
40
|
+
|
|
41
|
+
def __str__(self) -> str:
|
|
42
|
+
return self.raw
|
|
43
|
+
|
|
44
|
+
def colored(self) -> str:
|
|
45
|
+
return "\n".join(
|
|
46
|
+
style(
|
|
47
|
+
line,
|
|
48
|
+
fg="green"
|
|
49
|
+
if line.startswith("+")
|
|
50
|
+
else "red"
|
|
51
|
+
if line.startswith("-")
|
|
52
|
+
else None,
|
|
53
|
+
)
|
|
54
|
+
for line in self.raw.splitlines()
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
def format_ranges(self) -> str:
|
|
58
|
+
base = self.base
|
|
59
|
+
result = []
|
|
60
|
+
for fn, _ranges in sorted(self.ranges.items()):
|
|
61
|
+
ranges = ", ".join(f"{s}" if s == e else f"{s}-{e}" for s, e in _ranges)
|
|
62
|
+
result.append(f"{style(fn.relative_to(base), bold=True)}: {ranges}")
|
|
63
|
+
return "\n".join(result)
|
|
64
|
+
|
|
65
|
+
@cached_property
|
|
66
|
+
def path_lines_map(self) -> dict[Path, frozenset[int]]:
|
|
67
|
+
parsed_diff = parse_unified_diff(self.raw.splitlines())
|
|
68
|
+
return {self.base / fn: line_nums for fn, line_nums in parsed_diff.items()}
|
|
69
|
+
|
|
70
|
+
@cached_property
|
|
71
|
+
def ranges(self) -> dict[Path, list[LineRange]]:
|
|
72
|
+
return {
|
|
73
|
+
fn: list(ranges(lines_nums))
|
|
74
|
+
for fn, lines_nums in self.path_lines_map.items()
|
|
75
|
+
if lines_nums
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def parse_unified_diff(lines: Iterable[str]) -> dict[str, frozenset[int]]:
|
|
80
|
+
return {
|
|
81
|
+
k: frozenset(v)
|
|
82
|
+
for k, v in _parse_unified_diff(unstyle(line) for line in lines).items()
|
|
83
|
+
}
|