Skip to content

Instantly share code, notes, and snippets.

@oriapp
Created January 11, 2025 09:42
Show Gist options
  • Save oriapp/f956dc7633760ebed7a238b3be6e58ab to your computer and use it in GitHub Desktop.
Save oriapp/f956dc7633760ebed7a238b3be6e58ab to your computer and use it in GitHub Desktop.
import argparse
import ast
import json
import os
from termcolor import colored
from typing import List, Dict
# Define colors for different priorities
COLORS = {
'high': 'red',
'medium': 'yellow',
'low': 'cyan',
}
def parse_arguments():
"""Parse command-line arguments."""
parser = argparse.ArgumentParser(description="Check code quality and cleanliness.")
parser.add_argument("file", type=str, nargs="?", help="Path to the Python file to check.")
parser.add_argument(
"--ignore-complete-sentence",
action="store_true",
help="Ignore the rule that comments should be complete sentences.",
)
parser.add_argument(
"--generate-config",
action="store_true",
help="Generate a default configuration file.",
)
parser.add_argument(
"--config",
type=str,
default=None,
help="Path to the configuration file to use.",
)
return parser.parse_args()
def generate_default_config(file_path: str):
"""Generate a default configuration file."""
default_config = {
"check_inline_comments": True,
"check_docstrings": True,
"check_compile_time_errors": True,
"ignore_complete_sentence": False,
"severity": {
"inline_comment": "low",
"docstring": "medium",
"compile_error": "high"
},
"check_method_docstrings": True,
"check_class_docstrings": True,
"check_function_docstrings": True,
"check_init_docstrings": False,
}
# Ensure the directory exists for the config file
config_dir = os.path.dirname(file_path)
if config_dir and not os.path.exists(config_dir):
os.makedirs(config_dir, exist_ok=True)
# Write the default config to the specified file
with open(file_path, 'w') as f:
json.dump(default_config, f, indent=4)
print(colored(f"Default configuration file generated at {file_path}.", 'green'))
def check_inline_comments(content: str, config: Dict) -> List[str]:
"""Check if inline comments follow proper formatting."""
issues = []
if not config.get("check_inline_comments", True):
return issues
lines = content.splitlines()
for i, line in enumerate(lines):
stripped_line = line.strip()
if '#' in stripped_line:
comment_index = stripped_line.index('#')
comment_text = stripped_line[comment_index + 1:].strip()
if comment_text: # Only check non-empty comments
if not comment_text[0].isupper():
issues.append(
colored(f"{i+1}: Inline comment should start with a capital letter.",
COLORS[config['severity']['inline_comment']])
)
if not config["ignore_complete_sentence"] and not comment_text.endswith('.'):
issues.append(
colored(f"{i+1}: Inline comment should be a complete sentence.",
COLORS[config['severity']['inline_comment']])
)
return issues
def check_compile_time_errors(file_path: str) -> List[str]:
"""Check for compile-time errors."""
issues = []
try:
with open(file_path, "r") as file:
content = file.read()
ast.parse(content) # Try parsing the file for syntax errors
except SyntaxError as e:
issues.append(
colored(f"Compile-time error in {file_path}, line {e.lineno}: {e.msg}", COLORS['high'])
)
except Exception as e:
issues.append(
colored(f"Unexpected error during parsing: {e}", COLORS['high'])
)
return issues
def check_docstrings(content: str, config: Dict) -> List[str]:
"""Check if all methods, classes, and functions have proper docstrings."""
issues = []
if not config.get("check_docstrings", True):
return issues
tree = ast.parse(content)
for node in ast.walk(tree):
# Check functions and methods for docstrings
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
if not node.body or (not isinstance(node.body[0], ast.Expr) or not isinstance(node.body[0].value, ast.Str)):
if isinstance(node, ast.FunctionDef) and node.name == "__init__" and not config.get("check_init_docstrings", False):
continue
issues.append(colored(f"Missing docstring for {node.__class__.__name__} '{node.name}' at line {node.lineno}.", COLORS['medium']))
return issues
def load_config(config_path: str) -> Dict:
"""Load configuration from a JSON file."""
if os.path.exists(config_path):
with open(config_path, 'r') as f:
return json.load(f)
return {}
def main():
args = parse_arguments()
if args.generate_config:
# Generate default configuration file if flag is set
config_file_path = "config.json" # Default path for the config file
generate_default_config(config_file_path)
return
# Ensure that file argument is passed if not generating config
if not args.file:
print(colored("Error: You must specify a Python file to check.", 'red'))
return
config = {}
if args.config:
config = load_config(args.config)
else:
print(colored("No configuration file specified. Using default settings.", 'yellow'))
# Read the file content
try:
with open(args.file, "r") as file:
content = file.read()
except FileNotFoundError:
print(colored(f"File not found: {args.file}", COLORS['high']))
return
# Perform checks
issues = []
issues.extend(check_compile_time_errors(args.file))
issues.extend(check_inline_comments(content, config))
issues.extend(check_docstrings(content, config))
# Output results
if issues:
print("\n".join(issues))
else:
print(colored("No issues found.", 'green'))
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment