Files
wehub-resource-sync 7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

218 lines
7.7 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Pre-commit hook to enforce using pathlib.Path instead of os.path.
This hook checks for os.path usage in Python files and suggests
using pathlib.Path instead for better cross-platform compatibility
and more modern Python code.
"""
import argparse
import ast
import sys
from pathlib import Path
class OsPathChecker(ast.NodeVisitor):
"""AST visitor to find os.path usage."""
def __init__(self, filename: str):
self.filename = filename
self.violations: list[tuple[int, str]] = []
# Local names bound to the ``os`` module. Includes aliases from
# ``import os as <alias>`` so they cannot bypass the check.
self.os_module_names: set[str] = set()
self.has_os_path_import = False
def visit_Import(self, node: ast.Import) -> None:
"""Check for 'import os' statements (incl. ``import os as <alias>``)."""
for alias in node.names:
if alias.name == "os":
self.os_module_names.add(alias.asname or "os")
self.generic_visit(node)
def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
"""Check for 'from os import path' or 'from os.path import ...' statements."""
if node.module == "os" and any(
alias.name == "path" for alias in node.names
):
self.has_os_path_import = True
self.violations.append(
(
node.lineno,
"Found 'from os import path' - use 'from pathlib import Path' instead",
)
)
elif node.module == "os.path":
self.has_os_path_import = True
imported_names = [alias.name for alias in node.names]
self.violations.append(
(
node.lineno,
f"Found 'from os.path import {', '.join(imported_names)}' - use pathlib.Path methods instead",
)
)
self.generic_visit(node)
def visit_Attribute(self, node: ast.Attribute) -> None:
"""Check for os.path.* usage."""
if (
isinstance(node.value, ast.Name)
and node.value.id in self.os_module_names
and node.attr == "path"
):
# This is os.path usage
# Try to get the specific method being called
parent = getattr(node, "parent", None)
if parent and isinstance(parent, ast.Attribute):
method = parent.attr
# Skip os.path.expandvars as it has no pathlib equivalent
if method == "expandvars":
return
suggestion = get_pathlib_equivalent(f"os.path.{method}")
else:
suggestion = "Use pathlib.Path instead"
self.violations.append(
(node.lineno, f"Found os.path usage - {suggestion}")
)
self.generic_visit(node)
def visit_Call(self, node: ast.Call) -> None:
"""Check for direct calls to os.path functions."""
if isinstance(node.func, ast.Attribute):
# Store parent reference for better context
node.func.parent = node
# Check for os.path.* calls
if (
isinstance(node.func.value, ast.Attribute)
and isinstance(node.func.value.value, ast.Name)
and node.func.value.value.id in self.os_module_names
and node.func.value.attr == "path"
):
method = node.func.attr
# Skip os.path.expandvars as it has no pathlib equivalent
if method == "expandvars":
return
suggestion = get_pathlib_equivalent(f"os.path.{method}")
self.violations.append(
(node.lineno, f"Found os.path.{method}() - {suggestion}")
)
self.generic_visit(node)
def get_pathlib_equivalent(os_path_call: str) -> str:
"""Get the pathlib equivalent for common os.path operations."""
equivalents = {
"os.path.join": "Use Path() / 'subpath' or Path().joinpath()",
"os.path.exists": "Use Path().exists()",
"os.path.isfile": "Use Path().is_file()",
"os.path.isdir": "Use Path().is_dir()",
"os.path.dirname": "Use Path().parent",
"os.path.basename": "Use Path().name",
"os.path.abspath": "Use Path().resolve()",
"os.path.realpath": "Use Path().resolve()",
"os.path.expanduser": "Use Path().expanduser()",
"os.path.split": "Use Path().parent and Path().name",
"os.path.splitext": "Use Path().stem and Path().suffix",
"os.path.getsize": "Use Path().stat().st_size",
"os.path.getmtime": "Use Path().stat().st_mtime",
"os.path.normpath": "Use Path() - it normalizes automatically",
# Note: os.path.expandvars has no pathlib equivalent and is allowed
"os.path.expandvars": "(No pathlib equivalent - allowed)",
}
return equivalents.get(os_path_call, "Use pathlib.Path equivalent method")
def check_file(
filepath: Path, allow_legacy: bool = False
) -> list[tuple[str, int, str]]:
"""
Check a Python file for os.path usage.
Args:
filepath: Path to the Python file to check
allow_legacy: If True, only check modified lines (not implemented yet)
Returns:
List of (filename, line_number, violation_message) tuples
"""
try:
content = filepath.read_text(encoding="utf-8")
tree = ast.parse(content, filename=str(filepath))
except SyntaxError as e:
print(f"Syntax error in {filepath}: {e}", file=sys.stderr)
return []
except Exception as e:
print(f"Error reading {filepath}: {e}", file=sys.stderr)
return []
checker = OsPathChecker(str(filepath))
checker.visit(tree)
return [(str(filepath), line, msg) for line, msg in checker.violations]
def main() -> int:
"""Main entry point for the pre-commit hook."""
parser = argparse.ArgumentParser(
description="Check for os.path usage and suggest pathlib alternatives"
)
parser.add_argument(
"filenames",
nargs="*",
help="Python files to check",
)
parser.add_argument(
"--allow-legacy",
action="store_true",
help="Allow os.path in existing code (only check new/modified lines)",
)
args = parser.parse_args()
# List of files that are allowed to use os.path (legacy or special cases)
ALLOWED_FILES = {
"src/local_deep_research/utilities/log_utils.py", # May need os.path for low-level operations
"src/local_deep_research/config/paths.py", # Already migrated but may have legacy code
".pre-commit-hooks/check-pathlib-usage.py", # This file itself
}
violations = []
for filename in args.filenames:
filepath = Path(filename)
# Skip non-Python files
if not filename.endswith(".py"):
continue
# Skip allowed files
if any(filename.endswith(allowed) for allowed in ALLOWED_FILES):
continue
file_violations = check_file(filepath, args.allow_legacy)
violations.extend(file_violations)
if violations:
print("\n❌ Found os.path usage - please use pathlib.Path instead:\n")
for filename, line, message in violations:
print(f" {filename}:{line}: {message}")
print(
"\n💡 Tip: pathlib.Path provides a more modern and cross-platform API."
)
print(
" Example: Path('dir') / 'file.txt' instead of os.path.join('dir', 'file.txt')"
)
print(
"\n📚 See https://docs.python.org/3/library/pathlib.html for more information.\n"
)
return 1
return 0
if __name__ == "__main__":
sys.exit(main())