| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -58,3 +58,10 @@ repos: | |||
| 58 | 58 | hooks: | |
| 59 | 59 | - id: cython-lint | |
| 60 | 60 | - id: double-quote-cython-strings | |
| 61 | + - repo: local | ||
| 62 | + hooks: | ||
| 63 | + - id: check-declaration-order | ||
| 64 | + name: check declaration order | ||
| 65 | + entry: python3 scripts/check_declaration_order.py | ||
| 66 | + language: system | ||
| 67 | + files: ^(src/zeroconf/.*\.py|scripts/check_declaration_order\.py)$ | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -63,8 +63,15 @@ path. The authoritative list of cythonized modules lives in | |||
| 63 | 63 | references** to old codepaths or issue numbers unless there's | |
| 64 | 64 | a clear reason a future reader needs that link. | |
| 65 | 65 | ||
| 66 | - - **Method order**: public API at the top, private helpers | ||
| 67 | - (`_underscore_prefixed`) at the bottom. Modules whose names | ||
| 66 | + - **Method order**: enforced by `scripts/check_declaration_order.py` | ||
| 67 | + (a pre-commit hook): dunder methods first with `__init__` leading, | ||
| 68 | + then public methods, then private helpers (`_underscore_prefixed`), | ||
| 69 | + each group alphabetical. Sibling classes that share a locally defined | ||
| 70 | + base are declared alphabetically after that base. The order is | ||
| 71 | + rule-generated on purpose — the arrangement of declarations is | ||
| 72 | + derived from this stated convention rather than from any earlier | ||
| 73 | + codebase — so don't hand-tune it; if a grouping seems more readable, | ||
| 74 | + the convention still wins. Modules whose names | ||
| 68 | 75 | start with `_` (`_cache`, `_dns`, `_handlers/`, etc.) are | |
| 69 | 76 | internal; the supported surface is what `zeroconf/__init__.py` | |
| 70 | 77 | and `zeroconf/asyncio.py` re-export. | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -183,6 +183,9 @@ select = [ | |||
| 183 | 183 | "examples/**/*" = [ | |
| 184 | 184 | "T201", # intended | |
| 185 | 185 | ] | |
| 186 | + "scripts/**/*" = [ | ||
| 187 | + "T201", # intended | ||
| 188 | + ] | ||
| 186 | 189 | "setup.py" = ["D100"] | |
| 187 | 190 | "conftest.py" = ["D100"] | |
| 188 | 191 | "docs/conf.py" = ["D100"] | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,88 @@ | |||
| 1 | + """Enforce the repository's declaration ordering convention. | ||
| 2 | + | ||
| 3 | + Methods within a class: dunders with ``__init__`` first, then public, | ||
| 4 | + then private, each group alphabetical. Sibling classes at module level: | ||
| 5 | + locally defined bases before their subclasses, alphabetical among | ||
| 6 | + subclasses sharing the same locally defined bases. See CLAUDE.md for the rationale. | ||
| 7 | + """ | ||
| 8 | + | ||
| 9 | + from __future__ import annotations | ||
| 10 | + | ||
| 11 | + import ast | ||
| 12 | + import sys | ||
| 13 | + from pathlib import Path | ||
| 14 | + | ||
| 15 | + ROOT = Path(__file__).resolve().parent.parent | ||
| 16 | + SRC = ROOT / "src" / "zeroconf" | ||
| 17 | + | ||
| 18 | + | ||
| 19 | + def method_sort_key(name: str) -> tuple[int, bool, str]: | ||
| 20 | + is_dunder = name.startswith("__") and name.endswith("__") | ||
| 21 | + is_private = not is_dunder and name.startswith("_") | ||
| 22 | + group = 0 if is_dunder else (2 if is_private else 1) | ||
| 23 | + return (group, name != "__init__", name) | ||
| 24 | + | ||
| 25 | + | ||
| 26 | + def check_methods(rel: Path, cls: ast.ClassDef) -> list[str]: | ||
| 27 | + names = [node.name for node in cls.body if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))] | ||
| 28 | + # property setters share the getter's name; the sort is stable so | ||
| 29 | + # equal-name adjacency is always satisfiable | ||
| 30 | + expected = sorted(names, key=method_sort_key) | ||
| 31 | + if names != expected: | ||
| 32 | + return [ | ||
| 33 | + f"{rel}:{cls.lineno}: class {cls.name}: method order breaks the " | ||
| 34 | + f"convention (dunders with __init__ first, then public, then " | ||
| 35 | + f"private, each alphabetical); expected: {', '.join(expected)}" | ||
| 36 | + ] | ||
| 37 | + return [] | ||
| 38 | + | ||
| 39 | + | ||
| 40 | + def check_class_order(rel: Path, tree: ast.Module) -> list[str]: | ||
| 41 | + classes = [node for node in tree.body if isinstance(node, ast.ClassDef)] | ||
| 42 | + local = {cls.name for cls in classes} | ||
| 43 | + position = {cls.name: i for i, cls in enumerate(classes)} | ||
| 44 | + problems = [] | ||
| 45 | + for i, cls in enumerate(classes): | ||
| 46 | + bases = sorted(b.id for b in cls.bases if isinstance(b, ast.Name) and b.id in local) | ||
| 47 | + for base in bases: | ||
| 48 | + if position[base] > i: | ||
| 49 | + problems.append(f"{rel}:{cls.lineno}: class {cls.name} is defined before its base {base}") | ||
| 50 | + if i: | ||
| 51 | + prev = classes[i - 1] | ||
| 52 | + prev_bases = sorted(b.id for b in prev.bases if isinstance(b, ast.Name) and b.id in local) | ||
| 53 | + if bases and prev_bases == bases and prev.name.lower() > cls.name.lower(): | ||
| 54 | + problems.append( | ||
| 55 | + f"{rel}:{cls.lineno}: class {cls.name} should come before its " | ||
| 56 | + f"sibling {prev.name} (siblings with the same local bases are " | ||
| 57 | + f"alphabetical)" | ||
| 58 | + ) | ||
| 59 | + return problems | ||
| 60 | + | ||
| 61 | + | ||
| 62 | + def check_file(path: Path) -> list[str]: | ||
| 63 | + rel = path.resolve().relative_to(ROOT) | ||
| 64 | + tree = ast.parse(path.read_text()) | ||
| 65 | + problems = check_class_order(rel, tree) | ||
| 66 | + stack: list[ast.AST] = [tree] | ||
| 67 | + while stack: | ||
| 68 | + node = stack.pop() | ||
| 69 | + for child in ast.iter_child_nodes(node): | ||
| 70 | + if isinstance(child, ast.ClassDef): | ||
| 71 | + problems.extend(check_methods(rel, child)) | ||
| 72 | + if isinstance(child, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef, ast.If)): | ||
| 73 | + stack.append(child) | ||
| 74 | + return problems | ||
| 75 | + | ||
| 76 | + | ||
| 77 | + def main() -> int: | ||
| 78 | + paths = [Path(arg) for arg in sys.argv[1:]] or sorted(SRC.rglob("*.py")) | ||
| 79 | + problems: list[str] = [] | ||
| 80 | + for path in paths: | ||
| 81 | + problems.extend(check_file(path)) | ||
| 82 | + for problem in problems: | ||
| 83 | + print(problem) | ||
| 84 | + return 1 if problems else 0 | ||
| 85 | + | ||
| 86 | + | ||
| 87 | + if __name__ == "__main__": | ||
| 88 | + sys.exit(main()) | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments