Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 31 additions & 23 deletions scripts/actions.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,29 +18,40 @@ case "$1" in
;;
lint)
(
cd "$SCRIPT_DIR" || exit 1
uv run --no-project --with "ruff==0.12.2" ruff format --target-version py312 \
src/ scripts/ packages/ \
&& \
uv run --no-project --with "ruff==0.12.2" \
ruff check --target-version py312 \
--fix \
src/ scripts/ packages/
echo "LINTING WORKFLOW STARTING..."

echo "lint-check..."
./scripts/actions.sh lint-check
echo "lint-fix..."
./scripts/actions.sh lint-fix
echo "toml-checking..."
./scripts/actions.sh toml-check
echo "type-checking..."
./scripts/actions.sh type-check

echo "LINTING WORKFLOW COMPLETED."
)
;;
lint-check)
(
cd "$SCRIPT_DIR" || exit 1
uv run --no-project --with "ruff==0.12.2" ruff format --target-version py312 \
-n \
src/ scripts/ packages/ \
uv run --no-project --with "ruff==0.12.2" \
ruff format --target-version py312 -n src/ scripts/ packages/ \
&& \
uv run --no-project --with "ruff==0.12.2" \
ruff check --target-version py312 \
src/ scripts/ packages/ \
ruff check --target-version py312 src/ scripts/ packages/ \
&& \
uv run --no-project --with "pylint==4.0.3" \
pylint src/ packages/
pylint src/ packages/ \
&& \
uv run --no-project python scripts/check_badfunctions.py
)
;;
lint-fix)
(
cd "$SCRIPT_DIR" || exit 1
uv run --no-project --with "ruff==0.12.2" ruff check --target-version py312 \
--fix src/ scripts/ packages/
)
;;
type-check)
Expand All @@ -50,7 +61,6 @@ case "$1" in

# weathergen-common
uv sync --project packages/common --no-install-workspace
uv pip list
uv run --project packages/common --frozen pyrefly check packages/common
# Fail for errors on weathergen-common:
if [ $? -ne 0 ]; then
Expand All @@ -60,7 +70,6 @@ case "$1" in

# weathergen-metrics
uv sync --project packages/metrics --no-install-workspace
uv pip list
uv run --project packages/metrics --frozen pyrefly check packages/metrics
# Fail for errors on weathergen-metrics:
if [ $? -ne 0 ]; then
Expand All @@ -70,21 +79,18 @@ case "$1" in

# weathergen-evaluate
uv sync --project packages/evaluate --no-install-workspace --package weathergen-evaluate
uv pip list
uv run --project packages/evaluate --frozen pyrefly check packages/evaluate

# weathergen (root)
# Install the whole workspace. It also needs the extra cpu option for the right version of pytorch.
uv sync --all-packages --extra cpu --no-install-workspace
uv pip list
uv run --all-packages pyrefly check src
echo "Type checking completed."
)
;;
unit-test)
(
cd "$SCRIPT_DIR" || exit 1
uv sync --extra cpu
uv run --extra cpu pytest src/
)
;;
Expand Down Expand Up @@ -176,9 +182,11 @@ case "$1" in
)
;;
*)
# Automatically extract all options from the case statement
options=$(grep -oP '^\s*\K[\w-]+(?=\))' "$0" | tr '\n' '|' | sed 's/|$//')
echo "Usage: $0 {$options}"
exit 1
(
# Automatically extract all options from the case statement
options=$(grep -oP '^\s*\K[\w-]+(?=\))' "$0" | tr '\n' '|' | sed 's/|$//')
echo "Usage: $0 {$options}"
exit 1
)
;;
esac
150 changes: 150 additions & 0 deletions scripts/check_badfunctions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
# ruff: noqa: T201, N802

"""
Check for usage of banned functions in Python source files.

This script uses the Abstract Syntax Tree (AST) module to parse Python files
and detect calls to functions that are explicitly banned in the codebase.
"""

import ast
import sys
from pathlib import Path

# List of function names that are not allowed in the codebase
BANNED_FUNCTIONS: set[str] = {
"getattr",
# "setattr",
# "delattr",
# "eval",
# "exec",
# Add more banned functions here as needed
}


class BannedFunctionVisitor(ast.NodeVisitor):
"""
AST visitor that detects calls to banned functions.

This visitor walks through the Abstract Syntax Tree of a Python file
and records any calls to functions that are in the BANNED_FUNCTIONS set.
"""

def __init__(self, filepath: Path):
"""
Initialize the visitor.

Args:
filepath: Path to the file being checked (for error reporting)
"""
self.filepath = filepath
self.errors: list[str] = []

def visit_Call(self, node: ast.Call) -> None:
"""
Visit a function call node in the AST.

This method is called automatically by the AST walker whenever
it encounters a function call. We check if the called function
is in our banned list.

Args:
node: The AST Call node representing a function call
"""
# Check if this is a simple function call (e.g., getattr(...))
# as opposed to a method call (e.g., obj.method(...))
if isinstance(node.func, ast.Name):
function_name = node.func.id

# If the function is banned, record an error
if function_name in BANNED_FUNCTIONS:
error_msg = (
f"{self.filepath}:{node.lineno}:{node.col_offset}: "
f"Use of banned function '{function_name}()' is not allowed"
)
self.errors.append(error_msg)

# Continue visiting child nodes
self.generic_visit(node)


def check_file(filepath: Path) -> list[str]:
"""
Check a single Python file for banned function usage.

Args:
filepath: Path to the Python file to check

Returns:
List of error messages (empty if no banned functions found)
"""
try:
# Read and parse the Python file into an AST
with open(filepath, encoding="utf-8") as f:
source_code = f.read()

tree = ast.parse(source_code, filename=str(filepath))

except SyntaxError as e:
# If the file has syntax errors, report them but don't fail
# (syntax errors will be caught by other linters)
return [f"{filepath}: Syntax error, skipping banned function check: {e}"]

except Exception as e:
# Handle other unexpected errors (e.g., encoding issues)
return [f"{filepath}: Error reading file: {e}"]

# Create a visitor and walk through the AST
visitor = BannedFunctionVisitor(filepath)
visitor.visit(tree)

return visitor.errors


def main() -> int:
"""
Main entry point for the banned functions checker.

Scans all Python files in the configured directories and reports
any usage of banned functions.

Returns:
Exit code: 0 if no banned functions found, 1 otherwise
"""
# Directories to check (relative to the script's parent directory)
script_dir = Path(__file__).parent.parent
directories_to_check = ["src/", "scripts/", "packages/"]

all_errors: list[str] = []

# Walk through each directory and check all Python files
for directory in directories_to_check:
dir_path = script_dir / directory

if not dir_path.exists():
print(f"Warning: Directory {directory} does not exist, skipping")
continue

# Find all .py files recursively
for py_file in dir_path.rglob("*.py"):
errors = check_file(py_file)
all_errors.extend(errors)

# Print all errors found
if all_errors:
print("=" * 70)
print("BANNED FUNCTION USAGE DETECTED")
print("=" * 70)
for error in all_errors:
print(error)
print("=" * 70)
print(f"\nTotal violations: {len(all_errors)}")
print(f"Banned functions: {', '.join(sorted(BANNED_FUNCTIONS))}")
return 1

print("✓ No banned functions detected")
return 0


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