gruff 0.0.1__py3-none-win_amd64.whl
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.
|
Binary file
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gruff
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
5
|
+
Classifier: Environment :: Console
|
|
6
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
7
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
8
|
+
Classifier: Programming Language :: Rust
|
|
9
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Summary: An opinionated deterministic maintainability linter for Python
|
|
12
|
+
License-Expression: MIT
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
15
|
+
Project-URL: Repository, https://github.com/wkentaro/gruff
|
|
16
|
+
|
|
17
|
+
# Gruff
|
|
18
|
+
|
|
19
|
+
Gruff is an opinionated, deterministic maintainability linter for Python. It complements Ruff with project policies that make agent-assisted code easier to understand and review; it does not infer who or what wrote the code.
|
|
20
|
+
|
|
21
|
+
The first release tests four theses: private inputs are easier to trace when callers name them, private behavior is easier to review when callers supply every value, package initializer manifests are easier to review when every public import path defines `__all__`, and constants are easier to review when uppercase names and `Final` annotations always appear together.
|
|
22
|
+
|
|
23
|
+
All rules are opt-in. Teams enable policies one at a time as they decide which opinionated constraints fit their codebase. A check with no enabled rules succeeds but warns that it performed no policy analysis.
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
Requires Python 3.10 or later.
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install gruff
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Or with [uv](https://docs.astral.sh/uv/):
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
uv tool install gruff
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Verify it works:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
gruff --version
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
> [!TIP]
|
|
46
|
+
> To try the latest development version (the head of `main` on GitHub) before
|
|
47
|
+
> it is published:
|
|
48
|
+
>
|
|
49
|
+
> ```bash
|
|
50
|
+
> uv tool install git+https://github.com/wkentaro/gruff
|
|
51
|
+
> ```
|
|
52
|
+
|
|
53
|
+
## Rules
|
|
54
|
+
|
|
55
|
+
### `keyword-only-private-inputs` (GR001)
|
|
56
|
+
|
|
57
|
+
Flags each fixed caller-supplied input to a private module-level function or method that is positional; implicit method receivers and variadic parameters are excluded.
|
|
58
|
+
|
|
59
|
+
Before → after:
|
|
60
|
+
|
|
61
|
+
```diff
|
|
62
|
+
-def _resize_image(data: bytes, width: int) -> bytes:
|
|
63
|
+
+def _resize_image(*, data: bytes, width: int) -> bytes:
|
|
64
|
+
return resize(data, width=width)
|
|
65
|
+
|
|
66
|
+
def make_thumbnail(data: bytes) -> bytes:
|
|
67
|
+
- return _resize_image(data, width=512)
|
|
68
|
+
+ return _resize_image(data=data, width=512)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### `required-private-inputs` (GR002)
|
|
72
|
+
|
|
73
|
+
Flags each fixed caller-supplied input to a private module-level function or method that has a default; implicit method receivers and variadic parameters are excluded.
|
|
74
|
+
|
|
75
|
+
Before → after:
|
|
76
|
+
|
|
77
|
+
```diff
|
|
78
|
+
-def _resize_image(*, data: bytes, width: int = 512) -> bytes:
|
|
79
|
+
+def _resize_image(*, data: bytes, width: int) -> bytes:
|
|
80
|
+
return resize(data, width=width)
|
|
81
|
+
|
|
82
|
+
def make_thumbnail(data: bytes) -> bytes:
|
|
83
|
+
- return _resize_image(data=data)
|
|
84
|
+
+ return _resize_image(data=data, width=512)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### `package-dunder-all` (GR003)
|
|
88
|
+
|
|
89
|
+
Flags a package initializer when a successfully completing import path leaves a public binding without `__all__`. The rule covers `__init__.py` and `__init__.pyi`, including bindings in module-level control flow, and reports at most one finding per file. Empty, private-only, type-checking-only, and statically false paths do not require a manifest.
|
|
90
|
+
|
|
91
|
+
Before → after:
|
|
92
|
+
|
|
93
|
+
```diff
|
|
94
|
+
from .client import Client
|
|
95
|
+
from .errors import GruffError
|
|
96
|
+
|
|
97
|
+
+__all__ = ["Client", "GruffError"]
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### `final-constants` (GR004)
|
|
101
|
+
|
|
102
|
+
Flags simple-name assignments when an uppercase name and a `Final` annotation do not appear together. The rule applies in module, class, and function scopes, including nested control flow. Enum members, type aliases, chained and unpacking assignments, augmented assignments, loop and context-manager targets, attributes, subscripts, and imports are excluded.
|
|
103
|
+
|
|
104
|
+
Before → after:
|
|
105
|
+
|
|
106
|
+
```diff
|
|
107
|
+
from typing import Final
|
|
108
|
+
|
|
109
|
+
-THUMBNAIL_WIDTH = 512
|
|
110
|
+
-image_format: Final = "png"
|
|
111
|
+
+THUMBNAIL_WIDTH: Final = 512
|
|
112
|
+
+IMAGE_FORMAT: Final = "png"
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Exceptions
|
|
116
|
+
|
|
117
|
+
Suppress a rule on definitions that must follow an external calling convention or intentionally provide a convenience default:
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
def _format_cost(value: float) -> str: # noqa: GR001 -- Callable[[float], str]
|
|
121
|
+
return f"${value:.2f}"
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _render(*, value: float, unit: str = ""): # noqa: GR002 -- optional suffix
|
|
125
|
+
return f"{value}{unit}"
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
EXTERNAL_NAME = 1 # noqa: GR004 -- public protocol spelling
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
For a dynamic package manifest, suppress GR003 on the reported public binding and state why deterministic source analysis does not apply:
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
public = load_exports() # noqa: GR003 -- exec() defines __all__ below
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Prefer an inline suppression because it keeps the exception next to its reason. For files made entirely of protocol implementations, use a per-file ignore instead.
|
|
138
|
+
|
|
139
|
+
## Recommended Ruff pairing
|
|
140
|
+
|
|
141
|
+
Gruff does not duplicate checks that Ruff already provides. These Ruff rules extend the same theses to code Gruff does not cover:
|
|
142
|
+
|
|
143
|
+
```toml
|
|
144
|
+
[tool.ruff.lint]
|
|
145
|
+
extend-select = ["ARG", "FBT", "B006", "B008", "PLR2004", "RUF012", "RUF022"]
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
`F401` and `F822` are in Ruff's default rule set; the pairing below assumes they stay enabled.
|
|
149
|
+
|
|
150
|
+
### Callable inputs (GR001, GR002)
|
|
151
|
+
|
|
152
|
+
`ARG` flags unused function and method arguments, including arguments on private definitions:
|
|
153
|
+
|
|
154
|
+
```diff
|
|
155
|
+
-def _resize_image(*, data: bytes, width: int, legacy: bool) -> bytes:
|
|
156
|
+
+def _resize_image(*, data: bytes, width: int) -> bytes:
|
|
157
|
+
return resize(data, width=width)
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
GR001 makes callers of private callables name every input; `FBT001` and `FBT002` extend that to boolean inputs on public callables:
|
|
161
|
+
|
|
162
|
+
```diff
|
|
163
|
+
-def resize_image(data: bytes, keep_aspect: bool) -> bytes:
|
|
164
|
+
+def resize_image(data: bytes, *, keep_aspect: bool) -> bytes:
|
|
165
|
+
return resize(data, keep_aspect=keep_aspect)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
GR002 removes defaults from private callables; `B006` and `B008` catch shared mutable defaults and import-time call defaults on the public callables that keep theirs:
|
|
169
|
+
|
|
170
|
+
```diff
|
|
171
|
+
-def make_thumbnails(data: bytes, widths: list[int] = []) -> list[bytes]:
|
|
172
|
+
+def make_thumbnails(data: bytes, widths: list[int] | None = None) -> list[bytes]:
|
|
173
|
+
|
|
174
|
+
-def fetch_image(client: Client = Client()) -> bytes:
|
|
175
|
+
+def fetch_image(client: Client | None = None) -> bytes:
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Package manifests (GR003)
|
|
179
|
+
|
|
180
|
+
GR003 only requires the manifest to exist. Once it does, `F401` flags re-exports missing from it:
|
|
181
|
+
|
|
182
|
+
```diff
|
|
183
|
+
from .client import Client
|
|
184
|
+
from .errors import GruffError
|
|
185
|
+
|
|
186
|
+
-__all__ = ["Client"]
|
|
187
|
+
+__all__ = ["Client", "GruffError"]
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
`F822` finds names in the manifest that are not defined:
|
|
191
|
+
|
|
192
|
+
```diff
|
|
193
|
+
-__all__ = ["Client", "GruffErorr"]
|
|
194
|
+
+__all__ = ["Client", "GruffError"]
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
`RUF022` sorts static manifests:
|
|
198
|
+
|
|
199
|
+
```diff
|
|
200
|
+
-__all__ = ["GruffError", "Client"]
|
|
201
|
+
+__all__ = ["Client", "GruffError"]
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Constants (GR004)
|
|
205
|
+
|
|
206
|
+
`PLR2004` turns magic values into named constants, which GR004 then requires to be uppercase and `Final`:
|
|
207
|
+
|
|
208
|
+
```diff
|
|
209
|
+
+MAX_WIDTH: Final = 4096
|
|
210
|
+
+
|
|
211
|
+
def validate_width(width: int) -> None:
|
|
212
|
+
- if width > 4096:
|
|
213
|
+
+ if width > MAX_WIDTH:
|
|
214
|
+
raise ValueError(width)
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
`RUF012` applies the same annotation discipline to mutable class attributes, which GR004 excludes:
|
|
218
|
+
|
|
219
|
+
```diff
|
|
220
|
+
class ThumbnailWriter:
|
|
221
|
+
- formats = ["png", "jpg"]
|
|
222
|
+
+ formats: ClassVar[list[str]] = ["png", "jpg"]
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## Interface
|
|
226
|
+
|
|
227
|
+
Gruff will follow Ruff's familiar command and diagnostic conventions:
|
|
228
|
+
|
|
229
|
+
```console
|
|
230
|
+
gruff check .
|
|
231
|
+
gruff check --select GR001 .
|
|
232
|
+
gruff check --select GR002 .
|
|
233
|
+
gruff check --select GR003 .
|
|
234
|
+
gruff check --select GR004 .
|
|
235
|
+
gruff check --select GR001,GR002,GR003,GR004 .
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Lint findings, including invalid Python syntax, will exit with status 1. Configuration, I/O, and internal failures will exit with status 2.
|
|
239
|
+
|
|
240
|
+
Gruff does not rewrite source code in the first release.
|
|
241
|
+
|
|
242
|
+
## Configuration
|
|
243
|
+
|
|
244
|
+
Gruff reads configuration only from `pyproject.toml`:
|
|
245
|
+
|
|
246
|
+
```toml
|
|
247
|
+
[tool.gruff.lint]
|
|
248
|
+
select = ["GR001", "GR002", "GR003", "GR004"]
|
|
249
|
+
ignore = []
|
|
250
|
+
per-file-ignores = { "callbacks.py" = ["GR001"] }
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Directory discovery checks `.py`, `.pyi`, and `.pyw` files and respects Git ignore files.
|
|
254
|
+
|
|
255
|
+
## Distribution
|
|
256
|
+
|
|
257
|
+
Public releases will use PyPI wheels for Linux x86_64 and aarch64, macOS x86_64 and arm64, and Windows x86_64. Gruff is not published to crates.io.
|
|
258
|
+
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
gruff-0.0.1.data/scripts/gruff.exe,sha256=JQAeaCQrdkLDNDV4TR-ArVBE9LycGpWUeo2bhYB3DFY,4363264
|
|
2
|
+
gruff-0.0.1.dist-info/METADATA,sha256=JRrKmtaUdydtA_-G_ZmBDJToha9dfz5kGdgs-F2tEgc,8450
|
|
3
|
+
gruff-0.0.1.dist-info/WHEEL,sha256=8Aej0W0a6Cz6apA3IzJrTnxLRVLAt-w0Oh8SA3Con_c,94
|
|
4
|
+
gruff-0.0.1.dist-info/licenses/LICENSE,sha256=CJJKc1H10DyG0jl-qEWfr64KtWnMaXxfmsruVZ0nD7c,1090
|
|
5
|
+
gruff-0.0.1.dist-info/sboms/gruff.cyclonedx.json,sha256=tbeEwx8Dxt8M3zcryGkkYV-vuzJgalTaGs1-P8oAg1I,132253
|
|
6
|
+
gruff-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kentaro Wada
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|