Skip to content

Commit 0432bc4

Browse files
authored
Add initial flake8 HTK rules plugin (#1)
## Summary - extract the accounts-django structured-programming checker shape into the standalone plugin - add `SP100` and `SP101`, gated by `--structured-programming-files` - broaden `SP101` so literal displays match the public docs - add datetime clarity rules `DT100`-`DT102` - add debugger prevention rules `DB100` and `DB101` for debugger imports and calls - add naming rule `NM100` to warn on vague `get_` function/method prefixes - split check implementation into thematic modules under `src/flake8_htk_rules/checks/` - add real Flake8 entry-point integration coverage and CI flake8 linting - update README/package metadata and CI branch targeting for `master` - remove internal decision-log material from the public OSS package ## Verification - temp virtualenv: `python -m unittest discover -s tests -v` -> 20 passed - temp virtualenv: `python -m pytest` -> 20 passed - temp virtualenv: `python -m flake8 src tests` - temp virtualenv: `python -m build --wheel` and wheel contents include `flake8_htk_rules/checks/` modules - integration test runs `python -m flake8` and verifies `DT100`, `SP100`, `SP101`, `DB100`, `DB101`, and `NM100` via the installed entry point
1 parent 5eee46b commit 0432bc4

14 files changed

Lines changed: 887 additions & 0 deletions

File tree

.github/workflows/ci.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [master]
6+
pull_request:
7+
8+
jobs:
9+
test:
10+
runs-on: ubuntu-latest
11+
strategy:
12+
fail-fast: false
13+
matrix:
14+
python-version: ["3.9", "3.10", "3.11", "3.12"]
15+
16+
steps:
17+
- uses: actions/checkout@v4
18+
- uses: actions/setup-python@v5
19+
with:
20+
python-version: ${{ matrix.python-version }}
21+
- run: python -m pip install --upgrade pip
22+
- run: python -m pip install -e ".[dev]"
23+
- run: python -m unittest discover -s tests -v
24+
- run: python -m flake8 src tests

.gitignore

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
.coverage
2+
.mypy_cache/
3+
.pytest_cache/
4+
.ruff_cache/
5+
.tox/
6+
build/
7+
dist/
8+
*.egg-info/
9+
__pycache__/
10+
*.py[cod]
11+
.venv/
12+
venv/
13+

LICENSE

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
MIT License
2+
3+
Copyright (c) 2026 Hacktoolkit
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.
22+

README.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,70 @@
11
# flake8-htk-rules
2+
3+
Hacktoolkit Flake8 rules for structured Python code, datetime clarity,
4+
debugger prevention, and naming precision.
5+
6+
## Installation
7+
8+
```bash
9+
pip install flake8-htk-rules
10+
```
11+
12+
For local development:
13+
14+
```bash
15+
python -m pip install -e ".[dev]"
16+
python -m unittest discover -s tests -v
17+
```
18+
19+
## Rules
20+
21+
| Code | Description |
22+
| --- | --- |
23+
| `SP100` | Functions in configured files should prefer a single return statement. |
24+
| `SP101` | Return values in configured files should be simple variables, attributes, literals, or bare returns. |
25+
| `DT100` | Use `import datetime` instead of `from datetime import datetime`. |
26+
| `DT101` | Use `import datetime` instead of `from datetime import date`. |
27+
| `DT102` | Use `import datetime` instead of `from datetime import timedelta`. |
28+
| `DB100` | Do not commit debugger imports such as `import pdb` or `from pdb import set_trace`. |
29+
| `DB101` | Do not commit debugger calls such as `breakpoint()` or `pdb.set_trace()`. |
30+
| `NM100` | Avoid the vague `get_` function or method prefix; choose a more precise verb. |
31+
32+
## Flake8 Configuration
33+
34+
Enable the rules:
35+
36+
```ini
37+
[flake8]
38+
select = SP,DT,DB,NM
39+
structured-programming-files =
40+
accounts/services.py
41+
accounts/view_helpers.py
42+
accounts/views.py
43+
```
44+
45+
Or combine with existing checks:
46+
47+
```ini
48+
[flake8]
49+
extend-select = SP,DT,DB,NM
50+
structured-programming-files =
51+
accounts/services.py
52+
accounts/view_helpers.py
53+
accounts/views.py
54+
```
55+
56+
The `SP` rules are gated by `structured-programming-files` so teams can roll them
57+
out on a targeted set of modules. The `DT`, `DB`, and `NM` rules are always
58+
active when selected.
59+
60+
## Development
61+
62+
The plugin uses a single Flake8 entry point and delegates rule logic to
63+
family modules under `src/flake8_htk_rules/checks/`. Add new rule families
64+
there and cover them in `tests/`.
65+
66+
Run the test suite without installing the package:
67+
68+
```bash
69+
PYTHONPATH=src python -m unittest discover -s tests -v
70+
```

pyproject.toml

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
[build-system]
2+
requires = ["hatchling>=1.21"]
3+
build-backend = "hatchling.build"
4+
5+
[project]
6+
name = "flake8-htk-rules"
7+
version = "0.1.0"
8+
description = "Hacktoolkit Flake8 rules for structured Python, datetime clarity, debugger prevention, and naming precision."
9+
readme = "README.md"
10+
requires-python = ">=3.9"
11+
license = "MIT"
12+
authors = [
13+
{ name = "Hacktoolkit" },
14+
{ name = "Jonathan Tsai" }
15+
]
16+
classifiers = [
17+
"Development Status :: 3 - Alpha",
18+
"Environment :: Console",
19+
"Framework :: Flake8",
20+
"Intended Audience :: Developers",
21+
"License :: OSI Approved :: MIT License",
22+
"Programming Language :: Python :: 3",
23+
"Programming Language :: Python :: 3 :: Only",
24+
"Topic :: Software Development :: Quality Assurance",
25+
]
26+
dependencies = [
27+
"flake8>=5",
28+
]
29+
30+
[project.optional-dependencies]
31+
dev = [
32+
"build>=1.0",
33+
"flake8>=5",
34+
"pytest>=7",
35+
]
36+
37+
[project.urls]
38+
Homepage = "https://github.com/hacktoolkit/flake8-htk-rules"
39+
Issues = "https://github.com/hacktoolkit/flake8-htk-rules/issues"
40+
41+
[project.entry-points."flake8.extension"]
42+
HTK = "flake8_htk_rules:Plugin"
43+
44+
[tool.hatch.build.targets.wheel]
45+
packages = ["src/flake8_htk_rules"]
46+

src/flake8_htk_rules/__init__.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
"""Flake8 entry point for Hacktoolkit rules."""
2+
3+
from __future__ import annotations
4+
5+
import ast
6+
from collections.abc import Iterator
7+
8+
from .checks import HtkVisitor
9+
10+
__version__ = "0.1.0"
11+
12+
13+
class Plugin:
14+
"""Flake8 AST plugin entry point."""
15+
16+
name = "flake8-htk-rules"
17+
version = __version__
18+
structured_programming_files: tuple[str, ...] = ()
19+
20+
def __init__(self, tree: ast.AST, filename: str = "<unknown>") -> None:
21+
self.tree = tree
22+
self.filename = filename
23+
24+
@classmethod
25+
def add_options(cls, parser) -> None:
26+
parser.add_option(
27+
"--structured-programming-files",
28+
parse_from_config=True,
29+
comma_separated_list=True,
30+
default=[],
31+
help=(
32+
"Comma-separated file globs for structured programming "
33+
"checks."
34+
),
35+
)
36+
37+
@classmethod
38+
def parse_options(cls, options) -> None:
39+
cls.structured_programming_files = tuple(
40+
pattern.strip()
41+
for pattern in getattr(options, "structured_programming_files", [])
42+
if pattern.strip()
43+
)
44+
45+
def run(self) -> Iterator[tuple[int, int, str, type["Plugin"]]]:
46+
visitor = HtkVisitor(
47+
filename=self.filename,
48+
structured_programming_files=self.structured_programming_files,
49+
)
50+
visitor.visit(self.tree)
51+
for violation in visitor.violations:
52+
yield (
53+
violation.line,
54+
violation.column,
55+
violation.message,
56+
type(self),
57+
)
58+
59+
60+
__all__ = ["Plugin", "__version__"]
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
"""Check orchestration for Hacktoolkit Flake8 rules."""
2+
3+
from __future__ import annotations
4+
5+
import ast
6+
7+
from . import datetime as datetime_checks
8+
from . import debugger, naming, structured
9+
from .types import Violation
10+
11+
12+
class HtkVisitor(ast.NodeVisitor):
13+
"""Collect Hacktoolkit rule violations from a Python AST."""
14+
15+
def __init__(
16+
self,
17+
*,
18+
filename: str,
19+
structured_programming_files: tuple[str, ...] = (),
20+
) -> None:
21+
self.filename = filename
22+
self.structured_programming_files = structured_programming_files
23+
self.violations: list[Violation] = []
24+
self._debugger_state = debugger.DebuggerState()
25+
26+
def visit_Import(self, node: ast.Import) -> None:
27+
for violation_node, message in debugger.check_import(
28+
node,
29+
self._debugger_state,
30+
):
31+
self._add(violation_node, node, message)
32+
self.generic_visit(node)
33+
34+
def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
35+
for violation_node, message in datetime_checks.check_import_from(node):
36+
self._add(violation_node, node, message)
37+
for violation_node, message in debugger.check_import_from(
38+
node,
39+
self._debugger_state,
40+
):
41+
self._add(violation_node, node, message)
42+
self.generic_visit(node)
43+
44+
def visit_Call(self, node: ast.Call) -> None:
45+
for violation_node, message in debugger.check_call(
46+
node,
47+
self._debugger_state,
48+
):
49+
self._add(violation_node, node, message)
50+
self.generic_visit(node)
51+
52+
def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
53+
self._check_function(node)
54+
self.generic_visit(node)
55+
56+
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None:
57+
self._check_function(node)
58+
self.generic_visit(node)
59+
60+
def _check_function(
61+
self,
62+
node: ast.FunctionDef | ast.AsyncFunctionDef,
63+
) -> None:
64+
for violation_node, message in naming.check_function(node):
65+
self._add(violation_node, node, message)
66+
67+
if not structured.should_check_file(
68+
self.filename,
69+
self.structured_programming_files,
70+
):
71+
return
72+
73+
for violation_node, message in structured.check_function(node):
74+
self._add(violation_node, node, message)
75+
76+
def _add(self, node: ast.AST, fallback: ast.AST, message: str) -> None:
77+
self.violations.append(
78+
Violation(
79+
line=getattr(node, "lineno", getattr(fallback, "lineno", 1)),
80+
column=getattr(
81+
node,
82+
"col_offset",
83+
getattr(fallback, "col_offset", 0),
84+
),
85+
message=message,
86+
)
87+
)
88+
89+
90+
__all__ = ["HtkVisitor", "Violation"]
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"""Datetime clarity checks."""
2+
3+
from __future__ import annotations
4+
5+
import ast
6+
7+
8+
DT100 = (
9+
"DT100 use 'import datetime' instead of "
10+
"'from datetime import datetime'"
11+
)
12+
DT101 = (
13+
"DT101 use 'import datetime' instead of "
14+
"'from datetime import date'"
15+
)
16+
DT102 = (
17+
"DT102 use 'import datetime' instead of "
18+
"'from datetime import timedelta'"
19+
)
20+
21+
DATETIME_IMPORT_MESSAGES = {
22+
"datetime": DT100,
23+
"date": DT101,
24+
"timedelta": DT102,
25+
}
26+
27+
28+
def check_import_from(node: ast.ImportFrom) -> list[tuple[ast.AST, str]]:
29+
if node.module != "datetime" or node.level != 0:
30+
return []
31+
32+
violations = []
33+
for alias in node.names:
34+
message = DATETIME_IMPORT_MESSAGES.get(alias.name)
35+
if message is not None:
36+
violations.append((alias, message))
37+
return violations

0 commit comments

Comments
 (0)