scripts/interface_privacy.py (68 lines of code) (raw):
#!/usr/bin/env python3
# TODO: We want to go the other way around too
# In other words, finding underscoreless functions which are not accesssed externally
import ast
import enum
import pathlib
import sys
class ExitCode(enum.IntEnum):
"""Exit codes for the script."""
SUCCESS = 0
FAILURE = 1
USAGE_ERROR = 2
class PrivateAccessVisitor(ast.NodeVisitor):
"""Visits AST nodes to find external access to private attributes."""
def __init__(self, filename: str) -> None:
"""Construct a visitor."""
super().__init__()
self.filename: str = filename
self.violations: list[tuple[int, int, str]] = []
def visit_attribute(self, node: ast.Attribute) -> None:
"""Visits ast.Attribute nodes."""
# Check whether the attribute name starts with a single underscore
if node.attr.startswith("_") and (not node.attr.startswith("__")):
# Exclude "cls" and "self"
if isinstance(node.value, ast.Name) and (node.value.id not in {"cls", "self"}):
accessed_name = f"{node.value.id}.{node.attr}"
self.violations.append((node.lineno, node.col_offset, accessed_name))
self.generic_visit(node)
def _parse_python_code(code: str, filename: str) -> ast.Module | None:
"""Parses Python code string into an AST module."""
try:
return ast.parse(code, filename=filename)
except SyntaxError as e:
print(f"!! {filename} - invalid syntax: {e}", file=sys.stderr)
return None
def _read_file_content(file_path: pathlib.Path) -> str | None:
"""Reads the content of a file."""
try:
return file_path.read_text(encoding="utf-8")
except FileNotFoundError:
print(f"!! {file_path} - file not found", file=sys.stderr)
return None
except OSError:
print(f"!! {file_path} - could not read file", file=sys.stderr)
return None
def main() -> None:
"""Main entry point for the script."""
quiet = sys.argv[2:3] == ["--quiet"]
argc = len(sys.argv)
match (argc, quiet):
case (2, False):
...
case (3, True):
...
case _:
print(f"Usage: {sys.argv[0]} <filename.py> [ --quiet ]", file=sys.stderr)
sys.exit(ExitCode.USAGE_ERROR)
file_path = pathlib.Path(sys.argv[1])
filename = str(file_path)
if not file_path.is_file() or (not filename.endswith(".py")):
print(f"!! {filename} - invalid file", file=sys.stderr)
sys.exit(ExitCode.USAGE_ERROR)
content = _read_file_content(file_path)
if content is None:
sys.exit(ExitCode.FAILURE)
tree = _parse_python_code(content, filename)
if tree is None:
sys.exit(ExitCode.FAILURE)
visitor = PrivateAccessVisitor(filename)
visitor.visit(tree)
if visitor.violations:
# print(f"!! {filename} - found violations of private attribute access")
for lineno, col, name in visitor.violations:
print(f"!! {filename}:{lineno}:{col} - access to {name}")
sys.exit(ExitCode.FAILURE)
else:
if not quiet:
print(f"ok {filename}")
sys.exit(ExitCode.SUCCESS)
if __name__ == "__main__":
main()