Spaces:
Running
Running
Upload 4 files
Browse filesIMport des fichiers
- Dockerfile +34 -20
- README.md +57 -19
- app.py +220 -0
- requirements.txt +4 -3
Dockerfile
CHANGED
|
@@ -1,20 +1,34 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
RUN apt-get update && apt-get install -y \
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# syntax = docker/dockerfile:1.4
|
| 2 |
+
FROM python:3.12-slim
|
| 3 |
+
|
| 4 |
+
# Installe les dépendances système nécessaires
|
| 5 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 6 |
+
gcc \
|
| 7 |
+
g++ \
|
| 8 |
+
libjpeg62-turbo-dev \
|
| 9 |
+
zlib1g-dev \
|
| 10 |
+
libpng-dev \
|
| 11 |
+
libfreetype6-dev \
|
| 12 |
+
libopenjp2-7-dev \
|
| 13 |
+
libtiff5-dev \
|
| 14 |
+
curl \
|
| 15 |
+
git \
|
| 16 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 17 |
+
|
| 18 |
+
WORKDIR /app
|
| 19 |
+
|
| 20 |
+
# Copie requirements en premier (meilleur cache)
|
| 21 |
+
COPY requirements.txt .
|
| 22 |
+
|
| 23 |
+
# Installe tout (sans cache pour réduire la taille finale)
|
| 24 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 25 |
+
|
| 26 |
+
# Copie le code
|
| 27 |
+
COPY app.py .
|
| 28 |
+
|
| 29 |
+
# Port + commande obligatoire pour HF Spaces
|
| 30 |
+
EXPOSE 8501
|
| 31 |
+
|
| 32 |
+
HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health || exit 1
|
| 33 |
+
|
| 34 |
+
CMD ["streamlit", "run", "app.py", "--server.port=8501", "--server.address=0.0.0.0", "--server.enableCORS=false", "--server.enableXsrfProtection=false"]
|
README.md
CHANGED
|
@@ -1,19 +1,57 @@
|
|
| 1 |
-
---
|
| 2 |
-
title: Coach Code Python
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
-
sdk: docker
|
| 7 |
-
app_port: 8501
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Coach Code Python
|
| 3 |
+
emoji: 🐍
|
| 4 |
+
colorFrom: green
|
| 5 |
+
colorTo: green
|
| 6 |
+
sdk: docker
|
| 7 |
+
app_port: 8501
|
| 8 |
+
pinned: false
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
# 🐍 Coach Code Python
|
| 12 |
+
|
| 13 |
+
Application Streamlit pour analyser et corriger automatiquement votre code Python avec Ruff.
|
| 14 |
+
|
| 15 |
+
## Fonctionnalités
|
| 16 |
+
|
| 17 |
+
- **Analyse de code** : Détection automatique de tous les problèmes de qualité
|
| 18 |
+
- **Correction automatique** : Applique les corrections recommandées par Ruff
|
| 19 |
+
- **Support multi-fichiers** : Analysez un fichier unique ou plusieurs fichiers simultanément
|
| 20 |
+
- **Visualisations interactives** : Graphiques des erreurs par catégorie
|
| 21 |
+
- **Comparaison avant/après** : Visualisez les modifications apportées au code
|
| 22 |
+
- **Options configurables** :
|
| 23 |
+
- Désactiver l'obligation des commentaires (docstrings)
|
| 24 |
+
- Garder le code compact (sans espaces excessifs)
|
| 25 |
+
- Activer les corrections forcées (modernisation du code)
|
| 26 |
+
|
| 27 |
+
## Catégories d'analyse
|
| 28 |
+
|
| 29 |
+
- **F** : Erreurs Logiques
|
| 30 |
+
- **E** : Style PEP8
|
| 31 |
+
- **W** : Avertissements
|
| 32 |
+
- **I** : Tri des Imports
|
| 33 |
+
- **B** : Bugs Potentiels
|
| 34 |
+
- **UP** : Modernisation
|
| 35 |
+
- **N** : Nommage
|
| 36 |
+
- **D** : Documentation
|
| 37 |
+
- **ANN** : Annotations de type
|
| 38 |
+
- **T** : Tests & Debug
|
| 39 |
+
- **A** : Built-ins
|
| 40 |
+
|
| 41 |
+
## Utilisation
|
| 42 |
+
|
| 43 |
+
1. Sélectionnez votre mode (fichier unique ou plusieurs fichiers)
|
| 44 |
+
2. Uploadez votre/vos fichier(s) Python (.py)
|
| 45 |
+
3. Configurez les options de correction selon vos besoins
|
| 46 |
+
4. Cliquez sur "🚀 Analyser & Corriger"
|
| 47 |
+
5. Consultez les résultats dans les différents onglets :
|
| 48 |
+
- **📊 Statistiques** : Graphiques des erreurs détectées
|
| 49 |
+
- **📜 Rapport** : Liste détaillée de tous les problèmes
|
| 50 |
+
- **🔍 Comparatif** : Code avant/après (mode fichier unique)
|
| 51 |
+
|
| 52 |
+
## Métriques affichées
|
| 53 |
+
|
| 54 |
+
- **Qualité du Code** : Score sur 100 basé sur le nombre d'erreurs
|
| 55 |
+
- **Points corrigés** : Nombre total de problèmes détectés
|
| 56 |
+
- **Lignes modifiées** : Différence de lignes de code
|
| 57 |
+
- **Gain de poids** : Réduction de la taille du fichier en octets
|
app.py
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
if fix:
|
| 77 |
+
cmd_format = [sys.executable, "-m", "ruff", "format", *paths, "--isolated"]
|
| 78 |
+
subprocess.run(cmd_format, capture_output=True, shell=use_shell)
|
| 79 |
+
|
| 80 |
+
try:
|
| 81 |
+
return json.loads(result.stdout) if result.stdout.strip() else []
|
| 82 |
+
except:
|
| 83 |
+
return []
|
| 84 |
+
|
| 85 |
+
def get_stats(paths):
|
| 86 |
+
total_size, total_lines = 0, 0
|
| 87 |
+
for p in paths:
|
| 88 |
+
path_obj = Path(p)
|
| 89 |
+
files = [path_obj] if path_obj.is_file() else path_obj.rglob("*.py")
|
| 90 |
+
for f in files:
|
| 91 |
+
try:
|
| 92 |
+
content = f.read_text(errors='ignore')
|
| 93 |
+
total_size += f.stat().st_size
|
| 94 |
+
total_lines += len(content.splitlines())
|
| 95 |
+
except: continue
|
| 96 |
+
return total_size, total_lines
|
| 97 |
+
|
| 98 |
+
# --- SIDEBAR ---
|
| 99 |
+
with st.sidebar:
|
| 100 |
+
st.title("🛡️ Configuration")
|
| 101 |
+
mode = st.radio("Source :", ["Fichier unique", "Plusieurs fichiers"], index=0)
|
| 102 |
+
|
| 103 |
+
uploaded_files = []
|
| 104 |
+
if mode == "Fichier unique":
|
| 105 |
+
f = st.file_uploader("Fichier .py", type="py")
|
| 106 |
+
if f: uploaded_files = [f]
|
| 107 |
+
else:
|
| 108 |
+
f_list = st.file_uploader("Sélectionner fichiers", type="py", accept_multiple_files=True)
|
| 109 |
+
if f_list: uploaded_files = f_list
|
| 110 |
+
|
| 111 |
+
st.subheader("🛠️ Options de correction")
|
| 112 |
+
opt_docs = st.checkbox("Désactiver l'obligation des commentaires", value=True)
|
| 113 |
+
opt_compact = st.checkbox("Garder le code compact (sans espaces excessifs)", value=True)
|
| 114 |
+
opt_unsafe = st.checkbox("Activer les corrections forcées (modernisation)", value=True)
|
| 115 |
+
|
| 116 |
+
#st.divider()
|
| 117 |
+
btn_analyze = st.button("🚀 Analyser & Corriger", use_container_width=True)
|
| 118 |
+
|
| 119 |
+
# --- ZONE CENTRALE ---
|
| 120 |
+
st.title("🐍 Coach Code Python")
|
| 121 |
+
|
| 122 |
+
if btn_analyze and uploaded_files:
|
| 123 |
+
with tempfile.TemporaryDirectory() as temp_dir:
|
| 124 |
+
temp_workspace = Path(temp_dir)
|
| 125 |
+
work_paths, code_before, code_after = [], "", ""
|
| 126 |
+
|
| 127 |
+
try:
|
| 128 |
+
for uploaded_file in uploaded_files:
|
| 129 |
+
file_path = temp_workspace / uploaded_file.name
|
| 130 |
+
file_data = uploaded_file.getvalue().decode("utf-8", errors="ignore")
|
| 131 |
+
if mode == "Fichier unique": code_before = file_data
|
| 132 |
+
file_path.write_text(file_data, encoding="utf-8")
|
| 133 |
+
work_paths.append(str(file_path))
|
| 134 |
+
|
| 135 |
+
s_init, l_init = get_stats(work_paths)
|
| 136 |
+
|
| 137 |
+
# Analyse initiale
|
| 138 |
+
errors = run_ruff(work_paths, fix=False, disable_docs=opt_docs, compact=opt_compact, unsafe=opt_unsafe)
|
| 139 |
+
|
| 140 |
+
# Correction
|
| 141 |
+
run_ruff(work_paths, fix=True, disable_docs=opt_docs, compact=opt_compact, unsafe=opt_unsafe)
|
| 142 |
+
|
| 143 |
+
if opt_compact:
|
| 144 |
+
for path in work_paths:
|
| 145 |
+
file_path = Path(path)
|
| 146 |
+
content = file_path.read_text(encoding="utf-8")
|
| 147 |
+
cleaned = remove_excessive_blank_lines(content)
|
| 148 |
+
file_path.write_text(cleaned, encoding="utf-8")
|
| 149 |
+
|
| 150 |
+
s_after, l_after = get_stats(work_paths)
|
| 151 |
+
|
| 152 |
+
if mode == "Fichier unique":
|
| 153 |
+
code_after = Path(work_paths[0]).read_text(encoding="utf-8")
|
| 154 |
+
|
| 155 |
+
nb_err = len(errors)
|
| 156 |
+
score = max(0, min(100, 100 - (nb_err / (l_init if l_init > 0 else 1)) * 100))
|
| 157 |
+
|
| 158 |
+
m1, m2, m3, m4 = st.columns(4)
|
| 159 |
+
m1.metric("Qualité du Code", f"{score:.1f}/100")
|
| 160 |
+
m2.metric("Points corrigés", nb_err)
|
| 161 |
+
m3.metric("Lignes modifiées", l_init - l_after)
|
| 162 |
+
m4.metric("Gain de poids", f"{s_init - s_after} octets")
|
| 163 |
+
|
| 164 |
+
tabs = st.tabs(["📊 Statistiques", "📜 Rapport", "🔍 Comparatif"] if mode == "Fichier unique" else ["📊 Statistiques", "📜 Rapport"])
|
| 165 |
+
|
| 166 |
+
with tabs[0]:
|
| 167 |
+
if nb_err > 0:
|
| 168 |
+
df = pd.DataFrame(errors)
|
| 169 |
+
df['Cat_Code'] = df['code'].str[0]
|
| 170 |
+
df['Catégorie'] = df['Cat_Code'].map(lambda x: RUFF_CAT_MAP.get(x, f"Autre ({x})"))
|
| 171 |
+
|
| 172 |
+
c1, c2 = st.columns(2)
|
| 173 |
+
with c1:
|
| 174 |
+
counts = df['code'].value_counts().reset_index().sort_values('count', ascending=True)
|
| 175 |
+
fig = px.bar(counts, x='count', y='code', orientation='h',
|
| 176 |
+
title="Fréquence par code d'erreur",
|
| 177 |
+
color='count', color_continuous_scale='Blues')
|
| 178 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 179 |
+
with c2:
|
| 180 |
+
cat_counts = df['Catégorie'].value_counts().reset_index().sort_values('count', ascending=False)
|
| 181 |
+
fig2 = px.bar(cat_counts, x='count', y='Catégorie', orientation='h',
|
| 182 |
+
title="Problèmes par famille",
|
| 183 |
+
color='Catégorie', color_discrete_sequence=px.colors.qualitative.G10)
|
| 184 |
+
fig2.update_layout(showlegend=False, yaxis={'categoryorder':'total ascending'})
|
| 185 |
+
st.plotly_chart(fig2, use_container_width=True)
|
| 186 |
+
else:
|
| 187 |
+
st.success("✨ Félicitations ! Ruff n'a trouvé aucune erreur.")
|
| 188 |
+
|
| 189 |
+
with tabs[1]:
|
| 190 |
+
if nb_err > 0:
|
| 191 |
+
# --- TRANSFORMATION DES DONNÉES POUR LE TABLEAU ---
|
| 192 |
+
report_data = []
|
| 193 |
+
for err in errors:
|
| 194 |
+
filename = Path(err['filename']).name
|
| 195 |
+
line = err['location']['row']
|
| 196 |
+
col = err['location']['column']
|
| 197 |
+
|
| 198 |
+
report_data.append({
|
| 199 |
+
"Code": err['code'],
|
| 200 |
+
"Message": err['message'],
|
| 201 |
+
"Localisation": f"{filename} (L:{line}, C:{col})"
|
| 202 |
+
})
|
| 203 |
+
|
| 204 |
+
st.dataframe(pd.DataFrame(report_data), use_container_width=True)
|
| 205 |
+
|
| 206 |
+
if mode == "Fichier unique":
|
| 207 |
+
with tabs[2]:
|
| 208 |
+
col1, col2 = st.columns(2)
|
| 209 |
+
col1.subheader("Version Originale")
|
| 210 |
+
col1.code(code_before, language="python")
|
| 211 |
+
col2.subheader("Version Corrigée")
|
| 212 |
+
col2.code(code_after, language="python")
|
| 213 |
+
|
| 214 |
+
except Exception as e:
|
| 215 |
+
st.error(f"Erreur : {e}")
|
| 216 |
+
|
| 217 |
+
if Path(".ruff_cache").exists():
|
| 218 |
+
shutil.rmtree(".ruff_cache")
|
| 219 |
+
else:
|
| 220 |
+
st.info("👈 Veuillez sélectionner un ou plusieurs fichiers Python dans la barre latérale pour commencer l'analyse.")
|
requirements.txt
CHANGED
|
@@ -1,3 +1,4 @@
|
|
| 1 |
-
|
| 2 |
-
pandas
|
| 3 |
-
|
|
|
|
|
|
| 1 |
+
streamlit==1.40.2
|
| 2 |
+
pandas==2.2.3
|
| 3 |
+
plotly==5.24.1
|
| 4 |
+
ruff==0.8.4
|