fastaistyle 0.0.5__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.
- fastaistyle-0.0.5/.claude/settings.local.json +18 -0
- fastaistyle-0.0.5/.github/workflows/release.yml +18 -0
- fastaistyle-0.0.5/.gitignore +146 -0
- fastaistyle-0.0.5/PKG-INFO +243 -0
- fastaistyle-0.0.5/README.md +227 -0
- fastaistyle-0.0.5/chkstyle.py +297 -0
- fastaistyle-0.0.5/pyproject.toml +36 -0
- fastaistyle-0.0.5/style.md +53 -0
- fastaistyle-0.0.5/tests/test_chkstyle.py +92 -0
- fastaistyle-0.0.5/tools/release.sh +20 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(claude --version)",
|
|
5
|
+
"Bash(npm update:*)",
|
|
6
|
+
"Bash(pip install:*)",
|
|
7
|
+
"Bash(pytest:*)",
|
|
8
|
+
"Bash(chkstyle --help:*)",
|
|
9
|
+
"Bash(chkstyle:*)",
|
|
10
|
+
"Bash(hatch version:*)",
|
|
11
|
+
"Bash(hatch build:*)",
|
|
12
|
+
"Bash(gh run list:*)",
|
|
13
|
+
"Bash(gh run rerun:*)",
|
|
14
|
+
"Bash(git push)",
|
|
15
|
+
"Bash(./tools/release.sh)"
|
|
16
|
+
]
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
tags: ['v*']
|
|
5
|
+
|
|
6
|
+
jobs:
|
|
7
|
+
pypi:
|
|
8
|
+
runs-on: ubuntu-latest
|
|
9
|
+
permissions:
|
|
10
|
+
id-token: write
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
- uses: actions/setup-python@v5
|
|
14
|
+
with:
|
|
15
|
+
python-version: '3.12'
|
|
16
|
+
- run: pip install hatch
|
|
17
|
+
- run: hatch build
|
|
18
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
_proc/
|
|
2
|
+
sidebar.yml
|
|
3
|
+
Gemfile.lock
|
|
4
|
+
token
|
|
5
|
+
_docs/
|
|
6
|
+
conda/
|
|
7
|
+
.last_checked
|
|
8
|
+
.gitconfig
|
|
9
|
+
*.bak
|
|
10
|
+
*.log
|
|
11
|
+
*~
|
|
12
|
+
~*
|
|
13
|
+
_tmp*
|
|
14
|
+
tmp*
|
|
15
|
+
tags
|
|
16
|
+
|
|
17
|
+
# Byte-compiled / optimized / DLL files
|
|
18
|
+
__pycache__/
|
|
19
|
+
*.py[cod]
|
|
20
|
+
*$py.class
|
|
21
|
+
|
|
22
|
+
# C extensions
|
|
23
|
+
*.so
|
|
24
|
+
|
|
25
|
+
# Distribution / packaging
|
|
26
|
+
.Python
|
|
27
|
+
env/
|
|
28
|
+
build/
|
|
29
|
+
develop-eggs/
|
|
30
|
+
dist/
|
|
31
|
+
downloads/
|
|
32
|
+
eggs/
|
|
33
|
+
.eggs/
|
|
34
|
+
lib/
|
|
35
|
+
lib64/
|
|
36
|
+
parts/
|
|
37
|
+
sdist/
|
|
38
|
+
var/
|
|
39
|
+
wheels/
|
|
40
|
+
*.egg-info/
|
|
41
|
+
.installed.cfg
|
|
42
|
+
*.egg
|
|
43
|
+
|
|
44
|
+
# PyInstaller
|
|
45
|
+
# Usually these files are written by a python script from a template
|
|
46
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
47
|
+
*.manifest
|
|
48
|
+
*.spec
|
|
49
|
+
|
|
50
|
+
# Installer logs
|
|
51
|
+
pip-log.txt
|
|
52
|
+
pip-delete-this-directory.txt
|
|
53
|
+
|
|
54
|
+
# Unit test / coverage reports
|
|
55
|
+
htmlcov/
|
|
56
|
+
.tox/
|
|
57
|
+
.coverage
|
|
58
|
+
.coverage.*
|
|
59
|
+
.cache
|
|
60
|
+
nosetests.xml
|
|
61
|
+
coverage.xml
|
|
62
|
+
*.cover
|
|
63
|
+
.hypothesis/
|
|
64
|
+
|
|
65
|
+
# Translations
|
|
66
|
+
*.mo
|
|
67
|
+
*.pot
|
|
68
|
+
|
|
69
|
+
# Django stuff:
|
|
70
|
+
*.log
|
|
71
|
+
local_settings.py
|
|
72
|
+
|
|
73
|
+
# Flask stuff:
|
|
74
|
+
instance/
|
|
75
|
+
.webassets-cache
|
|
76
|
+
|
|
77
|
+
# Scrapy stuff:
|
|
78
|
+
.scrapy
|
|
79
|
+
|
|
80
|
+
# Sphinx documentation
|
|
81
|
+
docs/_build/
|
|
82
|
+
|
|
83
|
+
# PyBuilder
|
|
84
|
+
target/
|
|
85
|
+
|
|
86
|
+
# Jupyter Notebook
|
|
87
|
+
.ipynb_checkpoints
|
|
88
|
+
|
|
89
|
+
# pyenv
|
|
90
|
+
.python-version
|
|
91
|
+
|
|
92
|
+
# celery beat schedule file
|
|
93
|
+
celerybeat-schedule
|
|
94
|
+
|
|
95
|
+
# SageMath parsed files
|
|
96
|
+
*.sage.py
|
|
97
|
+
|
|
98
|
+
# dotenv
|
|
99
|
+
.env
|
|
100
|
+
|
|
101
|
+
# virtualenv
|
|
102
|
+
.venv
|
|
103
|
+
venv/
|
|
104
|
+
ENV/
|
|
105
|
+
|
|
106
|
+
# Spyder project settings
|
|
107
|
+
.spyderproject
|
|
108
|
+
.spyproject
|
|
109
|
+
|
|
110
|
+
# Rope project settings
|
|
111
|
+
.ropeproject
|
|
112
|
+
|
|
113
|
+
# mkdocs documentation
|
|
114
|
+
/site
|
|
115
|
+
|
|
116
|
+
# mypy
|
|
117
|
+
.mypy_cache/
|
|
118
|
+
|
|
119
|
+
.vscode
|
|
120
|
+
*.swp
|
|
121
|
+
|
|
122
|
+
# osx generated files
|
|
123
|
+
.DS_Store
|
|
124
|
+
.DS_Store?
|
|
125
|
+
.Trashes
|
|
126
|
+
ehthumbs.db
|
|
127
|
+
Thumbs.db
|
|
128
|
+
.idea
|
|
129
|
+
|
|
130
|
+
# pytest
|
|
131
|
+
.pytest_cache
|
|
132
|
+
|
|
133
|
+
# tools/trust-doc-nbs
|
|
134
|
+
docs_src/.last_checked
|
|
135
|
+
|
|
136
|
+
# symlinks to fastai
|
|
137
|
+
docs_src/fastai
|
|
138
|
+
tools/fastai
|
|
139
|
+
|
|
140
|
+
# link checker
|
|
141
|
+
checklink/cookies.txt
|
|
142
|
+
|
|
143
|
+
# .gitconfig is now autogenerated
|
|
144
|
+
.gitconfig
|
|
145
|
+
|
|
146
|
+
_docs
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fastaistyle
|
|
3
|
+
Version: 0.0.5
|
|
4
|
+
Summary: Style checker for fast.ai coding conventions
|
|
5
|
+
Project-URL: Homepage, https://github.com/AnswerDotAI/fastaistyle
|
|
6
|
+
Author-email: Jeremy Howard <j@fast.ai>
|
|
7
|
+
License-Expression: Apache-2.0
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# fastaistyle
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install fastaistyle
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
A style checker that enforces the [fast.ai coding style](https://docs.fast.ai/dev/style.html)—a compact, readable approach to Python that keeps more code visible on screen and reduces cognitive load.
|
|
24
|
+
|
|
25
|
+
## Why This Style?
|
|
26
|
+
|
|
27
|
+
Most style guides optimize for the wrong thing. They add vertical space, mandate verbose names, and scatter related code across many lines. The result? You see less code at once, which means more scrolling, more context-switching, and more mental effort to understand what's happening.
|
|
28
|
+
|
|
29
|
+
The fast.ai style takes a different approach, rooted in decades of experience with APL, J, K, and scientific programming. The core insight comes from Kenneth Iverson: **"brevity facilitates reasoning."**
|
|
30
|
+
|
|
31
|
+
### Your Brain Can Only Hold So Much
|
|
32
|
+
|
|
33
|
+
When you're reading code, your working memory is limited. If a function spans 50 lines, you can't see the whole thing at once. You scroll down, forget what was at the top, scroll back up. Each scroll is a context switch. Each context switch costs mental energy.
|
|
34
|
+
|
|
35
|
+
But if that same function fits in 15 lines? You see the whole picture. Your eyes can jump between related parts instantly. Patterns become obvious. Bugs stand out.
|
|
36
|
+
|
|
37
|
+
This isn't about cramming code together—it's about *removing unnecessary vertical space* so your brain can do what it's good at: recognizing patterns across visible information.
|
|
38
|
+
|
|
39
|
+
### One Line, One Idea
|
|
40
|
+
|
|
41
|
+
The goal is density without confusion. Each line should express one complete thought:
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
# Good: you see the whole pattern at once
|
|
45
|
+
if not data: return None
|
|
46
|
+
for item in items: process(item)
|
|
47
|
+
def _is_ready(self): return self._ready.is_set()
|
|
48
|
+
|
|
49
|
+
# Bad: same logic, but now it's 6 lines instead of 3
|
|
50
|
+
if not data:
|
|
51
|
+
return None
|
|
52
|
+
for item in items:
|
|
53
|
+
process(item)
|
|
54
|
+
def _is_ready(self):
|
|
55
|
+
return self._ready.is_set()
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
When the body is simple, keep it on the same line. Save vertical space for code that actually needs it.
|
|
59
|
+
|
|
60
|
+
### Names Should Be Short (When Used Often)
|
|
61
|
+
|
|
62
|
+
This follows "Huffman coding" for variable names—frequently used things get short names:
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
# Good: conventional, recognizable
|
|
66
|
+
img, i, msg, ctx
|
|
67
|
+
|
|
68
|
+
# Bad: verbose for no benefit
|
|
69
|
+
image_data, loop_index, message_object, context_instance
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Domain experts recognize `nll` (negative log likelihood) instantly. Spelling it out doesn't help them, and the extra characters push code off the right edge of the screen.
|
|
73
|
+
|
|
74
|
+
## Installation
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
pip install fastaistyle
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Or install from source:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
git clone https://github.com/AnswerDotAI/fastaistyle
|
|
84
|
+
cd fastaistyle
|
|
85
|
+
pip install -e .
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Usage
|
|
89
|
+
|
|
90
|
+
Check the current directory:
|
|
91
|
+
```bash
|
|
92
|
+
chkstyle
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Check a specific path:
|
|
96
|
+
```bash
|
|
97
|
+
chkstyle path/to/code/
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
The checker prints violations with file paths, line numbers, and the offending code.
|
|
101
|
+
|
|
102
|
+
## What It Checks
|
|
103
|
+
|
|
104
|
+
### `dict literal with 3+ identifier keys`
|
|
105
|
+
Use `dict()` for keyword-like keys—it's easier to scan and produces cleaner diffs.
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
# Bad
|
|
109
|
+
payload = {"host": host, "port": port, "timeout": timeout}
|
|
110
|
+
|
|
111
|
+
# Good
|
|
112
|
+
payload = dict(host=host, port=port, timeout=timeout)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### `single-statement body not one-liner`
|
|
116
|
+
If the body is one simple statement, keep it on the header line.
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
# Bad
|
|
120
|
+
if ready:
|
|
121
|
+
return True
|
|
122
|
+
|
|
123
|
+
# Good
|
|
124
|
+
if ready: return True
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### `single-line docstring uses triple quotes`
|
|
128
|
+
Triple quotes are for multi-line strings. Single-line docstrings should use regular quotes.
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
# Bad
|
|
132
|
+
def foo():
|
|
133
|
+
"""Return the value."""
|
|
134
|
+
return x
|
|
135
|
+
|
|
136
|
+
# Good
|
|
137
|
+
def foo():
|
|
138
|
+
"Return the value."
|
|
139
|
+
return x
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### `multi-line from-import`
|
|
143
|
+
If it fits on one line, keep it on one line.
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
# Bad
|
|
147
|
+
from os import (
|
|
148
|
+
path,
|
|
149
|
+
environ,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# Good
|
|
153
|
+
from os import path, environ
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### `line >150 chars`
|
|
157
|
+
Wrap at a natural boundary: argument lists, binary operators, or strings. But 150 is generous—aim for ~120 when practical.
|
|
158
|
+
|
|
159
|
+
### `semicolon statement separator`
|
|
160
|
+
Don't use `;` to combine statements. Use separate lines.
|
|
161
|
+
|
|
162
|
+
### `inefficient multiline expression`
|
|
163
|
+
If the content would fit in fewer lines, condense it.
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
# Bad
|
|
167
|
+
result = call(
|
|
168
|
+
a,
|
|
169
|
+
b,
|
|
170
|
+
c,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# Good
|
|
174
|
+
result = call(a, b, c)
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### `lhs assignment annotation`
|
|
178
|
+
Avoid `x: int = 1` in normal code. Put type hints on function parameters and return values instead. The exception is dataclass fields, where annotations are required.
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
# Bad
|
|
182
|
+
x: int = 1
|
|
183
|
+
name: str = "hello"
|
|
184
|
+
|
|
185
|
+
# Good (in a function signature)
|
|
186
|
+
def process(x: int, name: str) -> Result: ...
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### `nested generics depth >= 2`
|
|
190
|
+
Keep type annotations simple. Deep nesting makes them hard to read.
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
# Bad
|
|
194
|
+
items: list[dict[str, list[int]]]
|
|
195
|
+
|
|
196
|
+
# Good
|
|
197
|
+
Payload = dict[str, list[int]]
|
|
198
|
+
items: list[Payload]
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## Opting Out
|
|
202
|
+
|
|
203
|
+
Sometimes you have a good reason to format code a specific way. The checker supports pragmas:
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
# Ignore a single line
|
|
207
|
+
x: int = 1 # chkstyle: ignore
|
|
208
|
+
|
|
209
|
+
# Ignore the next line
|
|
210
|
+
# chkstyle: ignore
|
|
211
|
+
y: int = 2
|
|
212
|
+
|
|
213
|
+
# Disable for a block
|
|
214
|
+
# chkstyle: off
|
|
215
|
+
carefully_formatted = {
|
|
216
|
+
"alignment": "matters",
|
|
217
|
+
"here": "for readability",
|
|
218
|
+
}
|
|
219
|
+
# chkstyle: on
|
|
220
|
+
|
|
221
|
+
# Skip an entire file (must be in first 5 lines)
|
|
222
|
+
# chkstyle: skip
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## Running Tests
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
pytest
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## Philosophy
|
|
232
|
+
|
|
233
|
+
This tool exists to catch mechanical issues, not to enforce taste. The violations it reports are almost always things you'd want to fix—extra vertical space that doesn't help, type annotations that clutter rather than clarify, formatting that makes diffs noisier than necessary.
|
|
234
|
+
|
|
235
|
+
The goal is code that's pleasant to read and easy to maintain. Dense, but not cramped. Clear, but not verbose. When in doubt, look at the surrounding code and match its style.
|
|
236
|
+
|
|
237
|
+
For the full style guide, see:
|
|
238
|
+
- [fast.ai Style Guide](https://docs.fast.ai/dev/style.html)
|
|
239
|
+
- [style.md](style.md) in this repo
|
|
240
|
+
|
|
241
|
+
## License
|
|
242
|
+
|
|
243
|
+
Apache 2.0
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# fastaistyle
|
|
2
|
+
|
|
3
|
+
```bash
|
|
4
|
+
pip install fastaistyle
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
A style checker that enforces the [fast.ai coding style](https://docs.fast.ai/dev/style.html)—a compact, readable approach to Python that keeps more code visible on screen and reduces cognitive load.
|
|
8
|
+
|
|
9
|
+
## Why This Style?
|
|
10
|
+
|
|
11
|
+
Most style guides optimize for the wrong thing. They add vertical space, mandate verbose names, and scatter related code across many lines. The result? You see less code at once, which means more scrolling, more context-switching, and more mental effort to understand what's happening.
|
|
12
|
+
|
|
13
|
+
The fast.ai style takes a different approach, rooted in decades of experience with APL, J, K, and scientific programming. The core insight comes from Kenneth Iverson: **"brevity facilitates reasoning."**
|
|
14
|
+
|
|
15
|
+
### Your Brain Can Only Hold So Much
|
|
16
|
+
|
|
17
|
+
When you're reading code, your working memory is limited. If a function spans 50 lines, you can't see the whole thing at once. You scroll down, forget what was at the top, scroll back up. Each scroll is a context switch. Each context switch costs mental energy.
|
|
18
|
+
|
|
19
|
+
But if that same function fits in 15 lines? You see the whole picture. Your eyes can jump between related parts instantly. Patterns become obvious. Bugs stand out.
|
|
20
|
+
|
|
21
|
+
This isn't about cramming code together—it's about *removing unnecessary vertical space* so your brain can do what it's good at: recognizing patterns across visible information.
|
|
22
|
+
|
|
23
|
+
### One Line, One Idea
|
|
24
|
+
|
|
25
|
+
The goal is density without confusion. Each line should express one complete thought:
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
# Good: you see the whole pattern at once
|
|
29
|
+
if not data: return None
|
|
30
|
+
for item in items: process(item)
|
|
31
|
+
def _is_ready(self): return self._ready.is_set()
|
|
32
|
+
|
|
33
|
+
# Bad: same logic, but now it's 6 lines instead of 3
|
|
34
|
+
if not data:
|
|
35
|
+
return None
|
|
36
|
+
for item in items:
|
|
37
|
+
process(item)
|
|
38
|
+
def _is_ready(self):
|
|
39
|
+
return self._ready.is_set()
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
When the body is simple, keep it on the same line. Save vertical space for code that actually needs it.
|
|
43
|
+
|
|
44
|
+
### Names Should Be Short (When Used Often)
|
|
45
|
+
|
|
46
|
+
This follows "Huffman coding" for variable names—frequently used things get short names:
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
# Good: conventional, recognizable
|
|
50
|
+
img, i, msg, ctx
|
|
51
|
+
|
|
52
|
+
# Bad: verbose for no benefit
|
|
53
|
+
image_data, loop_index, message_object, context_instance
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Domain experts recognize `nll` (negative log likelihood) instantly. Spelling it out doesn't help them, and the extra characters push code off the right edge of the screen.
|
|
57
|
+
|
|
58
|
+
## Installation
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
pip install fastaistyle
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Or install from source:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
git clone https://github.com/AnswerDotAI/fastaistyle
|
|
68
|
+
cd fastaistyle
|
|
69
|
+
pip install -e .
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Usage
|
|
73
|
+
|
|
74
|
+
Check the current directory:
|
|
75
|
+
```bash
|
|
76
|
+
chkstyle
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Check a specific path:
|
|
80
|
+
```bash
|
|
81
|
+
chkstyle path/to/code/
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The checker prints violations with file paths, line numbers, and the offending code.
|
|
85
|
+
|
|
86
|
+
## What It Checks
|
|
87
|
+
|
|
88
|
+
### `dict literal with 3+ identifier keys`
|
|
89
|
+
Use `dict()` for keyword-like keys—it's easier to scan and produces cleaner diffs.
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
# Bad
|
|
93
|
+
payload = {"host": host, "port": port, "timeout": timeout}
|
|
94
|
+
|
|
95
|
+
# Good
|
|
96
|
+
payload = dict(host=host, port=port, timeout=timeout)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### `single-statement body not one-liner`
|
|
100
|
+
If the body is one simple statement, keep it on the header line.
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
# Bad
|
|
104
|
+
if ready:
|
|
105
|
+
return True
|
|
106
|
+
|
|
107
|
+
# Good
|
|
108
|
+
if ready: return True
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### `single-line docstring uses triple quotes`
|
|
112
|
+
Triple quotes are for multi-line strings. Single-line docstrings should use regular quotes.
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
# Bad
|
|
116
|
+
def foo():
|
|
117
|
+
"""Return the value."""
|
|
118
|
+
return x
|
|
119
|
+
|
|
120
|
+
# Good
|
|
121
|
+
def foo():
|
|
122
|
+
"Return the value."
|
|
123
|
+
return x
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### `multi-line from-import`
|
|
127
|
+
If it fits on one line, keep it on one line.
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
# Bad
|
|
131
|
+
from os import (
|
|
132
|
+
path,
|
|
133
|
+
environ,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# Good
|
|
137
|
+
from os import path, environ
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### `line >150 chars`
|
|
141
|
+
Wrap at a natural boundary: argument lists, binary operators, or strings. But 150 is generous—aim for ~120 when practical.
|
|
142
|
+
|
|
143
|
+
### `semicolon statement separator`
|
|
144
|
+
Don't use `;` to combine statements. Use separate lines.
|
|
145
|
+
|
|
146
|
+
### `inefficient multiline expression`
|
|
147
|
+
If the content would fit in fewer lines, condense it.
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
# Bad
|
|
151
|
+
result = call(
|
|
152
|
+
a,
|
|
153
|
+
b,
|
|
154
|
+
c,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# Good
|
|
158
|
+
result = call(a, b, c)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### `lhs assignment annotation`
|
|
162
|
+
Avoid `x: int = 1` in normal code. Put type hints on function parameters and return values instead. The exception is dataclass fields, where annotations are required.
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
# Bad
|
|
166
|
+
x: int = 1
|
|
167
|
+
name: str = "hello"
|
|
168
|
+
|
|
169
|
+
# Good (in a function signature)
|
|
170
|
+
def process(x: int, name: str) -> Result: ...
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### `nested generics depth >= 2`
|
|
174
|
+
Keep type annotations simple. Deep nesting makes them hard to read.
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
# Bad
|
|
178
|
+
items: list[dict[str, list[int]]]
|
|
179
|
+
|
|
180
|
+
# Good
|
|
181
|
+
Payload = dict[str, list[int]]
|
|
182
|
+
items: list[Payload]
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Opting Out
|
|
186
|
+
|
|
187
|
+
Sometimes you have a good reason to format code a specific way. The checker supports pragmas:
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
# Ignore a single line
|
|
191
|
+
x: int = 1 # chkstyle: ignore
|
|
192
|
+
|
|
193
|
+
# Ignore the next line
|
|
194
|
+
# chkstyle: ignore
|
|
195
|
+
y: int = 2
|
|
196
|
+
|
|
197
|
+
# Disable for a block
|
|
198
|
+
# chkstyle: off
|
|
199
|
+
carefully_formatted = {
|
|
200
|
+
"alignment": "matters",
|
|
201
|
+
"here": "for readability",
|
|
202
|
+
}
|
|
203
|
+
# chkstyle: on
|
|
204
|
+
|
|
205
|
+
# Skip an entire file (must be in first 5 lines)
|
|
206
|
+
# chkstyle: skip
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Running Tests
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
pytest
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## Philosophy
|
|
216
|
+
|
|
217
|
+
This tool exists to catch mechanical issues, not to enforce taste. The violations it reports are almost always things you'd want to fix—extra vertical space that doesn't help, type annotations that clutter rather than clarify, formatting that makes diffs noisier than necessary.
|
|
218
|
+
|
|
219
|
+
The goal is code that's pleasant to read and easy to maintain. Dense, but not cramped. Clear, but not verbose. When in doubt, look at the surrounding code and match its style.
|
|
220
|
+
|
|
221
|
+
For the full style guide, see:
|
|
222
|
+
- [fast.ai Style Guide](https://docs.fast.ai/dev/style.html)
|
|
223
|
+
- [style.md](style.md) in this repo
|
|
224
|
+
|
|
225
|
+
## License
|
|
226
|
+
|
|
227
|
+
Apache 2.0
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
__version__ = "0.0.5"
|
|
3
|
+
|
|
4
|
+
import ast, io, math, os, re, sys, tokenize
|
|
5
|
+
|
|
6
|
+
SKIP_DIRS = {".git", ".hg", ".svn", "__pycache__", ".mypy_cache", ".pytest_cache", ".venv", "venv", "dist", "build"}
|
|
7
|
+
WRAP_WIDTH = 120
|
|
8
|
+
COMPOUND_NODES = (ast.If, ast.For, ast.AsyncFor, ast.While, ast.With, ast.AsyncWith, ast.Try, ast.FunctionDef,
|
|
9
|
+
ast.AsyncFunctionDef, ast.ClassDef)
|
|
10
|
+
|
|
11
|
+
def iter_py_files(root: str):
|
|
12
|
+
"Iter py files."
|
|
13
|
+
for dirpath, dirnames, filenames in os.walk(root, followlinks=False):
|
|
14
|
+
dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS and not d.startswith(".")]
|
|
15
|
+
for name in filenames:
|
|
16
|
+
if not name.endswith(".py"): continue
|
|
17
|
+
path = os.path.join(dirpath, name)
|
|
18
|
+
if os.path.islink(path): continue
|
|
19
|
+
yield path
|
|
20
|
+
|
|
21
|
+
def is_identifier_str(node) -> bool:
|
|
22
|
+
"Identifier str."
|
|
23
|
+
return isinstance(node, ast.Constant) and isinstance(node.value, str) and node.value.isidentifier()
|
|
24
|
+
|
|
25
|
+
def is_docstring_stmt(stmt) -> bool:
|
|
26
|
+
"Docstring stmt."
|
|
27
|
+
return isinstance(stmt, ast.Expr) and isinstance(stmt.value, ast.Constant) and isinstance(stmt.value.value, str)
|
|
28
|
+
|
|
29
|
+
def node_lines(source: str, lines: list[str], node) -> list[str]:
|
|
30
|
+
"Node lines."
|
|
31
|
+
seg = ast.get_source_segment(source, node)
|
|
32
|
+
if seg: return [line.rstrip() for line in seg.splitlines()]
|
|
33
|
+
lineno = getattr(node, "lineno", None)
|
|
34
|
+
if lineno and 1 <= lineno <= len(lines): return [lines[lineno - 1].rstrip("\n")]
|
|
35
|
+
return []
|
|
36
|
+
|
|
37
|
+
def segment_lines(source: str, node) -> list[str]:
|
|
38
|
+
"Segment lines."
|
|
39
|
+
seg = ast.get_source_segment(source, node)
|
|
40
|
+
if not seg: return []
|
|
41
|
+
return [line.rstrip() for line in seg.splitlines()]
|
|
42
|
+
|
|
43
|
+
def first_line_indent(lines: list[str], lineno: int | None) -> int:
|
|
44
|
+
"First line indent."
|
|
45
|
+
if not lineno or lineno < 1 or lineno > len(lines): return 0
|
|
46
|
+
line = lines[lineno - 1]
|
|
47
|
+
return len(line) - len(line.lstrip())
|
|
48
|
+
|
|
49
|
+
def combined_len(seg_lines: list[str], indent: int) -> int:
|
|
50
|
+
"Combined length."
|
|
51
|
+
return sum(len(line.strip()) for line in seg_lines) + indent
|
|
52
|
+
|
|
53
|
+
def is_inefficient_multiline(seg_lines: list[str], indent: int) -> bool:
|
|
54
|
+
"Inefficient multiline."
|
|
55
|
+
if len(seg_lines) <= 1: return False
|
|
56
|
+
total = combined_len(seg_lines, indent)
|
|
57
|
+
needed = math.ceil(total / WRAP_WIDTH)
|
|
58
|
+
return needed < len(seg_lines)
|
|
59
|
+
|
|
60
|
+
def suite_len(lines: list[str], header_lineno: int | None, stmt_lineno: int | None) -> int | None:
|
|
61
|
+
"Suite length."
|
|
62
|
+
if not header_lineno or not stmt_lineno: return None
|
|
63
|
+
if header_lineno < 1 or stmt_lineno < 1: return None
|
|
64
|
+
if header_lineno > len(lines) or stmt_lineno > len(lines): return None
|
|
65
|
+
first = lines[header_lineno - 1]
|
|
66
|
+
second = lines[stmt_lineno - 1]
|
|
67
|
+
indent = len(first) - len(first.lstrip())
|
|
68
|
+
return len(first.strip()) + len(second.strip()) + indent
|
|
69
|
+
|
|
70
|
+
def find_suite_header(lines: list[str], start: int, stop: int, keyword: str) -> int:
|
|
71
|
+
"Find suite header."
|
|
72
|
+
if start < 1 or stop < 1 or start > len(lines): return stop
|
|
73
|
+
stop = max(1, min(stop, len(lines)))
|
|
74
|
+
for idx in range(start - 1, stop - 2, -1):
|
|
75
|
+
if lines[idx].lstrip().startswith(f"{keyword}:"): return idx + 1
|
|
76
|
+
return stop
|
|
77
|
+
|
|
78
|
+
def add_violation(violations: list[tuple], path: str, lineno: int, msg: str, lines: list[str], suppressed: set[int]):
|
|
79
|
+
"Add violation."
|
|
80
|
+
if lineno in suppressed: return
|
|
81
|
+
violations.append((path, lineno, msg, lines))
|
|
82
|
+
|
|
83
|
+
def check_single_line_docstring(source: str, lines: list[str], stmt, path: str, violations: list[tuple], suppressed: set[int]):
|
|
84
|
+
"Check single-line docstring."
|
|
85
|
+
doc = stmt.value.value
|
|
86
|
+
if "\n" in doc: return
|
|
87
|
+
seg = ast.get_source_segment(source, stmt) or ""
|
|
88
|
+
if re.match(r'^[ \t]*[rRuUbBfF]*\"\"\"', seg):
|
|
89
|
+
add_violation(violations, path, stmt.lineno, "single-line docstring uses triple quotes",
|
|
90
|
+
node_lines(source, lines, stmt), suppressed)
|
|
91
|
+
|
|
92
|
+
def check_suite(parent_kind: str, node, suite, path: str, source: str, lines: list[str], violations: list[tuple],
|
|
93
|
+
suppressed: set[int]):
|
|
94
|
+
"Check single-statement suites."
|
|
95
|
+
if not suite: return
|
|
96
|
+
if len(suite) != 1: return
|
|
97
|
+
stmt = suite[0]
|
|
98
|
+
if is_docstring_stmt(stmt): return
|
|
99
|
+
if parent_kind == "else" and isinstance(node, ast.If) and isinstance(stmt, ast.If): return
|
|
100
|
+
if isinstance(stmt, COMPOUND_NODES): return
|
|
101
|
+
if getattr(stmt, "end_lineno", stmt.lineno) > stmt.lineno: return
|
|
102
|
+
header_lineno = getattr(node, "lineno", stmt.lineno)
|
|
103
|
+
if parent_kind in ("else", "finally"): header_lineno = find_suite_header(lines, stmt.lineno, header_lineno, parent_kind)
|
|
104
|
+
if stmt.lineno <= header_lineno: return
|
|
105
|
+
total_len = suite_len(lines, header_lineno, stmt.lineno)
|
|
106
|
+
if total_len is not None and total_len > 130: return
|
|
107
|
+
header_line = lines[header_lineno - 1].rstrip("\n")
|
|
108
|
+
body_line = lines[stmt.lineno - 1].rstrip("\n")
|
|
109
|
+
add_violation(violations, path, header_lineno, f"{parent_kind} single-statement body not one-liner",
|
|
110
|
+
[header_line] if header_lineno == stmt.lineno else [header_line, body_line], suppressed)
|
|
111
|
+
|
|
112
|
+
def check_multiline_sig(node, lines: list[str], path: str, violations: list[tuple], suppressed: set[int]):
|
|
113
|
+
"Check multiline signature/header."
|
|
114
|
+
if not node.body: return
|
|
115
|
+
start = node.lineno
|
|
116
|
+
body_start = node.body[0].lineno
|
|
117
|
+
if not start or not body_start or body_start <= start + 0: return
|
|
118
|
+
end = body_start - 1
|
|
119
|
+
if end <= start: return
|
|
120
|
+
seg_lines = [lines[i - 1].rstrip("\n") for i in range(start, end + 1)]
|
|
121
|
+
if isinstance(node, ast.ClassDef) and any(line.lstrip().startswith("@") for line in seg_lines[1:]): return
|
|
122
|
+
indent = first_line_indent(lines, start)
|
|
123
|
+
if not is_inefficient_multiline(seg_lines, indent): return
|
|
124
|
+
add_violation(violations, path, start, "inefficient multiline signature/header", seg_lines, suppressed)
|
|
125
|
+
|
|
126
|
+
def check_multiline_expr(node, source: str, lines: list[str], path: str, violations: list[tuple], suppressed: set[int]):
|
|
127
|
+
"Check multiline expression layout."
|
|
128
|
+
if not node: return
|
|
129
|
+
if getattr(node, "end_lineno", node.lineno) <= node.lineno: return
|
|
130
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, str): return
|
|
131
|
+
if isinstance(node, ast.JoinedStr): return
|
|
132
|
+
seg_lines = segment_lines(source, node)
|
|
133
|
+
if not seg_lines or len(seg_lines) <= 1: return
|
|
134
|
+
indent = first_line_indent(lines, node.lineno)
|
|
135
|
+
if not is_inefficient_multiline(seg_lines, indent): return
|
|
136
|
+
add_violation(violations, path, node.lineno, "inefficient multiline expression", seg_lines, suppressed)
|
|
137
|
+
|
|
138
|
+
def max_subscript_depth(node, depth: int = 0) -> int:
|
|
139
|
+
"Max subscript depth."
|
|
140
|
+
if node is None: return depth
|
|
141
|
+
if isinstance(node, ast.Subscript):
|
|
142
|
+
depth += 1
|
|
143
|
+
return max(depth, max_subscript_depth(node.value, depth), max_subscript_depth(node.slice, depth))
|
|
144
|
+
depths = [max_subscript_depth(child, depth) for child in ast.iter_child_nodes(node)]
|
|
145
|
+
return max(depths) if depths else depth
|
|
146
|
+
|
|
147
|
+
def is_dataclass_decorator(dec) -> bool:
|
|
148
|
+
"Dataclass decorator."
|
|
149
|
+
if isinstance(dec, ast.Name): return dec.id == "dataclass"
|
|
150
|
+
if isinstance(dec, ast.Attribute): return dec.attr == "dataclass"
|
|
151
|
+
if isinstance(dec, ast.Call): return is_dataclass_decorator(dec.func)
|
|
152
|
+
return False
|
|
153
|
+
|
|
154
|
+
def dataclass_annassigns(tree) -> set:
|
|
155
|
+
"Dataclass annassigns."
|
|
156
|
+
annassigns = set()
|
|
157
|
+
for node in ast.walk(tree):
|
|
158
|
+
if not isinstance(node, ast.ClassDef): continue
|
|
159
|
+
if not any(is_dataclass_decorator(dec) for dec in node.decorator_list): continue
|
|
160
|
+
for stmt in node.body:
|
|
161
|
+
if isinstance(stmt, ast.AnnAssign): annassigns.add(stmt)
|
|
162
|
+
return annassigns
|
|
163
|
+
|
|
164
|
+
def check_annotation(node, source: str, lines: list[str], path: str, violations: list[tuple], suppressed: set[int]):
|
|
165
|
+
"Check annotation depth and layout."
|
|
166
|
+
if node is None: return
|
|
167
|
+
if getattr(node, "end_lineno", node.lineno) > node.lineno:
|
|
168
|
+
seg_lines = segment_lines(source, node)
|
|
169
|
+
indent = first_line_indent(lines, node.lineno)
|
|
170
|
+
if is_inefficient_multiline(seg_lines, indent):
|
|
171
|
+
add_violation(violations, path, node.lineno, "inefficient multiline annotation", seg_lines, suppressed)
|
|
172
|
+
depth = max_subscript_depth(node)
|
|
173
|
+
if depth >= 2:
|
|
174
|
+
add_violation(violations, path, getattr(node, "lineno", 1), f"nested generics depth {depth}",
|
|
175
|
+
node_lines(source, lines, node), suppressed)
|
|
176
|
+
|
|
177
|
+
def should_skip_file(lines: list[str]) -> bool:
|
|
178
|
+
"Skip file."
|
|
179
|
+
head = lines[:5]
|
|
180
|
+
return any("chkstyle: skip" in line for line in head)
|
|
181
|
+
|
|
182
|
+
def suppressed_lines(lines: list[str]) -> set[int]:
|
|
183
|
+
"Suppressed lines."
|
|
184
|
+
suppressed = set()
|
|
185
|
+
off = False
|
|
186
|
+
ignore_next = False
|
|
187
|
+
for lineno, line in enumerate(lines, start=1):
|
|
188
|
+
stripped = line.strip()
|
|
189
|
+
if "chkstyle: on" in line:
|
|
190
|
+
off = False
|
|
191
|
+
ignore_next = False
|
|
192
|
+
continue
|
|
193
|
+
if "chkstyle: off" in line:
|
|
194
|
+
off = True
|
|
195
|
+
ignore_next = False
|
|
196
|
+
suppressed.add(lineno)
|
|
197
|
+
continue
|
|
198
|
+
if off: suppressed.add(lineno)
|
|
199
|
+
if "chkstyle: ignore" in line:
|
|
200
|
+
if stripped.startswith("#"): ignore_next = True
|
|
201
|
+
else: suppressed.add(lineno)
|
|
202
|
+
elif ignore_next and stripped and not stripped.startswith("#"):
|
|
203
|
+
suppressed.add(lineno)
|
|
204
|
+
ignore_next = False
|
|
205
|
+
return suppressed
|
|
206
|
+
|
|
207
|
+
def check_file(path: str) -> list[tuple]:
|
|
208
|
+
"Check file."
|
|
209
|
+
with open(path, encoding="utf-8") as f: source = f.read()
|
|
210
|
+
lines = source.splitlines()
|
|
211
|
+
if should_skip_file(lines): return []
|
|
212
|
+
try: tree = ast.parse(source, filename=path)
|
|
213
|
+
except SyntaxError as e: return [(path, e.lineno or 1, f"syntax error: {e.msg}", [])]
|
|
214
|
+
violations = []
|
|
215
|
+
suppressed = suppressed_lines(lines)
|
|
216
|
+
for lineno, line in enumerate(lines, start=1):
|
|
217
|
+
if len(line) > 150: add_violation(violations, path, lineno, "line >150 chars", [line], suppressed)
|
|
218
|
+
try:
|
|
219
|
+
for tok in tokenize.generate_tokens(io.StringIO(source).readline):
|
|
220
|
+
if tok.type == tokenize.OP and tok.string == ";":
|
|
221
|
+
lineno = tok.start[0]
|
|
222
|
+
add_violation(violations, path, lineno, "semicolon statement separator", [lines[lineno - 1]], suppressed)
|
|
223
|
+
except tokenize.TokenError: pass
|
|
224
|
+
if tree.body and is_docstring_stmt(tree.body[0]):
|
|
225
|
+
check_single_line_docstring(source, lines, tree.body[0], path, violations, suppressed)
|
|
226
|
+
dataclass_fields = dataclass_annassigns(tree)
|
|
227
|
+
for node in ast.walk(tree):
|
|
228
|
+
if isinstance(node, ast.Dict) and len(node.keys) >= 3 and all(is_identifier_str(k) for k in node.keys):
|
|
229
|
+
add_violation(violations, path, node.lineno, "dict literal with 3+ identifier keys",
|
|
230
|
+
node_lines(source, lines, node), suppressed)
|
|
231
|
+
if isinstance(node, ast.AnnAssign):
|
|
232
|
+
if node not in dataclass_fields:
|
|
233
|
+
add_violation(violations, path, node.lineno, "lhs assignment annotation", node_lines(source, lines, node), suppressed)
|
|
234
|
+
check_multiline_expr(node.value, source, lines, path, violations, suppressed)
|
|
235
|
+
check_annotation(node.annotation, source, lines, path, violations, suppressed)
|
|
236
|
+
if isinstance(node, ast.ImportFrom):
|
|
237
|
+
seg = ast.get_source_segment(source, node) or ""
|
|
238
|
+
if "\n" in seg:
|
|
239
|
+
import_lines = node_lines(source, lines, node)
|
|
240
|
+
total_len = sum(len(line.strip()) for line in import_lines)
|
|
241
|
+
if total_len <= 150:
|
|
242
|
+
add_violation(violations, path, node.lineno, "multi-line from-import", import_lines, suppressed)
|
|
243
|
+
has_doc = isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)) and node.body
|
|
244
|
+
if has_doc and is_docstring_stmt(node.body[0]):
|
|
245
|
+
check_single_line_docstring(source, lines, node.body[0], path, violations, suppressed)
|
|
246
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
247
|
+
check_multiline_sig(node, lines, path, violations, suppressed)
|
|
248
|
+
if node.returns: check_annotation(node.returns, source, lines, path, violations, suppressed)
|
|
249
|
+
for arg in node.args.args + node.args.kwonlyargs:
|
|
250
|
+
if arg.annotation: check_annotation(arg.annotation, source, lines, path, violations, suppressed)
|
|
251
|
+
if node.args.vararg and node.args.vararg.annotation:
|
|
252
|
+
check_annotation(node.args.vararg.annotation, source, lines, path, violations, suppressed)
|
|
253
|
+
if node.args.kwarg and node.args.kwarg.annotation:
|
|
254
|
+
check_annotation(node.args.kwarg.annotation, source, lines, path, violations, suppressed)
|
|
255
|
+
if node.args.posonlyargs:
|
|
256
|
+
for arg in node.args.posonlyargs:
|
|
257
|
+
if arg.annotation: check_annotation(arg.annotation, source, lines, path, violations, suppressed)
|
|
258
|
+
if isinstance(node, ast.ClassDef): check_multiline_sig(node, lines, path, violations, suppressed)
|
|
259
|
+
if isinstance(node, ast.Assign): check_multiline_expr(node.value, source, lines, path, violations, suppressed)
|
|
260
|
+
if isinstance(node, ast.AugAssign): check_multiline_expr(node.value, source, lines, path, violations, suppressed)
|
|
261
|
+
if isinstance(node, ast.Return): check_multiline_expr(node.value, source, lines, path, violations, suppressed)
|
|
262
|
+
if isinstance(node, ast.Expr) and not is_docstring_stmt(node):
|
|
263
|
+
check_multiline_expr(node.value, source, lines, path, violations, suppressed)
|
|
264
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
265
|
+
check_suite("def", node, node.body, path, source, lines, violations, suppressed)
|
|
266
|
+
elif isinstance(node, ast.If):
|
|
267
|
+
check_suite("if", node, node.body, path, source, lines, violations, suppressed)
|
|
268
|
+
check_suite("else", node, node.orelse, path, source, lines, violations, suppressed)
|
|
269
|
+
elif isinstance(node, (ast.For, ast.AsyncFor)):
|
|
270
|
+
check_suite("for", node, node.body, path, source, lines, violations, suppressed)
|
|
271
|
+
check_suite("else", node, node.orelse, path, source, lines, violations, suppressed)
|
|
272
|
+
elif isinstance(node, ast.While):
|
|
273
|
+
check_suite("while", node, node.body, path, source, lines, violations, suppressed)
|
|
274
|
+
check_suite("else", node, node.orelse, path, source, lines, violations, suppressed)
|
|
275
|
+
elif isinstance(node, (ast.With, ast.AsyncWith)): check_suite("with", node, node.body, path, source, lines, violations, suppressed)
|
|
276
|
+
elif isinstance(node, ast.Try):
|
|
277
|
+
check_suite("try", node, node.body, path, source, lines, violations, suppressed)
|
|
278
|
+
for handler in node.handlers: check_suite("except", handler, handler.body, path, source, lines, violations, suppressed)
|
|
279
|
+
check_suite("else", node, node.orelse, path, source, lines, violations, suppressed)
|
|
280
|
+
check_suite("finally", node, node.finalbody, path, source, lines, violations, suppressed)
|
|
281
|
+
return violations
|
|
282
|
+
|
|
283
|
+
def main(argv: list[str]) -> int:
|
|
284
|
+
"Main."
|
|
285
|
+
root = argv[1] if len(argv) > 1 else "."
|
|
286
|
+
all_violations = []
|
|
287
|
+
for path in iter_py_files(root): all_violations.extend(check_file(path))
|
|
288
|
+
for path, lineno, msg, lines in sorted(all_violations,
|
|
289
|
+
key=lambda item: (item[0], item[1], item[2])):
|
|
290
|
+
print(f"# {path}:{lineno}: {msg}")
|
|
291
|
+
for line in lines: print(line)
|
|
292
|
+
print(f"found {len(all_violations)} potential violation(s)")
|
|
293
|
+
return 1 if all_violations else 0
|
|
294
|
+
|
|
295
|
+
def cli(): raise SystemExit(main(sys.argv))
|
|
296
|
+
|
|
297
|
+
if __name__ == "__main__": cli()
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "fastaistyle"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Style checker for fast.ai coding conventions"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "Apache-2.0"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [{name = "Jeremy Howard", email = "j@fast.ai"}]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 4 - Beta",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.10",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.scripts]
|
|
23
|
+
chkstyle = "chkstyle:cli"
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://github.com/AnswerDotAI/fastaistyle"
|
|
27
|
+
|
|
28
|
+
[tool.hatch.build.targets.wheel]
|
|
29
|
+
packages = ["."]
|
|
30
|
+
only-include = ["chkstyle.py"]
|
|
31
|
+
|
|
32
|
+
[tool.hatch.version]
|
|
33
|
+
path = "chkstyle.py"
|
|
34
|
+
|
|
35
|
+
[tool.pytest.ini_options]
|
|
36
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Repo Style
|
|
2
|
+
|
|
3
|
+
Keep the code compact, readable, and consistent with the surrounding file. Prefer clarity over strict formatting rules. Do not follow PEP-8.
|
|
4
|
+
|
|
5
|
+
## Layout
|
|
6
|
+
- Aim to avoid >140 chars on a line.
|
|
7
|
+
- Keep one logical idea per line.
|
|
8
|
+
- Never use `;` to combine multiple statements on a line
|
|
9
|
+
- Avoid inefficient multi-line expressions/signatures: if the combined stripped length would fit in fewer ~120-char lines, keep it tighter.
|
|
10
|
+
- Single-line bodies are preferred for short `with`, `open`, `try`, `except`, `catch`, `for`, and `if` statements, plus small one-line functions.
|
|
11
|
+
- Good: `if not data: return None`
|
|
12
|
+
- Bad: `if not data:\n return None`
|
|
13
|
+
- Single-line bodies are preferred for small one-line functions that don't need a docstring (i.e because they're private or a dunder function).
|
|
14
|
+
- Good: `def _is_ready(self): return self._ready.is_set()`
|
|
15
|
+
- Bad: `def _is_ready(self):\n return self._ready.is_set()`
|
|
16
|
+
- Be frugal with vertical whitespace.
|
|
17
|
+
- Avoid nearly all comments, unless really required to explain an otherwise-obscure issue.
|
|
18
|
+
- Indent with 4 spaces; avoid trailing whitespace.
|
|
19
|
+
- Avoid auto-formatters that rewrite layout.
|
|
20
|
+
- Group imports; multiple modules on one line is preferred.
|
|
21
|
+
- Good: `import json, os, time`
|
|
22
|
+
- Bad: `import json\nimport os\nimport time`
|
|
23
|
+
- Good: `from mymod import (\n a,\n b\n)`
|
|
24
|
+
- Bad: `from mymod import a,b`
|
|
25
|
+
|
|
26
|
+
## Naming
|
|
27
|
+
- Use standard Python casing.
|
|
28
|
+
- Prefer short, conventional names for frequently used values; follow existing abbreviations.
|
|
29
|
+
- Good: `msg_id`, `iopub`, `ctx`, `i`
|
|
30
|
+
- Bad: `message_identifier`, `io_pub_channel`, `context_object`, `loop_index`
|
|
31
|
+
|
|
32
|
+
## Structure
|
|
33
|
+
- Favor small helpers over repetitive blocks.
|
|
34
|
+
- Good: `def _send_stream(self, name, text): ...; self._send_stream("stdout", out)`
|
|
35
|
+
- Bad: `self._send(sock, "stream", {"name": "stdout", "text": out}, parent)`
|
|
36
|
+
- Use comprehensions or inline expressions when they improve clarity.
|
|
37
|
+
- Good: `ids = [m["header"]["msg_id"] for m in msgs]`
|
|
38
|
+
- Bad: `ids = []\nfor m in msgs: ids.append(m["header"]["msg_id"])`
|
|
39
|
+
|
|
40
|
+
## Typing
|
|
41
|
+
- Never add type annotations to LHS assignments (e.g. `x: int = 1`), except dataclass fields.
|
|
42
|
+
- Keep annotations simple; avoid nested generics beyond one level unless you have a strong reason.
|
|
43
|
+
|
|
44
|
+
## Documentation
|
|
45
|
+
- Use brief single-line docstrings for multi-line functions.
|
|
46
|
+
- Good: `def run(self):\n "Run the thread."\n ...`
|
|
47
|
+
- Bad: `def run(self):\n \"\"\"\n Run the thread.\n \"\"\"\n ...`
|
|
48
|
+
- Add comments only when they explain why. That means add VERY few comments!
|
|
49
|
+
- Good: `self._stop_event.set() # ensures poll loop exits before socket close`
|
|
50
|
+
- Bad: `self._stop_event.set() # stop the event`
|
|
51
|
+
|
|
52
|
+
## Testing
|
|
53
|
+
- All tests must pass before changes are considered complete.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# chkstyle: skip
|
|
2
|
+
import textwrap
|
|
3
|
+
|
|
4
|
+
import chkstyle
|
|
5
|
+
|
|
6
|
+
def _write(tmp_path, name: str, content: str):
|
|
7
|
+
path = tmp_path / name
|
|
8
|
+
path.write_text(textwrap.dedent(content).lstrip(), encoding="utf-8")
|
|
9
|
+
return path
|
|
10
|
+
|
|
11
|
+
def _messages(violations):
|
|
12
|
+
return {msg for _path, _lineno, msg, _lines in violations}
|
|
13
|
+
|
|
14
|
+
def test_chkstyle_reports_expected_violations(tmp_path):
|
|
15
|
+
path = _write(
|
|
16
|
+
tmp_path,
|
|
17
|
+
"cases.py",
|
|
18
|
+
'''
|
|
19
|
+
def f():
|
|
20
|
+
"""doc"""
|
|
21
|
+
return 1
|
|
22
|
+
|
|
23
|
+
x: int = 1
|
|
24
|
+
data = {"a": 1, "b": 2, "c": 3}
|
|
25
|
+
a = 1; b = 2
|
|
26
|
+
from os import (
|
|
27
|
+
path,
|
|
28
|
+
environ,
|
|
29
|
+
)
|
|
30
|
+
if True:
|
|
31
|
+
y = 1
|
|
32
|
+
z = dict(
|
|
33
|
+
a=1,
|
|
34
|
+
b=2,
|
|
35
|
+
)
|
|
36
|
+
long = "......................................................................................................................................................."
|
|
37
|
+
def g(x: list[list[int]]): return x
|
|
38
|
+
''',
|
|
39
|
+
)
|
|
40
|
+
msgs = _messages(chkstyle.check_file(str(path)))
|
|
41
|
+
expected = {
|
|
42
|
+
"single-line docstring uses triple quotes",
|
|
43
|
+
"lhs assignment annotation",
|
|
44
|
+
"dict literal with 3+ identifier keys",
|
|
45
|
+
"semicolon statement separator",
|
|
46
|
+
"multi-line from-import",
|
|
47
|
+
"if single-statement body not one-liner",
|
|
48
|
+
"inefficient multiline expression",
|
|
49
|
+
"line >150 chars",
|
|
50
|
+
"nested generics depth 2",
|
|
51
|
+
}
|
|
52
|
+
assert expected.issubset(msgs)
|
|
53
|
+
|
|
54
|
+
def test_chkstyle_ignore_and_off_on(tmp_path):
|
|
55
|
+
path = _write(
|
|
56
|
+
tmp_path,
|
|
57
|
+
"ignore.py",
|
|
58
|
+
"""
|
|
59
|
+
x: int = 1 # chkstyle: ignore
|
|
60
|
+
# chkstyle: ignore
|
|
61
|
+
y: int = 2
|
|
62
|
+
# chkstyle: off
|
|
63
|
+
z: int = 3
|
|
64
|
+
# chkstyle: on
|
|
65
|
+
""",
|
|
66
|
+
)
|
|
67
|
+
assert chkstyle.check_file(str(path)) == []
|
|
68
|
+
|
|
69
|
+
def test_chkstyle_skip_file(tmp_path):
|
|
70
|
+
path = _write(
|
|
71
|
+
tmp_path,
|
|
72
|
+
"skip.py",
|
|
73
|
+
"""
|
|
74
|
+
# chkstyle: skip
|
|
75
|
+
x: int = 1
|
|
76
|
+
data = {"a": 1, "b": 2, "c": 3}
|
|
77
|
+
""",
|
|
78
|
+
)
|
|
79
|
+
assert chkstyle.check_file(str(path)) == []
|
|
80
|
+
|
|
81
|
+
def test_chkstyle_allows_multiline_strings(tmp_path):
|
|
82
|
+
path = _write(
|
|
83
|
+
tmp_path,
|
|
84
|
+
"strings.py",
|
|
85
|
+
'''
|
|
86
|
+
value = """
|
|
87
|
+
line one
|
|
88
|
+
line two
|
|
89
|
+
"""
|
|
90
|
+
''',
|
|
91
|
+
)
|
|
92
|
+
assert chkstyle.check_file(str(path)) == []
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -e
|
|
3
|
+
|
|
4
|
+
bump=${1:-patch}
|
|
5
|
+
version=$(hatch version)
|
|
6
|
+
|
|
7
|
+
git add -A
|
|
8
|
+
git commit -m "v$version" || true # ok if nothing to commit
|
|
9
|
+
git tag "v$version"
|
|
10
|
+
git push
|
|
11
|
+
git push --tags
|
|
12
|
+
|
|
13
|
+
echo "Released v$version"
|
|
14
|
+
|
|
15
|
+
hatch version $bump
|
|
16
|
+
git add -A
|
|
17
|
+
git commit -m "bump to $(hatch version)"
|
|
18
|
+
git push
|
|
19
|
+
|
|
20
|
+
echo "Dev now at $(hatch version)"
|