|
|
import os |
|
|
import sys |
|
|
import ast |
|
|
import tempfile |
|
|
import subprocess |
|
|
from datetime import datetime |
|
|
from io import StringIO |
|
|
import traceback |
|
|
import gradio as gr |
|
|
|
|
|
|
|
|
class CodeEvaluator: |
|
|
""" |
|
|
Evaluates Python code for correctness and quality. |
|
|
""" |
|
|
def __init__(self): |
|
|
self.code = "" |
|
|
self.correctness_passed = False |
|
|
self.execution_error = None |
|
|
self.quality_issues = [] |
|
|
self.quality_strengths = [] |
|
|
self.timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") |
|
|
|
|
|
def evaluate_correctness(self, code): |
|
|
self.code = code |
|
|
try: |
|
|
ast.parse(code) |
|
|
except SyntaxError as e: |
|
|
self.execution_error = f"Syntax Error: {str(e)}" |
|
|
return {'passed': False, 'error_type': 'SyntaxError', 'error': self.execution_error} |
|
|
try: |
|
|
old_stdout = sys.stdout |
|
|
old_stderr = sys.stderr |
|
|
sys.stdout = StringIO() |
|
|
sys.stderr = StringIO() |
|
|
exec(code, {}) |
|
|
sys.stdout = old_stdout |
|
|
sys.stderr = old_stderr |
|
|
self.correctness_passed = True |
|
|
return {'passed': True, 'error': None} |
|
|
except Exception as e: |
|
|
sys.stdout = old_stdout |
|
|
sys.stderr = old_stderr |
|
|
error_type = type(e).__name__ |
|
|
self.execution_error = f"{error_type}: {str(e)}" |
|
|
return {'passed': False, 'error_type': error_type, 'error': self.execution_error, 'traceback': traceback.format_exc()} |
|
|
|
|
|
def evaluate_quality(self): |
|
|
if not self.correctness_passed: |
|
|
return {'evaluated': False, 'message': 'Quality evaluation skipped - code must run first'} |
|
|
self.quality_issues = [] |
|
|
self.quality_strengths = [] |
|
|
pep8_issues = self._check_pep8() |
|
|
try: |
|
|
tree = ast.parse(self.code) |
|
|
self._check_naming_conventions(tree) |
|
|
self._check_documentation(tree) |
|
|
self._check_function_structure(tree) |
|
|
self._check_code_smells(tree) |
|
|
except Exception as e: |
|
|
self.quality_issues.append({'principle': 'Analysis Error', 'issue': f"Could not fully analyze code: {str(e)}"}) |
|
|
grade = self._calculate_grade() |
|
|
return {'evaluated': True, 'grade': grade, 'issues': self.quality_issues, 'strengths': self.quality_strengths, 'pep8_issues': pep8_issues} |
|
|
|
|
|
def _check_pep8(self): |
|
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f: |
|
|
f.write(self.code) |
|
|
temp_file = f.name |
|
|
try: |
|
|
result = subprocess.run(['pycodestyle', temp_file], capture_output=True, text=True) |
|
|
issues = [] |
|
|
if result.stdout: |
|
|
for line in result.stdout.strip().split('\n'): |
|
|
if line: |
|
|
issues.append(line) |
|
|
if not issues: |
|
|
self.quality_strengths.append("β Follows PEP 8 style guidelines") |
|
|
return issues |
|
|
finally: |
|
|
os.unlink(temp_file) |
|
|
|
|
|
def _check_naming_conventions(self, tree): |
|
|
issues = [] |
|
|
for node in ast.walk(tree): |
|
|
if isinstance(node, ast.FunctionDef): |
|
|
if not self._is_snake_case(node.name): |
|
|
issues.append(f"Function '{node.name}' should use snake_case naming") |
|
|
if isinstance(node, ast.ClassDef): |
|
|
if not self._is_pascal_case(node.name): |
|
|
issues.append(f"Class '{node.name}' should use PascalCase naming") |
|
|
if issues: |
|
|
self.quality_issues.append({'principle': 'Principle 3: Consistency', 'issue': 'Naming convention violations', 'details': issues}) |
|
|
else: |
|
|
self.quality_strengths.append("β Follows Python naming conventions") |
|
|
|
|
|
def _check_documentation(self, tree): |
|
|
functions_without_docs = [] |
|
|
total_functions = 0 |
|
|
for node in ast.walk(tree): |
|
|
if isinstance(node, ast.FunctionDef): |
|
|
total_functions += 1 |
|
|
if not ast.get_docstring(node): |
|
|
functions_without_docs.append(node.name) |
|
|
if functions_without_docs: |
|
|
self.quality_issues.append({'principle': 'Principle 5: Documentation', 'issue': f"{len(functions_without_docs)} function(s) missing docstrings", 'details': [f"Function '{name}' has no docstring" for name in functions_without_docs]}) |
|
|
elif total_functions > 0: |
|
|
self.quality_strengths.append("β All functions have docstrings") |
|
|
|
|
|
def _check_function_structure(self, tree): |
|
|
long_functions = [] |
|
|
for node in ast.walk(tree): |
|
|
if isinstance(node, ast.FunctionDef): |
|
|
if hasattr(node, 'end_lineno') and hasattr(node, 'lineno'): |
|
|
func_length = node.end_lineno - node.lineno |
|
|
if func_length > 50: |
|
|
long_functions.append((node.name, func_length)) |
|
|
if long_functions: |
|
|
self.quality_issues.append({'principle': 'Principle 6: Code Structure', 'issue': 'Functions are too long', 'details': [f"Function '{name}' is {length} lines (consider breaking into smaller functions)" for name, length in long_functions]}) |
|
|
|
|
|
def _check_code_smells(self, tree): |
|
|
bare_excepts = [] |
|
|
for node in ast.walk(tree): |
|
|
if isinstance(node, ast.ExceptHandler): |
|
|
if node.type is None: |
|
|
bare_excepts.append(node.lineno) |
|
|
if bare_excepts: |
|
|
self.quality_issues.append({'principle': 'Principle 7: Error Handling', 'issue': 'Bare except clauses found', 'details': [f"Line {line}: Use specific exception types instead of bare 'except:'" for line in bare_excepts]}) |
|
|
|
|
|
def _is_snake_case(self, name): |
|
|
if name.startswith('_'): |
|
|
name = name[1:] |
|
|
return name.islower() or '_' in name |
|
|
|
|
|
def _is_pascal_case(self, name): |
|
|
return name[0].isupper() and '_' not in name |
|
|
|
|
|
def _calculate_grade(self): |
|
|
issue_count = len(self.quality_issues) |
|
|
if issue_count == 0: |
|
|
return 'A' |
|
|
elif issue_count <= 2: |
|
|
return 'B' |
|
|
elif issue_count <= 4: |
|
|
return 'C' |
|
|
else: |
|
|
return 'D' |
|
|
|
|
|
|
|
|
class LearningPathGenerator: |
|
|
""" |
|
|
Generates personalized learning resources based on code evaluation. |
|
|
""" |
|
|
def __init__(self): |
|
|
self.error_resources = { |
|
|
'SyntaxError': {'explanation': 'Python could not understand your code structure.', 'common_causes': ['Missing or mismatched parentheses, brackets, or quotes'], 'resources': ['https://realpython.com/invalid-syntax-python/']}, |
|
|
'NameError': {'explanation': 'You tried to use a variable or function that does not exist.', 'common_causes': ['Typo in variable name'], 'resources': ['https://realpython.com/python-nameerror/']}, |
|
|
'TypeError': {'explanation': 'An operation was performed on an incompatible data type.', 'common_causes': ['Trying to add strings and numbers'], 'resources': ['https://realpython.com/python-traceback/#typeerror']}, |
|
|
'IndexError': {'explanation': 'You tried to access a list index that does not exist.', 'common_causes': ['List is shorter than expected'], 'resources': ['https://realpython.com/python-indexerror/']}, |
|
|
'KeyError': {'explanation': 'You tried to access a dictionary key that does not exist.', 'common_causes': ['Typo in key name'], 'resources': ['https://realpython.com/python-keyerror/']}, |
|
|
'ZeroDivisionError': {'explanation': 'You tried to divide by zero.', 'common_causes': ['Variable is zero when it should not be'], 'resources': ['https://realpython.com/python-traceback/#zerodivisionerror']} |
|
|
} |
|
|
self.principle_resources = { |
|
|
'Principle 1: Readability': {'tips': ['Use descriptive variable names'], 'resources': ['https://peps.python.org/pep-0008/#code-lay-out']}, |
|
|
'Principle 3: Consistency': {'tips': ['Functions and variables: snake_case'], 'resources': ['https://peps.python.org/pep-0008/#naming-conventions']}, |
|
|
'Principle 4: DRY': {'tips': ['Extract repeated code into functions'], 'resources': ['https://realpython.com/python-functions/']}, |
|
|
'Principle 5: Documentation': {'tips': ['Write docstrings for all functions'], 'resources': ['https://peps.python.org/pep-0257/']}, |
|
|
'Principle 6: Code Structure': {'tips': ['Keep functions short and focused'], 'resources': ['https://realpython.com/python-refactoring/']}, |
|
|
'Principle 7: Error Handling': {'tips': ['Use try/except for operations that might fail'], 'resources': ['https://realpython.com/python-exceptions/']} |
|
|
} |
|
|
|
|
|
def generate_learning_path(self, correctness_result, quality_result): |
|
|
path = [] |
|
|
path.append("\n" + "="*70) |
|
|
path.append("π YOUR PERSONALIZED LEARNING PATH") |
|
|
path.append("="*70 + "\n") |
|
|
|
|
|
if not correctness_result['passed']: |
|
|
path.append("π΄ PRIORITY: FIX CORRECTNESS ISSUES FIRST\n") |
|
|
error_type = correctness_result.get('error_type', 'Unknown') |
|
|
path.append(f"β Error Type: {error_type}") |
|
|
path.append(f" {correctness_result['error']}\n") |
|
|
if error_type in self.error_resources: |
|
|
resource = self.error_resources[error_type] |
|
|
path.append(f"π‘ What this means:\n {resource['explanation']}\n") |
|
|
path.append("π Learn more:\n β’ " + resource['resources'][0] + "\n") |
|
|
path.append("βοΈ Next Steps:\n 1. Read the error message carefully\n 2. Fix the issue\n 3. Run evaluation again") |
|
|
|
|
|
elif quality_result['evaluated'] and quality_result['issues']: |
|
|
path.append("β
GREAT NEWS: Your code runs successfully!\n") |
|
|
path.append(f"π Quality Grade: {quality_result['grade']}\n") |
|
|
if quality_result['strengths']: |
|
|
path.append("πͺ Strengths in your code:") |
|
|
for strength in quality_result['strengths']: |
|
|
path.append(f" {strength}") |
|
|
path.append("") |
|
|
path.append("π‘ AREAS FOR IMPROVEMENT:\n") |
|
|
for i, issue in enumerate(quality_result['issues'], 1): |
|
|
path.append(f"{i}. {issue['principle']}") |
|
|
path.append(f" Issue: {issue['issue']}") |
|
|
if 'details' in issue: |
|
|
path.append(" Details:") |
|
|
for detail in issue['details'][:3]: |
|
|
path.append(f" β’ {detail}") |
|
|
principle_key = issue['principle'] |
|
|
if principle_key in self.principle_resources: |
|
|
resource = self.principle_resources[principle_key] |
|
|
path.append(" π‘ Tips:") |
|
|
for tip in resource['tips'][:2]: |
|
|
path.append(f" β’ {tip}") |
|
|
path.append(f" π Learn more: {resource['resources'][0]}") |
|
|
path.append("") |
|
|
if quality_result.get('pep8_issues'): |
|
|
path.append("π PEP 8 Style Issues:") |
|
|
for issue in quality_result['pep8_issues'][:5]: |
|
|
path.append(f" β’ {issue}") |
|
|
if len(quality_result['pep8_issues']) > 5: |
|
|
remaining = len(quality_result['pep8_issues']) - 5 |
|
|
path.append(f" ... and {remaining} more") |
|
|
|
|
|
elif quality_result['evaluated']: |
|
|
path.append("π EXCELLENT WORK!\n") |
|
|
path.append(f"π Quality Grade: {quality_result['grade']}\n") |
|
|
path.append("Your code runs successfully and follows best practices!\n") |
|
|
if quality_result['strengths']: |
|
|
path.append("β¨ Strengths:") |
|
|
for strength in quality_result['strengths']: |
|
|
path.append(f" {strength}") |
|
|
path.append("") |
|
|
path.append("π Ready for More?\n") |
|
|
path.append(" Next steps to improve your Python skills:") |
|
|
path.append(" β’ Learn about list comprehensions") |
|
|
path.append(" β’ Explore context managers (with statements)") |
|
|
path.append(" β’ Study decorators and generators") |
|
|
path.append(" β’ Practice writing unit tests") |
|
|
|
|
|
path.append("="*70) |
|
|
return "\n".join(path) |
|
|
|
|
|
|
|
|
|
|
|
def evaluate_code_for_gradio(code_to_evaluate): |
|
|
""" |
|
|
Main evaluation function that returns formatted results for Gradio. |
|
|
""" |
|
|
|
|
|
status_message = "π Evaluating your code...\n\n" |
|
|
|
|
|
try: |
|
|
evaluator = CodeEvaluator() |
|
|
learning_gen = LearningPathGenerator() |
|
|
|
|
|
|
|
|
status_message += "βοΈ Phase 1: Testing if code runs...\n" |
|
|
correctness_result = evaluator.evaluate_correctness(code_to_evaluate) |
|
|
|
|
|
|
|
|
quality_result = {'evaluated': False} |
|
|
if correctness_result['passed']: |
|
|
status_message += "β
Code runs successfully!\n" |
|
|
status_message += "π Phase 2: Evaluating code quality...\n" |
|
|
quality_result = evaluator.evaluate_quality() |
|
|
status_message += f"β
Quality Grade: {quality_result['grade']}\n\n" |
|
|
else: |
|
|
status_message += f"β Code failed: {correctness_result.get('error_type', 'Error')}\n\n" |
|
|
|
|
|
|
|
|
learning_path = learning_gen.generate_learning_path(correctness_result, quality_result) |
|
|
|
|
|
|
|
|
full_output = status_message + learning_path |
|
|
|
|
|
return full_output |
|
|
|
|
|
except Exception as e: |
|
|
|
|
|
return f"β An error occurred during evaluation:\n\n{str(e)}\n\nPlease try again or report this issue." |
|
|
|
|
|
|
|
|
|
|
|
with gr.Blocks(title="π AICELS: Code Quality Learning App") as demo: |
|
|
gr.Markdown(""" |
|
|
# π AICELS: AI-Powered Code Evaluation and Learning System |
|
|
|
|
|
**Correctness First, Quality Second** |
|
|
|
|
|
This tool evaluates Python code in two phases: |
|
|
1. **Correctness**: Does your code run without errors? |
|
|
2. **Quality**: How well does it follow best practices? |
|
|
|
|
|
Paste your Python code below and get personalized feedback! |
|
|
""") |
|
|
|
|
|
with gr.Row(): |
|
|
with gr.Column(): |
|
|
code_input = gr.Code( |
|
|
label="Paste your Python code here:", |
|
|
language="python", |
|
|
lines=20 |
|
|
) |
|
|
submit_btn = gr.Button("π Evaluate Code", variant="primary") |
|
|
|
|
|
with gr.Column(): |
|
|
output = gr.Textbox( |
|
|
label="Evaluation Results & Learning Path", |
|
|
lines=30, |
|
|
max_lines=50 |
|
|
) |
|
|
|
|
|
gr.Markdown(""" |
|
|
### Example Code to Try: |
|
|
""") |
|
|
|
|
|
gr.Examples( |
|
|
examples=[ |
|
|
["def hello():\n print('Hello World')\n\nhello()"], |
|
|
["def calculate_sum(a, b):\n \"\"\"Add two numbers.\"\"\"\n return a + b\n\nresult = calculate_sum(5, 3)\nprint(f\"Result: {result}\")"], |
|
|
["def calc(p,r,t):\n return p*(1+r)**t\n\nresult = calc(1000,0.05,10)\nprint(result)"], |
|
|
], |
|
|
inputs=code_input, |
|
|
label="Click an example to load it" |
|
|
) |
|
|
|
|
|
submit_btn.click( |
|
|
fn=evaluate_code_for_gradio, |
|
|
inputs=code_input, |
|
|
outputs=output |
|
|
) |
|
|
|
|
|
if __name__ == '__main__': |
|
|
demo.launch() |