FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

chore: enforce the declaration ordering convention in ci by bdraco · Pull Request #1887 · python-zeroconf/python-zeroconf · GitHub

Repository navigation

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension .md  (1) .py  (16) .toml  (1) .yaml  (1) All 4 file types selected
Only manifest files
Viewed files
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Unified
Split
Hide whitespace
Diff view
Unified
Split
Hide whitespace
7 changes: 7 additions & 0 deletions .pre-commit-config.yaml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,10 @@ repos:
hooks:
- id: cython-lint
- id: double-quote-cython-strings
- repo: local
hooks:
- id: check-declaration-order
name: check declaration order
entry: python3 scripts/check_declaration_order.py
language: system
files: ^(src/zeroconf/.*\.py|scripts/check_declaration_order\.py)$
11 changes: 9 additions & 2 deletions CLAUDE.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,15 @@ path. The authoritative list of cythonized modules lives in
references** to old codepaths or issue numbers unless there's
a clear reason a future reader needs that link.

- **Method order**: public API at the top, private helpers
(`_underscore_prefixed`) at the bottom. Modules whose names
- **Method order**: enforced by `scripts/check_declaration_order.py`
(a pre-commit hook): dunder methods first with `__init__` leading,
then public methods, then private helpers (`_underscore_prefixed`),
each group alphabetical. Sibling classes that share a locally defined
base are declared alphabetically after that base. The order is
rule-generated on purpose — the arrangement of declarations is
derived from this stated convention rather than from any earlier
codebase — so don't hand-tune it; if a grouping seems more readable,
the convention still wins. Modules whose names
start with `_` (`_cache`, `_dns`, `_handlers/`, etc.) are
internal; the supported surface is what `zeroconf/__init__.py`
and `zeroconf/asyncio.py` re-export.
Expand Down
3 changes: 3 additions & 0 deletions pyproject.toml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,9 @@ select = [
"examples/**/*" = [
"T201", # intended
]
"scripts/**/*" = [
"T201", # intended
]
"setup.py" = ["D100"]
"conftest.py" = ["D100"]
"docs/conf.py" = ["D100"]
Expand Down
88 changes: 88 additions & 0 deletions scripts/check_declaration_order.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
"""Enforce the repository's declaration ordering convention.

Methods within a class: dunders with ``__init__`` first, then public,
then private, each group alphabetical. Sibling classes at module level:
locally defined bases before their subclasses, alphabetical among
subclasses sharing the same locally defined bases. See CLAUDE.md for the rationale.
"""

from __future__ import annotations

import ast
import sys
from pathlib import Path

ROOT = Path(__file__).resolve().parent.parent
SRC = ROOT / "src" / "zeroconf"


def method_sort_key(name: str) -> tuple[int, bool, str]:
is_dunder = name.startswith("__") and name.endswith("__")
is_private = not is_dunder and name.startswith("_")
group = 0 if is_dunder else (2 if is_private else 1)
return (group, name != "__init__", name)


def check_methods(rel: Path, cls: ast.ClassDef) -> list[str]:
names = [node.name for node in cls.body if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))]
# property setters share the getter's name; the sort is stable so
# equal-name adjacency is always satisfiable
expected = sorted(names, key=method_sort_key)
if names != expected:
return [
f"{rel}:{cls.lineno}: class {cls.name}: method order breaks the "
f"convention (dunders with __init__ first, then public, then "
f"private, each alphabetical); expected: {', '.join(expected)}"
]
return []


def check_class_order(rel: Path, tree: ast.Module) -> list[str]:
classes = [node for node in tree.body if isinstance(node, ast.ClassDef)]
local = {cls.name for cls in classes}
position = {cls.name: i for i, cls in enumerate(classes)}
problems = []
for i, cls in enumerate(classes):
bases = sorted(b.id for b in cls.bases if isinstance(b, ast.Name) and b.id in local)
for base in bases:
if position[base] > i:
problems.append(f"{rel}:{cls.lineno}: class {cls.name} is defined before its base {base}")
if i:
prev = classes[i - 1]
prev_bases = sorted(b.id for b in prev.bases if isinstance(b, ast.Name) and b.id in local)
if bases and prev_bases == bases and prev.name.lower() > cls.name.lower():
problems.append(
f"{rel}:{cls.lineno}: class {cls.name} should come before its "
f"sibling {prev.name} (siblings with the same local bases are "
f"alphabetical)"
)
return problems


def check_file(path: Path) -> list[str]:
rel = path.resolve().relative_to(ROOT)
tree = ast.parse(path.read_text())
problems = check_class_order(rel, tree)
stack: list[ast.AST] = [tree]
while stack:
node = stack.pop()
for child in ast.iter_child_nodes(node):
if isinstance(child, ast.ClassDef):
problems.extend(check_methods(rel, child))
if isinstance(child, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef, ast.If)):
stack.append(child)
return problems


def main() -> int:
paths = [Path(arg) for arg in sys.argv[1:]] or sorted(SRC.rglob("*.py"))
problems: list[str] = []
for path in paths:
problems.extend(check_file(path))
for problem in problems:
print(problem)
return 1 if problems else 0


if __name__ == "__main__":
sys.exit(main())
Loading
Loading

Back | FazBrowse Home | New Git URL