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.
@@ -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)"