Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,218 +1,239 @@
|
|
| 1 |
-
import streamlit as st
|
| 2 |
-
import os
|
| 3 |
-
import sys
|
| 4 |
-
import subprocess
|
| 5 |
-
import json
|
| 6 |
-
import pandas as pd
|
| 7 |
-
import plotly.express as px
|
| 8 |
-
import shutil
|
| 9 |
-
import tempfile
|
| 10 |
-
from pathlib import Path
|
| 11 |
-
|
| 12 |
-
# --- CONFIGURATION ---
|
| 13 |
-
st.set_page_config(page_title="Coach Code Python", layout="wide")
|
| 14 |
-
|
| 15 |
-
# Dictionnaire pédagogique mis à jour avec les lettres
|
| 16 |
-
RUFF_CAT_MAP = {
|
| 17 |
-
"F": "Erreurs Logiques (F)",
|
| 18 |
-
"E": "Style PEP8 (E)",
|
| 19 |
-
"W": "Avertissements (W)",
|
| 20 |
-
"I": "Tri des Imports (I)",
|
| 21 |
-
"B": "Bugs Potentiels (B)",
|
| 22 |
-
"UP": "Modernisation (UP)",
|
| 23 |
-
"N": "Nommage (N)",
|
| 24 |
-
"D": "Documentation (D)",
|
| 25 |
-
"ANN": "Annotations de type (ANN)",
|
| 26 |
-
"T": "Tests & Debug (T)",
|
| 27 |
-
"A": "Built-ins (A)"
|
| 28 |
-
}
|
| 29 |
-
|
| 30 |
-
def remove_excessive_blank_lines(code):
|
| 31 |
-
"""Supprime les lignes vides excessives (plus de 1 ligne vide consécutive)"""
|
| 32 |
-
lines = code.split('\n')
|
| 33 |
-
result = []
|
| 34 |
-
blank_count = 0
|
| 35 |
-
|
| 36 |
-
for line in lines:
|
| 37 |
-
if line.strip() == '':
|
| 38 |
-
blank_count += 1
|
| 39 |
-
if blank_count <= 1: # Garde max 1 ligne vide
|
| 40 |
-
result.append(line)
|
| 41 |
-
else:
|
| 42 |
-
blank_count = 0
|
| 43 |
-
result.append(line)
|
| 44 |
-
|
| 45 |
-
return '\n'.join(result)
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
def run_ruff(paths, fix=False, disable_docs=False, compact=False, unsafe=False):
|
| 49 |
-
"""Exécute l'analyse et le formatage Ruff"""
|
| 50 |
-
ignore_list = []
|
| 51 |
-
|
| 52 |
-
if compact:
|
| 53 |
-
ignore_list.extend(["E302", "E303", "E305", "E301", "E401", "W391"])
|
| 54 |
-
|
| 55 |
-
if disable_docs:
|
| 56 |
-
ignore_list.extend(["D100", "D101", "D102", "D103", "D104", "D107"])
|
| 57 |
-
|
| 58 |
-
cmd_check = [
|
| 59 |
-
sys.executable, "-m", "ruff", "check", *paths,
|
| 60 |
-
"--output-format", "json",
|
| 61 |
-
"--select", "ALL",
|
| 62 |
-
"--isolated", "--no-cache",
|
| 63 |
-
]
|
| 64 |
-
|
| 65 |
-
if ignore_list:
|
| 66 |
-
cmd_check.extend(["--ignore", ",".join(ignore_list)])
|
| 67 |
-
|
| 68 |
-
if fix:
|
| 69 |
-
cmd_check.append("--fix")
|
| 70 |
-
if unsafe:
|
| 71 |
-
cmd_check.append("--unsafe-fixes")
|
| 72 |
-
|
| 73 |
-
use_shell = os.name == 'nt'
|
| 74 |
-
result = subprocess.run(cmd_check, capture_output=True, text=True, encoding="utf-8", shell=use_shell)
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
st.
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 218 |
shutil.rmtree(".ruff_cache")
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import os
|
| 3 |
+
import sys
|
| 4 |
+
import subprocess
|
| 5 |
+
import json
|
| 6 |
+
import pandas as pd
|
| 7 |
+
import plotly.express as px
|
| 8 |
+
import shutil
|
| 9 |
+
import tempfile
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
|
| 12 |
+
# --- CONFIGURATION ---
|
| 13 |
+
st.set_page_config(page_title="Coach Code Python", layout="wide")
|
| 14 |
+
|
| 15 |
+
# Dictionnaire pédagogique mis à jour avec les lettres
|
| 16 |
+
RUFF_CAT_MAP = {
|
| 17 |
+
"F": "Erreurs Logiques (F)",
|
| 18 |
+
"E": "Style PEP8 (E)",
|
| 19 |
+
"W": "Avertissements (W)",
|
| 20 |
+
"I": "Tri des Imports (I)",
|
| 21 |
+
"B": "Bugs Potentiels (B)",
|
| 22 |
+
"UP": "Modernisation (UP)",
|
| 23 |
+
"N": "Nommage (N)",
|
| 24 |
+
"D": "Documentation (D)",
|
| 25 |
+
"ANN": "Annotations de type (ANN)",
|
| 26 |
+
"T": "Tests & Debug (T)",
|
| 27 |
+
"A": "Built-ins (A)"
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
def remove_excessive_blank_lines(code):
|
| 31 |
+
"""Supprime les lignes vides excessives (plus de 1 ligne vide consécutive)"""
|
| 32 |
+
lines = code.split('\n')
|
| 33 |
+
result = []
|
| 34 |
+
blank_count = 0
|
| 35 |
+
|
| 36 |
+
for line in lines:
|
| 37 |
+
if line.strip() == '':
|
| 38 |
+
blank_count += 1
|
| 39 |
+
if blank_count <= 1: # Garde max 1 ligne vide
|
| 40 |
+
result.append(line)
|
| 41 |
+
else:
|
| 42 |
+
blank_count = 0
|
| 43 |
+
result.append(line)
|
| 44 |
+
|
| 45 |
+
return '\n'.join(result)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def run_ruff(paths, fix=False, disable_docs=False, compact=False, unsafe=False):
|
| 49 |
+
"""Exécute l'analyse et le formatage Ruff"""
|
| 50 |
+
ignore_list = []
|
| 51 |
+
|
| 52 |
+
if compact:
|
| 53 |
+
ignore_list.extend(["E302", "E303", "E305", "E301", "E401", "W391"])
|
| 54 |
+
|
| 55 |
+
if disable_docs:
|
| 56 |
+
ignore_list.extend(["D100", "D101", "D102", "D103", "D104", "D107"])
|
| 57 |
+
|
| 58 |
+
cmd_check = [
|
| 59 |
+
sys.executable, "-m", "ruff", "check", *paths,
|
| 60 |
+
"--output-format", "json",
|
| 61 |
+
"--select", "ALL",
|
| 62 |
+
"--isolated", "--no-cache",
|
| 63 |
+
]
|
| 64 |
+
|
| 65 |
+
if ignore_list:
|
| 66 |
+
cmd_check.extend(["--ignore", ",".join(ignore_list)])
|
| 67 |
+
|
| 68 |
+
if fix:
|
| 69 |
+
cmd_check.append("--fix")
|
| 70 |
+
if unsafe:
|
| 71 |
+
cmd_check.append("--unsafe-fixes")
|
| 72 |
+
|
| 73 |
+
use_shell = os.name == 'nt'
|
| 74 |
+
result = subprocess.run(cmd_check, capture_output=True, text=True, encoding="utf-8", shell=use_shell)
|
| 75 |
+
|
| 76 |
+
# Debug: afficher stderr si présent
|
| 77 |
+
if result.stderr:
|
| 78 |
+
st.warning(f"Ruff stderr: {result.stderr}")
|
| 79 |
+
|
| 80 |
+
# Debug: afficher le code de retour
|
| 81 |
+
if result.returncode not in [0, 1]: # Ruff retourne 1 quand il y a des erreurs
|
| 82 |
+
st.error(f"Ruff return code: {result.returncode}")
|
| 83 |
+
|
| 84 |
+
if fix:
|
| 85 |
+
cmd_format = [sys.executable, "-m", "ruff", "format", *paths, "--isolated"]
|
| 86 |
+
subprocess.run(cmd_format, capture_output=True, shell=use_shell)
|
| 87 |
+
|
| 88 |
+
try:
|
| 89 |
+
return json.loads(result.stdout) if result.stdout.strip() else []
|
| 90 |
+
except Exception as e:
|
| 91 |
+
st.error(f"Erreur parsing JSON: {e}, stdout: {result.stdout[:200]}")
|
| 92 |
+
return []
|
| 93 |
+
|
| 94 |
+
def get_stats(paths):
|
| 95 |
+
total_size, total_lines = 0, 0
|
| 96 |
+
for p in paths:
|
| 97 |
+
path_obj = Path(p)
|
| 98 |
+
files = [path_obj] if path_obj.is_file() else path_obj.rglob("*.py")
|
| 99 |
+
for f in files:
|
| 100 |
+
try:
|
| 101 |
+
content = f.read_text(errors='ignore')
|
| 102 |
+
total_size += f.stat().st_size
|
| 103 |
+
total_lines += len(content.splitlines())
|
| 104 |
+
except: continue
|
| 105 |
+
return total_size, total_lines
|
| 106 |
+
|
| 107 |
+
# --- SIDEBAR ---
|
| 108 |
+
with st.sidebar:
|
| 109 |
+
st.title("🛡️ Configuration")
|
| 110 |
+
mode = st.radio("Source :", ["Fichier unique", "Plusieurs fichiers"], index=0)
|
| 111 |
+
|
| 112 |
+
uploaded_files = []
|
| 113 |
+
if mode == "Fichier unique":
|
| 114 |
+
f = st.file_uploader("Fichier .py", type="py")
|
| 115 |
+
if f: uploaded_files = [f]
|
| 116 |
+
else:
|
| 117 |
+
f_list = st.file_uploader("Sélectionner fichiers", type="py", accept_multiple_files=True)
|
| 118 |
+
if f_list: uploaded_files = f_list
|
| 119 |
+
|
| 120 |
+
st.subheader("🛠️ Options de correction")
|
| 121 |
+
opt_docs = st.checkbox("Désactiver l'obligation des commentaires", value=True)
|
| 122 |
+
opt_compact = st.checkbox("Garder le code compact (sans espaces excessifs)", value=True)
|
| 123 |
+
opt_unsafe = st.checkbox("Activer les corrections forcées (modernisation)", value=True)
|
| 124 |
+
|
| 125 |
+
#st.divider()
|
| 126 |
+
btn_analyze = st.button("🚀 Analyser & Corriger", use_container_width=True)
|
| 127 |
+
|
| 128 |
+
# --- ZONE CENTRALE ---
|
| 129 |
+
st.title("🐍 Coach Code Python")
|
| 130 |
+
|
| 131 |
+
if btn_analyze and uploaded_files:
|
| 132 |
+
with tempfile.TemporaryDirectory() as temp_dir:
|
| 133 |
+
temp_workspace = Path(temp_dir)
|
| 134 |
+
work_paths, code_before, code_after = [], "", ""
|
| 135 |
+
|
| 136 |
+
try:
|
| 137 |
+
for uploaded_file in uploaded_files:
|
| 138 |
+
file_path = temp_workspace / uploaded_file.name
|
| 139 |
+
file_data = uploaded_file.getvalue().decode("utf-8", errors="ignore")
|
| 140 |
+
if mode == "Fichier unique": code_before = file_data
|
| 141 |
+
file_path.write_text(file_data, encoding="utf-8")
|
| 142 |
+
work_paths.append(str(file_path))
|
| 143 |
+
|
| 144 |
+
s_init, l_init = get_stats(work_paths)
|
| 145 |
+
|
| 146 |
+
# Debug: vérifier que les fichiers existent
|
| 147 |
+
st.info(f"Fichiers créés: {[str(p) for p in work_paths]}")
|
| 148 |
+
for wp in work_paths:
|
| 149 |
+
if Path(wp).exists():
|
| 150 |
+
content = Path(wp).read_text(encoding="utf-8")
|
| 151 |
+
st.info(f"Taille fichier {Path(wp).name}: {len(content)} caractères, {len(content.splitlines())} lignes")
|
| 152 |
+
|
| 153 |
+
# Analyse initiale
|
| 154 |
+
errors = run_ruff(work_paths, fix=False, disable_docs=opt_docs, compact=opt_compact, unsafe=opt_unsafe)
|
| 155 |
+
|
| 156 |
+
# Debug: afficher le nombre d'erreurs trouvées
|
| 157 |
+
st.info(f"Nombre d'erreurs détectées: {len(errors)}")
|
| 158 |
+
if errors:
|
| 159 |
+
st.info(f"Première erreur: {errors[0]}")
|
| 160 |
+
|
| 161 |
+
# Correction
|
| 162 |
+
run_ruff(work_paths, fix=True, disable_docs=opt_docs, compact=opt_compact, unsafe=opt_unsafe)
|
| 163 |
+
|
| 164 |
+
if opt_compact:
|
| 165 |
+
for path in work_paths:
|
| 166 |
+
file_path = Path(path)
|
| 167 |
+
content = file_path.read_text(encoding="utf-8")
|
| 168 |
+
cleaned = remove_excessive_blank_lines(content)
|
| 169 |
+
file_path.write_text(cleaned, encoding="utf-8")
|
| 170 |
+
|
| 171 |
+
s_after, l_after = get_stats(work_paths)
|
| 172 |
+
|
| 173 |
+
if mode == "Fichier unique":
|
| 174 |
+
code_after = Path(work_paths[0]).read_text(encoding="utf-8")
|
| 175 |
+
|
| 176 |
+
nb_err = len(errors)
|
| 177 |
+
score = max(0, min(100, 100 - (nb_err / (l_init if l_init > 0 else 1)) * 100))
|
| 178 |
+
|
| 179 |
+
m1, m2, m3, m4 = st.columns(4)
|
| 180 |
+
m1.metric("Qualité du Code", f"{score:.1f}/100")
|
| 181 |
+
m2.metric("Points corrigés", nb_err)
|
| 182 |
+
m3.metric("Lignes modifiées", l_init - l_after)
|
| 183 |
+
m4.metric("Gain de poids", f"{s_init - s_after} octets")
|
| 184 |
+
|
| 185 |
+
tabs = st.tabs(["📊 Statistiques", "📜 Rapport", "🔍 Comparatif"] if mode == "Fichier unique" else ["📊 Statistiques", "📜 Rapport"])
|
| 186 |
+
|
| 187 |
+
with tabs[0]:
|
| 188 |
+
if nb_err > 0:
|
| 189 |
+
df = pd.DataFrame(errors)
|
| 190 |
+
df['Cat_Code'] = df['code'].str[0]
|
| 191 |
+
df['Catégorie'] = df['Cat_Code'].map(lambda x: RUFF_CAT_MAP.get(x, f"Autre ({x})"))
|
| 192 |
+
|
| 193 |
+
c1, c2 = st.columns(2)
|
| 194 |
+
with c1:
|
| 195 |
+
counts = df['code'].value_counts().reset_index().sort_values('count', ascending=True)
|
| 196 |
+
fig = px.bar(counts, x='count', y='code', orientation='h',
|
| 197 |
+
title="Fréquence par code d'erreur",
|
| 198 |
+
color='count', color_continuous_scale='Blues')
|
| 199 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 200 |
+
with c2:
|
| 201 |
+
cat_counts = df['Catégorie'].value_counts().reset_index().sort_values('count', ascending=False)
|
| 202 |
+
fig2 = px.bar(cat_counts, x='count', y='Catégorie', orientation='h',
|
| 203 |
+
title="Problèmes par famille",
|
| 204 |
+
color='Catégorie', color_discrete_sequence=px.colors.qualitative.G10)
|
| 205 |
+
fig2.update_layout(showlegend=False, yaxis={'categoryorder':'total ascending'})
|
| 206 |
+
st.plotly_chart(fig2, use_container_width=True)
|
| 207 |
+
else:
|
| 208 |
+
st.success("✨ Félicitations ! Ruff n'a trouvé aucune erreur.")
|
| 209 |
+
|
| 210 |
+
with tabs[1]:
|
| 211 |
+
if nb_err > 0:
|
| 212 |
+
# --- TRANSFORMATION DES DONNÉES POUR LE TABLEAU ---
|
| 213 |
+
report_data = []
|
| 214 |
+
for err in errors:
|
| 215 |
+
filename = Path(err['filename']).name
|
| 216 |
+
line = err['location']['row']
|
| 217 |
+
col = err['location']['column']
|
| 218 |
+
|
| 219 |
+
report_data.append({
|
| 220 |
+
"Code": err['code'],
|
| 221 |
+
"Message": err['message'],
|
| 222 |
+
"Localisation": f"{filename} (L:{line}, C:{col})"
|
| 223 |
+
})
|
| 224 |
+
|
| 225 |
+
st.dataframe(pd.DataFrame(report_data), use_container_width=True)
|
| 226 |
+
|
| 227 |
+
if mode == "Fichier unique":
|
| 228 |
+
with tabs[2]:
|
| 229 |
+
col1, col2 = st.columns(2)
|
| 230 |
+
col1.subheader("Version Originale")
|
| 231 |
+
col1.code(code_before, language="python")
|
| 232 |
+
col2.subheader("Version Corrigée")
|
| 233 |
+
col2.code(code_after, language="python")
|
| 234 |
+
|
| 235 |
+
except Exception as e:
|
| 236 |
+
st.error(f"Erreur : {e}")
|
| 237 |
+
|
| 238 |
+
if Path(".ruff_cache").exists():
|
| 239 |
shutil.rmtree(".ruff_cache")
|