vibetodev 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- vibetodev-1.0.0/PKG-INFO +66 -0
- vibetodev-1.0.0/README.md +41 -0
- vibetodev-1.0.0/pyproject.toml +36 -0
- vibetodev-1.0.0/setup.cfg +4 -0
- vibetodev-1.0.0/setup.py +38 -0
- vibetodev-1.0.0/vibetodev/__init__.py +5 -0
- vibetodev-1.0.0/vibetodev/__main__.py +6 -0
- vibetodev-1.0.0/vibetodev/categorizer.py +124 -0
- vibetodev-1.0.0/vibetodev/cli.py +132 -0
- vibetodev-1.0.0/vibetodev/generator.py +240 -0
- vibetodev-1.0.0/vibetodev/scanner.py +277 -0
- vibetodev-1.0.0/vibetodev/validator.py +122 -0
- vibetodev-1.0.0/vibetodev.egg-info/PKG-INFO +66 -0
- vibetodev-1.0.0/vibetodev.egg-info/SOURCES.txt +15 -0
- vibetodev-1.0.0/vibetodev.egg-info/dependency_links.txt +1 -0
- vibetodev-1.0.0/vibetodev.egg-info/entry_points.txt +2 -0
- vibetodev-1.0.0/vibetodev.egg-info/top_level.txt +1 -0
vibetodev-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vibetodev
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Transforme n'importe quel projet vibecode en parcours d'apprentissage
|
|
5
|
+
Author: ConnectPro
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/connectpro/vibetodev
|
|
8
|
+
Project-URL: Source, https://github.com/connectpro/vibetodev
|
|
9
|
+
Keywords: learning,education,code-analysis,obsidian,ast
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Intended Audience :: Education
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Education
|
|
21
|
+
Classifier: Topic :: Software Development :: Code Generators
|
|
22
|
+
Requires-Python: >=3.8
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
Dynamic: requires-python
|
|
25
|
+
|
|
26
|
+
# VibeToDev
|
|
27
|
+
|
|
28
|
+
Transforme n'importe quel projet Python vibecodé en parcours d'apprentissage structuré.
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install vibetodev
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Ou depuis les sources :
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
git clone https://github.com/connectpro/vibetodev
|
|
40
|
+
cd vibetodev
|
|
41
|
+
pip install .
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Utilisation
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# Scanner un projet et generer un vault Obsidian
|
|
48
|
+
vibetodev scan /chemin/vers/mon_projet --output /chemin/vers/mon_vault
|
|
49
|
+
|
|
50
|
+
# Valider un exercice
|
|
51
|
+
vibetodev check mon_exercice.py
|
|
52
|
+
|
|
53
|
+
# Initialiser vibetodev dans un projet
|
|
54
|
+
vibetodev init /chemin/vers/mon_projet
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Comment ca marche
|
|
58
|
+
|
|
59
|
+
1. **Scan** — Analyse AST de tous les fichiers Python, extrait 50+ concepts avec leur position exacte
|
|
60
|
+
2. **Categorisation** — 9 modules pedagogiques (variables, fonctions, classes, etc.)
|
|
61
|
+
3. **Generation** — Cree un vault Obsidian avec lecons, exemples de code, et exercices
|
|
62
|
+
4. **Validation** — Execute et verifie les exercices sans donner la solution
|
|
63
|
+
|
|
64
|
+
## Licence
|
|
65
|
+
|
|
66
|
+
MIT
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# VibeToDev
|
|
2
|
+
|
|
3
|
+
Transforme n'importe quel projet Python vibecodé en parcours d'apprentissage structuré.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install vibetodev
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Ou depuis les sources :
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
git clone https://github.com/connectpro/vibetodev
|
|
15
|
+
cd vibetodev
|
|
16
|
+
pip install .
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Utilisation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# Scanner un projet et generer un vault Obsidian
|
|
23
|
+
vibetodev scan /chemin/vers/mon_projet --output /chemin/vers/mon_vault
|
|
24
|
+
|
|
25
|
+
# Valider un exercice
|
|
26
|
+
vibetodev check mon_exercice.py
|
|
27
|
+
|
|
28
|
+
# Initialiser vibetodev dans un projet
|
|
29
|
+
vibetodev init /chemin/vers/mon_projet
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Comment ca marche
|
|
33
|
+
|
|
34
|
+
1. **Scan** — Analyse AST de tous les fichiers Python, extrait 50+ concepts avec leur position exacte
|
|
35
|
+
2. **Categorisation** — 9 modules pedagogiques (variables, fonctions, classes, etc.)
|
|
36
|
+
3. **Generation** — Cree un vault Obsidian avec lecons, exemples de code, et exercices
|
|
37
|
+
4. **Validation** — Execute et verifie les exercices sans donner la solution
|
|
38
|
+
|
|
39
|
+
## Licence
|
|
40
|
+
|
|
41
|
+
MIT
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.backends._legacy:_Backend"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "vibetodev"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Transforme n'importe quel projet vibecode en parcours d'apprentissage"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
authors = [
|
|
12
|
+
{name = "ConnectPro"}
|
|
13
|
+
]
|
|
14
|
+
keywords = ["learning", "education", "code-analysis", "obsidian", "ast"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Intended Audience :: Education",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.8",
|
|
22
|
+
"Programming Language :: Python :: 3.9",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Topic :: Education",
|
|
27
|
+
"Topic :: Software Development :: Code Generators",
|
|
28
|
+
]
|
|
29
|
+
requires-python = ">=3.8"
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Homepage = "https://github.com/connectpro/vibetodev"
|
|
33
|
+
Source = "https://github.com/connectpro/vibetodev"
|
|
34
|
+
|
|
35
|
+
[project.scripts]
|
|
36
|
+
vibetodev = "vibetodev.cli:main"
|
vibetodev-1.0.0/setup.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Transforme n'importe quel projet vibecode en parcours d'apprentissage."""
|
|
2
|
+
from setuptools import setup, find_packages
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
this_dir = Path(__file__).parent
|
|
6
|
+
long_description = (this_dir / "README.md").read_text(encoding="utf-8") if (this_dir / "README.md").exists() else ""
|
|
7
|
+
|
|
8
|
+
setup(
|
|
9
|
+
name="vibetodev",
|
|
10
|
+
version="1.0.0",
|
|
11
|
+
description="Transforme n'importe quel projet vibecode en parcours d'apprentissage",
|
|
12
|
+
long_description=long_description,
|
|
13
|
+
long_description_content_type="text/markdown",
|
|
14
|
+
author="ConnectPro",
|
|
15
|
+
license="MIT",
|
|
16
|
+
keywords="learning education code-analysis obsidian ast",
|
|
17
|
+
classifiers=[
|
|
18
|
+
"Development Status :: 4 - Beta",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"Intended Audience :: Education",
|
|
21
|
+
"License :: OSI Approved :: MIT License",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3.8",
|
|
24
|
+
"Programming Language :: Python :: 3.9",
|
|
25
|
+
"Programming Language :: Python :: 3.10",
|
|
26
|
+
"Programming Language :: Python :: 3.11",
|
|
27
|
+
"Programming Language :: Python :: 3.12",
|
|
28
|
+
"Topic :: Education",
|
|
29
|
+
"Topic :: Software Development :: Code Generators",
|
|
30
|
+
],
|
|
31
|
+
packages=find_packages(),
|
|
32
|
+
entry_points={
|
|
33
|
+
"console_scripts": [
|
|
34
|
+
"vibetodev=vibetodev.cli:main",
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
python_requires=">=3.8",
|
|
38
|
+
)
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Categorise les concepts extraits en modules pedagogiques.
|
|
3
|
+
Assigne un niveau (debutant/intermediaire/avance) et un ordre.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from collections import defaultdict
|
|
7
|
+
|
|
8
|
+
# Mapping concept -> module
|
|
9
|
+
CONCEPT_MODULE = {
|
|
10
|
+
# Module 1 : Variables
|
|
11
|
+
"type_str": 1, "type_int": 1, "type_bool": 1, "type_float": 1,
|
|
12
|
+
"type_none": 1, "type_bytes": 1, "chaine": 1,
|
|
13
|
+
"assignation": 1, "assignation_multiple": 1, "assignation_typee": 1,
|
|
14
|
+
"assignation_augmentee": 1,
|
|
15
|
+
|
|
16
|
+
# Module 2 : Fonctions
|
|
17
|
+
"fonction_def": 2, "fonction_async": 2, "return": 2, "yield": 2,
|
|
18
|
+
"type_hint_param": 2, "type_hint_return": 2, "docstring": 2,
|
|
19
|
+
"decorateur": 2, "lambda": 2,
|
|
20
|
+
|
|
21
|
+
# Module 3 : Controle
|
|
22
|
+
"condition_if": 3, "condition_elif": 3, "condition_else": 3,
|
|
23
|
+
"boucle_for": 3, "boucle_while": 3, "boucle_async_for": 3,
|
|
24
|
+
"break": 3, "continue": 3, "ternaire": 3,
|
|
25
|
+
|
|
26
|
+
# Module 4 : Gestion d'erreurs
|
|
27
|
+
"try_except": 4, "try_finally": 4, "raise": 4,
|
|
28
|
+
"context_manager_with": 4, "context_manager_async_with": 4,
|
|
29
|
+
|
|
30
|
+
# Module 5 : Collections
|
|
31
|
+
"type_list": 5, "type_dict": 5, "type_set": 5, "type_tuple": 5,
|
|
32
|
+
"comprehension_liste": 5, "comprehension_dict": 5, "comprehension_set": 5,
|
|
33
|
+
"assignation_index": 5, "slice": 5,
|
|
34
|
+
|
|
35
|
+
# Module 6 : Operations
|
|
36
|
+
"appel_fonction": 6, "appel_methode": 6,
|
|
37
|
+
"operateur_comparaison": 6, "operateur_logique": 6,
|
|
38
|
+
"fstring": 6, "binop_Add": 6, "binop_Sub": 6, "binop_Mult": 6,
|
|
39
|
+
"binop_Div": 6, "unpacking": 6, "walrus_operator": 6,
|
|
40
|
+
|
|
41
|
+
# Module 7 : Imports
|
|
42
|
+
"import_module": 7, "import_from": 7,
|
|
43
|
+
|
|
44
|
+
# Module 8 : Classes et Objets
|
|
45
|
+
"class_def": 8,
|
|
46
|
+
"assignation_attr": 8,
|
|
47
|
+
|
|
48
|
+
# Module 9 : Programmation avancee
|
|
49
|
+
# (tout ce qui n'a pas encore ete categorise)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
NOMS_MODULES = {
|
|
53
|
+
1: "01_Variables_et_Types",
|
|
54
|
+
2: "02_Fonctions",
|
|
55
|
+
3: "03_Structures_de_Controle",
|
|
56
|
+
4: "04_Gestion_d_Erreurs",
|
|
57
|
+
5: "05_Collections",
|
|
58
|
+
6: "06_Operations_et_Appels",
|
|
59
|
+
7: "07_Imports_et_Modules",
|
|
60
|
+
8: "08_Classes_et_Objets",
|
|
61
|
+
9: "09_Concepts_Avances",
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
TITRES_MODULES = {
|
|
65
|
+
1: "Variables et types de donnees",
|
|
66
|
+
2: "Les fonctions",
|
|
67
|
+
3: "Structures de controle",
|
|
68
|
+
4: "Gestion d'erreurs",
|
|
69
|
+
5: "Collections (listes, dicts, sets, tuples)",
|
|
70
|
+
6: "Operations et appels",
|
|
71
|
+
7: "Imports et modules",
|
|
72
|
+
8: "Classes et objets",
|
|
73
|
+
9: "Concepts avances (async, decorateurs, etc.)",
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
NIVEAU_CONCEPT = {
|
|
77
|
+
"type_str": "debutant", "type_int": "debutant", "type_bool": "debutant",
|
|
78
|
+
"type_float": "debutant", "type_none": "debutant", "chaine": "debutant",
|
|
79
|
+
"assignation": "debutant", "assignation_multiple": "debutant",
|
|
80
|
+
"assignation_augmentee": "debutant",
|
|
81
|
+
"fonction_def": "debutant", "return": "debutant",
|
|
82
|
+
"docstring": "debutant",
|
|
83
|
+
"condition_if": "debutant", "condition_else": "debutant",
|
|
84
|
+
"boucle_for": "debutant",
|
|
85
|
+
"type_list": "debutant", "type_dict": "debutant",
|
|
86
|
+
"fstring": "debutant",
|
|
87
|
+
"import_module": "debutant", "import_from": "debutant",
|
|
88
|
+
"appel_fonction": "debutant",
|
|
89
|
+
"appel_methode": "debutant",
|
|
90
|
+
"operateur_comparaison": "debutant",
|
|
91
|
+
"operateur_logique": "debutant",
|
|
92
|
+
"type_float": "debutant",
|
|
93
|
+
"break": "debutant", "continue": "debutant",
|
|
94
|
+
"condition_elif": "intermediaire",
|
|
95
|
+
"assignation_index": "intermediaire",
|
|
96
|
+
"type_set": "intermediaire", "type_tuple": "intermediaire",
|
|
97
|
+
"assignation_typee": "intermediaire",
|
|
98
|
+
"type_hint_param": "intermediaire",
|
|
99
|
+
"try_except": "intermediaire",
|
|
100
|
+
"context_manager_with": "intermediaire",
|
|
101
|
+
"comprehension_liste": "intermediaire",
|
|
102
|
+
"decorateur": "avance",
|
|
103
|
+
"lambda": "avance",
|
|
104
|
+
"yield": "avance",
|
|
105
|
+
"class_def": "avance",
|
|
106
|
+
"comprehension_dict": "avance",
|
|
107
|
+
"walrus_operator": "avance",
|
|
108
|
+
"fonction_async": "avance",
|
|
109
|
+
"boucle_async_for": "avance",
|
|
110
|
+
"context_manager_async_with": "avance",
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def categorize_concepts(concepts_bruts):
|
|
115
|
+
"""
|
|
116
|
+
Prend {concept: [occurences]} et retourne {module_id: {concept: occurences}}.
|
|
117
|
+
"""
|
|
118
|
+
modules = defaultdict(lambda: defaultdict(list))
|
|
119
|
+
|
|
120
|
+
for concept, occs in concepts_bruts.items():
|
|
121
|
+
module_id = CONCEPT_MODULE.get(concept, 9) # defaut: avance
|
|
122
|
+
modules[module_id][concept] = occs
|
|
123
|
+
|
|
124
|
+
return dict(sorted(modules.items()))
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI principale de vibetodev.
|
|
3
|
+
Usage:
|
|
4
|
+
vibetodev scan <chemin_projet> [--output <chemin_vault>]
|
|
5
|
+
vibetodev check <fichier_exercice>
|
|
6
|
+
vibetodev init <chemin_projet>
|
|
7
|
+
vibetodev serve
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import sys
|
|
12
|
+
import os
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from .scanner import scan_project
|
|
16
|
+
from .categorizer import categorize_concepts
|
|
17
|
+
from .generator import generate_vault
|
|
18
|
+
from .validator import validate_exercise
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def cmd_scan(args):
|
|
22
|
+
"""Analyse un projet et genere un vault Obsidian."""
|
|
23
|
+
source = Path(args.source).resolve()
|
|
24
|
+
if not source.exists():
|
|
25
|
+
print(f"Erreur: '{source}' n'existe pas.")
|
|
26
|
+
return 1
|
|
27
|
+
|
|
28
|
+
output = Path(args.output).resolve() if args.output else source / ".vibetodev-vault"
|
|
29
|
+
|
|
30
|
+
print(f"Scan de {source}...")
|
|
31
|
+
concepts = scan_project(source, args.recursive)
|
|
32
|
+
|
|
33
|
+
if not concepts:
|
|
34
|
+
print("Aucun concept trouve.")
|
|
35
|
+
return 1
|
|
36
|
+
|
|
37
|
+
total = sum(len(v) for v in concepts.values())
|
|
38
|
+
print(f"-> {len(concepts)} concepts distincts, {total} occurrences")
|
|
39
|
+
|
|
40
|
+
print("Categorisation...")
|
|
41
|
+
modules = categorize_concepts(concepts)
|
|
42
|
+
|
|
43
|
+
print("Generation du vault Obsidian...")
|
|
44
|
+
generate_vault(modules, concepts, output)
|
|
45
|
+
|
|
46
|
+
print(f"\nVault cree dans: {output}")
|
|
47
|
+
print(f"Ouvre ce dossier dans Obsidian pour voir les lecons.")
|
|
48
|
+
return 0
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def cmd_check(args):
|
|
52
|
+
"""Valide un exercice."""
|
|
53
|
+
fichier = Path(args.fichier).resolve()
|
|
54
|
+
if not fichier.exists():
|
|
55
|
+
print(f"Erreur: '{fichier}' introuvable.")
|
|
56
|
+
return 1
|
|
57
|
+
|
|
58
|
+
result = validate_exercise(fichier, args.concept)
|
|
59
|
+
if result["status"] == "success":
|
|
60
|
+
print("RESULTAT: VALIDE")
|
|
61
|
+
for r in result.get("reussites", []):
|
|
62
|
+
print(f" [OK] {r}")
|
|
63
|
+
print(result.get("message", ""))
|
|
64
|
+
return 0
|
|
65
|
+
elif result["status"] == "partial":
|
|
66
|
+
print("RESULTAT: PARTIEL")
|
|
67
|
+
for r in result.get("reussites", []):
|
|
68
|
+
print(f" [OK] {r}")
|
|
69
|
+
for e in result.get("erreurs", []):
|
|
70
|
+
print(f" [A CORRIGER] {e}")
|
|
71
|
+
print(f"Indice: {result.get('hint', '')}")
|
|
72
|
+
return 1
|
|
73
|
+
else:
|
|
74
|
+
print("RESULTAT: ERREUR")
|
|
75
|
+
print(result.get("message", ""))
|
|
76
|
+
print(f"Indice: {result.get('hint', '')}")
|
|
77
|
+
return 1
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def cmd_init(args):
|
|
81
|
+
"""Initialise un projet pour vibetodev."""
|
|
82
|
+
source = Path(args.source).resolve()
|
|
83
|
+
if not source.exists():
|
|
84
|
+
print(f"Erreur: '{source}' n'existe pas.")
|
|
85
|
+
return 1
|
|
86
|
+
|
|
87
|
+
vibetodev_dir = source / ".vibetodev"
|
|
88
|
+
vibetodev_dir.mkdir(exist_ok=True)
|
|
89
|
+
|
|
90
|
+
print(f"VibeToDev initialise dans {source}")
|
|
91
|
+
print(f"Fichier de config: {vibetodev_dir / 'config.json'}")
|
|
92
|
+
print(f"\nProchaine etape: vibetodev scan {source}")
|
|
93
|
+
return 0
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def main():
|
|
97
|
+
parser = argparse.ArgumentParser(
|
|
98
|
+
prog="vibetodev",
|
|
99
|
+
description="Transforme un projet vibecode en parcours d'apprentissage",
|
|
100
|
+
)
|
|
101
|
+
parser.add_argument("--version", action="version", version="vibetodev 1.0.0")
|
|
102
|
+
|
|
103
|
+
sub = parser.add_subparsers(dest="command", help="Commande")
|
|
104
|
+
|
|
105
|
+
# scan
|
|
106
|
+
p_scan = sub.add_parser("scan", help="Analyser un projet et generer un vault")
|
|
107
|
+
p_scan.add_argument("source", help="Dossier du projet a analyser")
|
|
108
|
+
p_scan.add_argument("-o", "--output", help="Dossier de sortie du vault (defaut: <source>/.vibetodev-vault)")
|
|
109
|
+
p_scan.add_argument("-r", "--recursive", action="store_true", default=True, help="Scan recursif (defaut: True)")
|
|
110
|
+
p_scan.set_defaults(func=cmd_scan)
|
|
111
|
+
|
|
112
|
+
# check
|
|
113
|
+
p_check = sub.add_parser("check", help="Valider un exercice")
|
|
114
|
+
p_check.add_argument("fichier", help="Fichier exercice a valider")
|
|
115
|
+
p_check.add_argument("--concept", help="Concept a valider (optionnel)")
|
|
116
|
+
p_check.set_defaults(func=cmd_check)
|
|
117
|
+
|
|
118
|
+
# init
|
|
119
|
+
p_init = sub.add_parser("init", help="Initialiser vibetodev dans un projet")
|
|
120
|
+
p_init.add_argument("source", help="Dossier du projet")
|
|
121
|
+
p_init.set_defaults(func=cmd_init)
|
|
122
|
+
|
|
123
|
+
args = parser.parse_args()
|
|
124
|
+
if not args.command:
|
|
125
|
+
parser.print_help()
|
|
126
|
+
return 1
|
|
127
|
+
|
|
128
|
+
return args.func(args)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
if __name__ == "__main__":
|
|
132
|
+
sys.exit(main())
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Genere un vault Obsidian complet avec lecons, exercices et liens vers le code.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from collections import defaultdict
|
|
8
|
+
|
|
9
|
+
from .categorizer import NOMS_MODULES, TITRES_MODULES, NIVEAU_CONCEPT
|
|
10
|
+
|
|
11
|
+
EXPLICATIONS = {
|
|
12
|
+
"type_str": "Une **chaine** (str) est du texte entre guillemets.",
|
|
13
|
+
"type_int": "Un **entier** (int) est un nombre sans virgule.",
|
|
14
|
+
"type_bool": "Un **booleen** (bool) vaut True ou False.",
|
|
15
|
+
"type_float": "Un **float** est un nombre decimal.",
|
|
16
|
+
"type_none": "**None** represente l'absence de valeur.",
|
|
17
|
+
"chaine": "Une **chaine vide** \"\" est une chaine sans caractere.",
|
|
18
|
+
"assignation": "L'**assignation** (=) stocke une valeur dans une variable.",
|
|
19
|
+
"assignation_multiple": "L'**assignation multiple** (=) assigne plusieurs variables en une ligne.",
|
|
20
|
+
"assignation_typee": "L'**assignation typee** (=) avec annotation de type.",
|
|
21
|
+
"assignation_augmentee": "L'assignation **augmentee** (+=, -=, etc.) combine operation et assignation.",
|
|
22
|
+
"fonction_def": "Une **fonction** est un bloc de code reutilisable defini avec `def`.",
|
|
23
|
+
"fonction_async": "Une **fonction asynchrone** utilise `async def`.",
|
|
24
|
+
"return": "**return** renvoie une valeur depuis une fonction.",
|
|
25
|
+
"yield": "**yield** transforme une fonction en generateur.",
|
|
26
|
+
"type_hint_param": "Les **type hints** (`param: type`) indiquent le type attendu.",
|
|
27
|
+
"type_hint_return": "Le **type hint de retour** (`-> type`) indique le type de retour.",
|
|
28
|
+
"docstring": "Une **docstring** (\"""...\""") decrit ce que fait la fonction.",
|
|
29
|
+
"decorateur": "Un **decorateur** (@) ajoute un comportement a une fonction.",
|
|
30
|
+
"lambda": "Une **lambda** est une fonction anonyme courte.",
|
|
31
|
+
"condition_if": "**if** execute un bloc si la condition est vraie.",
|
|
32
|
+
"condition_elif": "**elif** teste une autre condition.",
|
|
33
|
+
"condition_else": "**else** s'execute si tout est faux.",
|
|
34
|
+
"boucle_for": "**for** parcourt chaque element d'une collection.",
|
|
35
|
+
"boucle_while": "**while** repete tant que la condition est vraie.",
|
|
36
|
+
"boucle_async_for": "**async for** parcourt un iterateur asynchrone.",
|
|
37
|
+
"break": "**break** sort de la boucle immediatement.",
|
|
38
|
+
"continue": "**continue** passe a l'iteration suivante.",
|
|
39
|
+
"ternaire": "Le **ternaire** (`x if cond else y`) est un if en une ligne.",
|
|
40
|
+
"try_except": "**try/except** capture les erreurs sans planter.",
|
|
41
|
+
"try_finally": "**finally** s'execute toujours, erreur ou pas.",
|
|
42
|
+
"raise": "**raise** declenche une erreur volontairement.",
|
|
43
|
+
"context_manager_with": "**with** gere automatiquement les ressources.",
|
|
44
|
+
"context_manager_async_with": "**async with** pour les contextes asynchrones.",
|
|
45
|
+
"type_list": "Une **liste** `[]` est une collection ordonnee et modifiable.",
|
|
46
|
+
"type_dict": "Un **dict** `{}` associe des cles a des valeurs.",
|
|
47
|
+
"type_set": "Un **set** `{}` est un ensemble sans doublon.",
|
|
48
|
+
"type_tuple": "Un **tuple** `()` est immuable.",
|
|
49
|
+
"comprehension_liste": "Une **comprehension** cree une liste en une ligne.",
|
|
50
|
+
"comprehension_dict": "Une **comprehension** cree un dict en une ligne.",
|
|
51
|
+
"comprehension_set": "Une **comprehension** cree un set en une ligne.",
|
|
52
|
+
"assignation_index": "Modifie un element par son index ou sa cle.",
|
|
53
|
+
"slice": "Le **slicing** `[debut:fin:pas]` extrait une portion.",
|
|
54
|
+
"appel_fonction": "**appel de fonction** () execute une fonction.",
|
|
55
|
+
"appel_methode": "Une **methode** . est une fonction attachee a un objet.",
|
|
56
|
+
"operateur_comparaison": "Compare avec ==, !=, <, >, in, etc.",
|
|
57
|
+
"operateur_logique": "Combine avec **and**, **or**, **not**.",
|
|
58
|
+
"fstring": "Les **f-strings** f\"...\" integrent des variables.",
|
|
59
|
+
"import_module": "**import module** importe tout un module.",
|
|
60
|
+
"import_from": "**from module import** importe specifiquement.",
|
|
61
|
+
"class_def": "Une **classe** definit un nouveau type d'objet.",
|
|
62
|
+
"assignation_attr": "Modifie un attribut d'objet (`objet.attr = ...`).",
|
|
63
|
+
"unpacking": "L'**unpacking** (`*args`, `**kwargs`) decompose des sequences.",
|
|
64
|
+
"walrus_operator": "L'**operateur morse** (:=) assigne dans une expression.",
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def generate_vault(modules, concepts_bruts, output_dir):
|
|
69
|
+
"""Genere le vault Obsidian dans output_dir."""
|
|
70
|
+
output = Path(output_dir)
|
|
71
|
+
output.mkdir(parents=True, exist_ok=True)
|
|
72
|
+
obsidian_dir = output / ".obsidian"
|
|
73
|
+
obsidian_dir.mkdir(exist_ok=True)
|
|
74
|
+
|
|
75
|
+
_generate_index(modules, concepts_bruts, output)
|
|
76
|
+
_generate_obsidian_config(obsidian_dir)
|
|
77
|
+
|
|
78
|
+
for module_id, concepts in modules.items():
|
|
79
|
+
_generate_module(module_id, concepts, output)
|
|
80
|
+
|
|
81
|
+
print(f"Vault genere dans {output}/")
|
|
82
|
+
print(f" -> {len(modules)} modules")
|
|
83
|
+
print(f" -> {sum(len(c) for c in concepts_bruts.values())} concepts au total")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _generate_obsidian_config(obsidian_dir):
|
|
87
|
+
"""Cree la config minimale Obsidian."""
|
|
88
|
+
core = {
|
|
89
|
+
"file-explorer": True, "global-search": True, "switcher": True,
|
|
90
|
+
"graph": True, "backlink": True, "outgoing-link": True,
|
|
91
|
+
"tag-pane": True, "page-preview": True, "templates": True,
|
|
92
|
+
"command-palette": True, "outline": True, "word-count": True,
|
|
93
|
+
"daily-notes": False,
|
|
94
|
+
}
|
|
95
|
+
with open(obsidian_dir / "core-plugins.json", "w", encoding="utf-8") as f:
|
|
96
|
+
import json
|
|
97
|
+
json.dump(core, f, indent=2)
|
|
98
|
+
|
|
99
|
+
app = {}
|
|
100
|
+
with open(obsidian_dir / "app.json", "w", encoding="utf-8") as f:
|
|
101
|
+
json.dump(app, f, indent=2)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _generate_index(modules, concepts_bruts, output):
|
|
105
|
+
"""Genere la page d'accueil du vault."""
|
|
106
|
+
path = output / "Index.md"
|
|
107
|
+
total_concepts = len(concepts_bruts)
|
|
108
|
+
total_occ = sum(len(v) for v in concepts_bruts.values())
|
|
109
|
+
|
|
110
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
111
|
+
f.write("# VibeToDev — Parcours d'apprentissage\n\n")
|
|
112
|
+
f.write("Vault genere automatiquement par analyse AST.\n\n")
|
|
113
|
+
f.write(f"- **{total_concepts} concepts** distincts\n")
|
|
114
|
+
f.write(f"- **{total_occ} occurrences** dans le code\n\n")
|
|
115
|
+
|
|
116
|
+
f.write("## Modules\n\n")
|
|
117
|
+
f.write("| Module | Contenu |\n")
|
|
118
|
+
f.write("|--------|--------|\n")
|
|
119
|
+
for mid in sorted(modules.keys()):
|
|
120
|
+
nom = NOMS_MODULES.get(mid, f"{mid:02d}")
|
|
121
|
+
titre = TITRES_MODULES.get(mid, f"Module {mid}")
|
|
122
|
+
concepts_count = sum(len(v) for v in modules[mid].values())
|
|
123
|
+
f.write(f"| [[{nom}/Lecon|Module {mid}]] | {titre} ({concepts_count} occ) |\n")
|
|
124
|
+
|
|
125
|
+
f.write("\n## Tous les concepts\n\n")
|
|
126
|
+
f.write("| Concept | Niveau | Occurrences |\n")
|
|
127
|
+
f.write("|---------|--------|------------|\n")
|
|
128
|
+
for concept in sorted(concepts_bruts.keys()):
|
|
129
|
+
niveau = NIVEAU_CONCEPT.get(concept, "intermediaire")
|
|
130
|
+
occ = len(concepts_bruts[concept])
|
|
131
|
+
f.write(f"| {concept} | {niveau} | {occ} |\n")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _generate_module(module_id, concepts, output):
|
|
135
|
+
"""Genere la lecon pour un module."""
|
|
136
|
+
nom_dossier = NOMS_MODULES.get(module_id, f"{module_id:02d}")
|
|
137
|
+
titre = TITRES_MODULES.get(module_id, f"Module {module_id}")
|
|
138
|
+
dossier = output / nom_dossier
|
|
139
|
+
dossier.mkdir(exist_ok=True)
|
|
140
|
+
|
|
141
|
+
path = dossier / "Lecon.md"
|
|
142
|
+
total_occ_module = sum(len(v) for v in concepts.values())
|
|
143
|
+
|
|
144
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
145
|
+
f.write(f"# Module {module_id} : {titre}\n\n")
|
|
146
|
+
f.write(f"> {total_occ_module} occurrences dans le projet\n\n")
|
|
147
|
+
|
|
148
|
+
for concept in sorted(concepts.keys()):
|
|
149
|
+
occs = concepts[concept]
|
|
150
|
+
nom_concept = concept.replace("_", " ").title()
|
|
151
|
+
explication = EXPLICATIONS.get(concept, "")
|
|
152
|
+
niveau = NIVEAU_CONCEPT.get(concept, "intermediaire")
|
|
153
|
+
|
|
154
|
+
f.write(f"## {nom_concept}\n")
|
|
155
|
+
f.write(f"_{len(occs)} occurrences | Niveau: {niveau}_\n\n")
|
|
156
|
+
|
|
157
|
+
if explication:
|
|
158
|
+
f.write(f"{explication}\n\n")
|
|
159
|
+
|
|
160
|
+
# Exemples de code
|
|
161
|
+
f.write("### Dans le code\n\n")
|
|
162
|
+
for i, occ in enumerate(occs[:3]):
|
|
163
|
+
f.write(f"**Exemple {i+1}** — `{occ['fichier']}:{occ['ligne']}`\n\n")
|
|
164
|
+
f.write("```python\n")
|
|
165
|
+
f.write(f"{occ['code']}\n")
|
|
166
|
+
f.write("```\n\n")
|
|
167
|
+
|
|
168
|
+
# Autres occurrences
|
|
169
|
+
if len(occs) > 3:
|
|
170
|
+
par_fichier = defaultdict(list)
|
|
171
|
+
for occ in occs[3:]:
|
|
172
|
+
par_fichier[occ["fichier"]].append(occ["ligne"])
|
|
173
|
+
f.write(f"**{len(occs) - 3} autres occurrences :**\n\n")
|
|
174
|
+
for fichier, lignes in sorted(par_fichier.items()):
|
|
175
|
+
lignes_str = ", ".join(str(l) for l in lignes[:5])
|
|
176
|
+
if len(lignes) > 5:
|
|
177
|
+
lignes_str += f", ... ({len(lignes) - 5} de plus)"
|
|
178
|
+
f.write(f"- `{fichier}` : lignes {lignes_str}\n")
|
|
179
|
+
f.write("\n")
|
|
180
|
+
|
|
181
|
+
# Mini-exercice genere
|
|
182
|
+
f.write("### Mini-exercice\n\n")
|
|
183
|
+
f.write("```python\n")
|
|
184
|
+
f.write(_generer_exercice(concept))
|
|
185
|
+
f.write("\n```\n\n")
|
|
186
|
+
|
|
187
|
+
f.write("---\n\n")
|
|
188
|
+
|
|
189
|
+
# Navigation
|
|
190
|
+
f.write("## Navigation\n\n")
|
|
191
|
+
if module_id > 1:
|
|
192
|
+
prev = NOMS_MODULES.get(module_id - 1)
|
|
193
|
+
f.write(f"- [[../{prev}/Lecon|Module {module_id - 1}]]\n")
|
|
194
|
+
if module_id < 9:
|
|
195
|
+
next_mod = NOMS_MODULES.get(module_id + 1)
|
|
196
|
+
f.write(f"- [[../{next_mod}/Lecon|Module {module_id + 1}]]\n")
|
|
197
|
+
f.write("- [[Index|Retour a l'accueil]]\n")
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _generer_exercice(concept):
|
|
201
|
+
"""Genere un mini-exercice adapte au concept."""
|
|
202
|
+
exercices = {
|
|
203
|
+
"type_str": 'nom = "VotrePrenom"\nprint(type(nom))\nprint(nom.upper())',
|
|
204
|
+
"type_int": 'age = 25\nannee = 2026 - age\nprint(annee)',
|
|
205
|
+
"type_bool": 'est_actif = True\nif est_actif:\n print("Actif")',
|
|
206
|
+
"type_float": 'prix = 19.99\ntva = prix * 0.2\nprint(tva)',
|
|
207
|
+
"type_none": 'resultat = None\nif resultat is None:\n print("Pas de resultat")',
|
|
208
|
+
"assignation": 'nom = "Sophie"\nage = 28\nprint(nom, age)',
|
|
209
|
+
"fonction_def": 'def saluer(nom):\n return f"Bonjour {nom}"\nprint(saluer("Sophie"))',
|
|
210
|
+
"return": 'def addition(a, b):\n return a + b\nprint(addition(3, 4))',
|
|
211
|
+
"condition_if": 'note = 15\nif note >= 10:\n print("Reussi")',
|
|
212
|
+
"boucle_for": 'noms = ["Alice", "Bob", "Charlie"]\nfor n in noms:\n print(n)',
|
|
213
|
+
"try_except": 'try:\n x = 1 / 0\nexcept ZeroDivisionError:\n print("Division par zero")',
|
|
214
|
+
"type_list": 'fruits = ["pomme", "banane"]\nfruits.append("kiwi")\nprint(fruits[0])',
|
|
215
|
+
"type_dict": 'profil = {"nom": "Alice", "age": 30}\nprint(profil["nom"])',
|
|
216
|
+
"type_set": 'a = {"x", "y"}\nb = {"y", "z"}\nprint(a & b) # intersection',
|
|
217
|
+
"type_tuple": 'coords = (48.85, 2.35)\nprint(coords[0])',
|
|
218
|
+
"comprehension_liste": 'nombres = [1, 2, 3, 4, 5]\npairs = [n for n in nombres if n % 2 == 0]\nprint(pairs)',
|
|
219
|
+
"appel_fonction": 'print("Hello")\nlen([1, 2, 3])',
|
|
220
|
+
"appel_methode": 'texte = "Bonjour"\nprint(texte.lower())\nprint(texte.upper())',
|
|
221
|
+
"fstring": 'nom = "Sophie"\nprint(f"Bonjour {nom}")',
|
|
222
|
+
"import_module": 'import math\nprint(math.sqrt(16))',
|
|
223
|
+
"import_from": 'from math import sqrt\nprint(sqrt(16))',
|
|
224
|
+
"decorateur": 'def mon_decorateur(f):\n def wrapper():\n print("Avant")\n f()\n print("Apres")\n return wrapper\n\n@mon_decorateur\ndef dire_bonjour():\n print("Bonjour")',
|
|
225
|
+
"lambda": 'carre = lambda x: x ** 2\nprint(carre(5))',
|
|
226
|
+
"class_def": 'class Chien:\n def __init__(self, nom):\n self.nom = nom\n def aboyer(self):\n return "Woof"\n\nmedor = Chien("Medor")\nprint(medor.aboyer())',
|
|
227
|
+
"context_manager_with": 'with open("test.txt", "w") as f:\n f.write("Hello")',
|
|
228
|
+
"break": 'for i in range(10):\n if i == 5:\n break\n print(i)',
|
|
229
|
+
"continue": 'for i in range(5):\n if i == 2:\n continue\n print(i)',
|
|
230
|
+
"operateur_comparaison": 'a, b = 5, 10\nprint(a < b)\nprint(a == b)',
|
|
231
|
+
"operateur_logique": 'age, pays = 20, "FR"\nif age >= 18 and pays == "FR":\n print("OK")',
|
|
232
|
+
"slice": 'liste = [0, 1, 2, 3, 4, 5]\nprint(liste[1:4])\nprint(liste[::-1])',
|
|
233
|
+
"assignation_index": 'liste = [1, 2, 3]\nliste[0] = 99\nprint(liste)',
|
|
234
|
+
"assignation_multiple": 'a, b, c = 1, 2, 3\nprint(a, b, c)',
|
|
235
|
+
"assignation_augmentee": 'compteur = 0\ncompteur += 1\nprint(compteur)',
|
|
236
|
+
"ternaire": 'age = 20\nstatut = "majeur" if age >= 18 else "mineur"\nprint(statut)',
|
|
237
|
+
"yield": 'def compter(n):\n for i in range(n):\n yield i\nfor x in compter(3):\n print(x)',
|
|
238
|
+
"walrus_operator": 'if (n := len([1, 2, 3])) > 0:\n print(f"Longueur: {n}")',
|
|
239
|
+
}
|
|
240
|
+
return exercices.get(concept, f'# Exercice: {concept}\n# Ecris du code qui utilise "{concept}"\npass')
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Scanner AST : analyse tous les fichiers Python d'un projet
|
|
3
|
+
et extrait chaque concept avec sa position exacte (fichier:ligne).
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import ast
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from collections import defaultdict
|
|
10
|
+
|
|
11
|
+
IGNORE_DIRS = {"__pycache__", "venv", ".venv", "node_modules", ".git", ".vibetodev", ".vibetodev-vault"}
|
|
12
|
+
IGNORE_FILES = {"__init__.py"}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ConceptExtractor(ast.NodeVisitor):
|
|
16
|
+
"""Visite l'AST et extrait les concepts avec fichier:ligne."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, fichier_rel, lignes_source):
|
|
19
|
+
self.fichier = fichier_rel
|
|
20
|
+
self.lignes = lignes_source
|
|
21
|
+
self.concepts = defaultdict(list)
|
|
22
|
+
|
|
23
|
+
def _extraire_code(self, node):
|
|
24
|
+
debut = max(0, getattr(node, 'lineno', 1) - 1)
|
|
25
|
+
fin = getattr(node, 'end_lineno', debut + 1) or (debut + 1)
|
|
26
|
+
if fin > len(self.lignes):
|
|
27
|
+
fin = len(self.lignes)
|
|
28
|
+
return "".join(self.lignes[debut:fin]).strip()
|
|
29
|
+
|
|
30
|
+
def _add(self, concept, node):
|
|
31
|
+
self.concepts[concept].append({
|
|
32
|
+
"fichier": self.fichier,
|
|
33
|
+
"ligne": getattr(node, 'lineno', 0),
|
|
34
|
+
"code": self._extraire_code(node),
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
def visit_Import(self, node):
|
|
38
|
+
for alias in node.names:
|
|
39
|
+
self._add("import_module", node)
|
|
40
|
+
self.generic_visit(node)
|
|
41
|
+
|
|
42
|
+
def visit_ImportFrom(self, node):
|
|
43
|
+
self._add("import_from", node)
|
|
44
|
+
self.generic_visit(node)
|
|
45
|
+
|
|
46
|
+
def visit_FunctionDef(self, node):
|
|
47
|
+
self._add("fonction_def", node)
|
|
48
|
+
if node.decorator_list:
|
|
49
|
+
self._add("decorateur", node)
|
|
50
|
+
for a in node.args.args:
|
|
51
|
+
if a.annotation:
|
|
52
|
+
self._add("type_hint_param", a)
|
|
53
|
+
if node.returns:
|
|
54
|
+
self._add("type_hint_return", node)
|
|
55
|
+
body = node.body
|
|
56
|
+
if body and isinstance(body[0], ast.Expr) and isinstance(body[0].value, ast.Constant) and isinstance(body[0].value.value, str):
|
|
57
|
+
self._add("docstring", node)
|
|
58
|
+
self.generic_visit(node)
|
|
59
|
+
|
|
60
|
+
def visit_AsyncFunctionDef(self, node):
|
|
61
|
+
self._add("fonction_async", node)
|
|
62
|
+
self.generic_visit(node)
|
|
63
|
+
|
|
64
|
+
def visit_ClassDef(self, node):
|
|
65
|
+
self._add("class_def", node)
|
|
66
|
+
self.generic_visit(node)
|
|
67
|
+
|
|
68
|
+
def visit_Lambda(self, node):
|
|
69
|
+
self._add("lambda", node)
|
|
70
|
+
self.generic_visit(node)
|
|
71
|
+
|
|
72
|
+
def visit_Return(self, node):
|
|
73
|
+
self._add("return", node)
|
|
74
|
+
self.generic_visit(node)
|
|
75
|
+
|
|
76
|
+
def visit_Yield(self, node):
|
|
77
|
+
self._add("yield", node)
|
|
78
|
+
self.generic_visit(node)
|
|
79
|
+
|
|
80
|
+
def visit_Assign(self, node):
|
|
81
|
+
for target in node.targets:
|
|
82
|
+
if isinstance(target, (ast.Tuple, ast.List)):
|
|
83
|
+
self._add("assignation_multiple", node)
|
|
84
|
+
elif isinstance(target, ast.Name):
|
|
85
|
+
self._add("assignation", node)
|
|
86
|
+
elif isinstance(target, ast.Subscript):
|
|
87
|
+
self._add("assignation_index", node)
|
|
88
|
+
elif isinstance(target, ast.Attribute):
|
|
89
|
+
self._add("assignation_attr", node)
|
|
90
|
+
self.generic_visit(node)
|
|
91
|
+
|
|
92
|
+
def visit_AnnAssign(self, node):
|
|
93
|
+
self._add("assignation_typee", node)
|
|
94
|
+
self.generic_visit(node)
|
|
95
|
+
|
|
96
|
+
def visit_AugAssign(self, node):
|
|
97
|
+
self._add("assignation_augmentee", node)
|
|
98
|
+
self.generic_visit(node)
|
|
99
|
+
|
|
100
|
+
def visit_For(self, node):
|
|
101
|
+
self._add("boucle_for", node)
|
|
102
|
+
self.generic_visit(node)
|
|
103
|
+
|
|
104
|
+
def visit_AsyncFor(self, node):
|
|
105
|
+
self._add("boucle_async_for", node)
|
|
106
|
+
self.generic_visit(node)
|
|
107
|
+
|
|
108
|
+
def visit_While(self, node):
|
|
109
|
+
self._add("boucle_while", node)
|
|
110
|
+
self.generic_visit(node)
|
|
111
|
+
|
|
112
|
+
def visit_Break(self, node):
|
|
113
|
+
self._add("break", node)
|
|
114
|
+
self.generic_visit(node)
|
|
115
|
+
|
|
116
|
+
def visit_Continue(self, node):
|
|
117
|
+
self._add("continue", node)
|
|
118
|
+
self.generic_visit(node)
|
|
119
|
+
|
|
120
|
+
def visit_If(self, node):
|
|
121
|
+
self._add("condition_if", node)
|
|
122
|
+
if node.orelse:
|
|
123
|
+
if isinstance(node.orelse[0], ast.If):
|
|
124
|
+
self._add("condition_elif", node.orelse[0])
|
|
125
|
+
else:
|
|
126
|
+
self._add("condition_else", node)
|
|
127
|
+
self.generic_visit(node)
|
|
128
|
+
|
|
129
|
+
def visit_Try(self, node):
|
|
130
|
+
self._add("try_except", node)
|
|
131
|
+
if node.finalbody:
|
|
132
|
+
self._add("try_finally", node)
|
|
133
|
+
self.generic_visit(node)
|
|
134
|
+
|
|
135
|
+
def visit_Raise(self, node):
|
|
136
|
+
self._add("raise", node)
|
|
137
|
+
self.generic_visit(node)
|
|
138
|
+
|
|
139
|
+
def visit_With(self, node):
|
|
140
|
+
self._add("context_manager_with", node)
|
|
141
|
+
self.generic_visit(node)
|
|
142
|
+
|
|
143
|
+
def visit_AsyncWith(self, node):
|
|
144
|
+
self._add("context_manager_async_with", node)
|
|
145
|
+
self.generic_visit(node)
|
|
146
|
+
|
|
147
|
+
def visit_ListComp(self, node):
|
|
148
|
+
self._add("comprehension_liste", node)
|
|
149
|
+
self.generic_visit(node)
|
|
150
|
+
|
|
151
|
+
def visit_DictComp(self, node):
|
|
152
|
+
self._add("comprehension_dict", node)
|
|
153
|
+
self.generic_visit(node)
|
|
154
|
+
|
|
155
|
+
def visit_SetComp(self, node):
|
|
156
|
+
self._add("comprehension_set", node)
|
|
157
|
+
self.generic_visit(node)
|
|
158
|
+
|
|
159
|
+
def visit_List(self, node):
|
|
160
|
+
if not isinstance(node.ctx, ast.Store):
|
|
161
|
+
self._add("type_list", node)
|
|
162
|
+
self.generic_visit(node)
|
|
163
|
+
|
|
164
|
+
def visit_Dict(self, node):
|
|
165
|
+
self._add("type_dict", node)
|
|
166
|
+
self.generic_visit(node)
|
|
167
|
+
|
|
168
|
+
def visit_Set(self, node):
|
|
169
|
+
self._add("type_set", node)
|
|
170
|
+
self.generic_visit(node)
|
|
171
|
+
|
|
172
|
+
def visit_Tuple(self, node):
|
|
173
|
+
self._add("type_tuple", node)
|
|
174
|
+
self.generic_visit(node)
|
|
175
|
+
|
|
176
|
+
def visit_Constant(self, node):
|
|
177
|
+
val = node.value
|
|
178
|
+
if isinstance(val, str):
|
|
179
|
+
self._add("chaine" if val == "" else "type_str", node)
|
|
180
|
+
elif isinstance(val, bool):
|
|
181
|
+
self._add("type_bool", node)
|
|
182
|
+
elif isinstance(val, int):
|
|
183
|
+
self._add("type_int", node)
|
|
184
|
+
elif isinstance(val, float):
|
|
185
|
+
self._add("type_float", node)
|
|
186
|
+
elif val is None:
|
|
187
|
+
self._add("type_none", node)
|
|
188
|
+
elif isinstance(val, bytes):
|
|
189
|
+
self._add("type_bytes", node)
|
|
190
|
+
self.generic_visit(node)
|
|
191
|
+
|
|
192
|
+
def visit_Compare(self, node):
|
|
193
|
+
self._add("operateur_comparaison", node)
|
|
194
|
+
self.generic_visit(node)
|
|
195
|
+
|
|
196
|
+
def visit_BoolOp(self, node):
|
|
197
|
+
self._add("operateur_logique", node)
|
|
198
|
+
self.generic_visit(node)
|
|
199
|
+
|
|
200
|
+
def visit_BinOp(self, node):
|
|
201
|
+
op_type = type(node.op).__name__
|
|
202
|
+
self._add(f"binop_{op_type}", node)
|
|
203
|
+
self.generic_visit(node)
|
|
204
|
+
|
|
205
|
+
def visit_Call(self, node):
|
|
206
|
+
if isinstance(node.func, ast.Attribute):
|
|
207
|
+
self._add("appel_methode", node)
|
|
208
|
+
elif isinstance(node.func, ast.Name):
|
|
209
|
+
self._add("appel_fonction", node)
|
|
210
|
+
self.generic_visit(node)
|
|
211
|
+
|
|
212
|
+
def visit_JoinedStr(self, node):
|
|
213
|
+
self._add("fstring", node)
|
|
214
|
+
self.generic_visit(node)
|
|
215
|
+
|
|
216
|
+
def visit_Slice(self, node):
|
|
217
|
+
self._add("slice", node)
|
|
218
|
+
self.generic_visit(node)
|
|
219
|
+
|
|
220
|
+
def visit_IfExp(self, node):
|
|
221
|
+
self._add("ternaire", node)
|
|
222
|
+
self.generic_visit(node)
|
|
223
|
+
|
|
224
|
+
def visit_Starred(self, node):
|
|
225
|
+
self._add("unpacking", node)
|
|
226
|
+
self.generic_visit(node)
|
|
227
|
+
|
|
228
|
+
def visit_walrus(self, node):
|
|
229
|
+
""":= operateur morse (Python 3.8+)"""
|
|
230
|
+
self._add("walrus_operator", node)
|
|
231
|
+
self.generic_visit(node)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def scan_project(source_dir, recursive=True):
|
|
235
|
+
"""
|
|
236
|
+
Scanne un projet et retourne {concept: [occurences]}.
|
|
237
|
+
Chaque occurrence a: fichier, ligne, code.
|
|
238
|
+
"""
|
|
239
|
+
source = Path(source_dir)
|
|
240
|
+
tous = defaultdict(list)
|
|
241
|
+
fichiers_trouves = []
|
|
242
|
+
|
|
243
|
+
if recursive:
|
|
244
|
+
for root, dirs, files in os.walk(source):
|
|
245
|
+
dirs[:] = [d for d in dirs if d not in IGNORE_DIRS]
|
|
246
|
+
for f in files:
|
|
247
|
+
if f.endswith(".py") and f not in IGNORE_FILES:
|
|
248
|
+
fichiers_trouves.append(Path(root) / f)
|
|
249
|
+
else:
|
|
250
|
+
for f in source.glob("*.py"):
|
|
251
|
+
if f.name not in IGNORE_FILES:
|
|
252
|
+
fichiers_trouves.append(f)
|
|
253
|
+
|
|
254
|
+
for chemin in sorted(set(fichiers_trouves)):
|
|
255
|
+
try:
|
|
256
|
+
rel = str(chemin.relative_to(source.parent)) if source.parent in chemin.parents else str(chemin)
|
|
257
|
+
except ValueError:
|
|
258
|
+
rel = str(chemin)
|
|
259
|
+
|
|
260
|
+
try:
|
|
261
|
+
with open(chemin, "r", encoding="utf-8", errors="replace") as f:
|
|
262
|
+
lignes = f.readlines()
|
|
263
|
+
except Exception:
|
|
264
|
+
continue
|
|
265
|
+
|
|
266
|
+
try:
|
|
267
|
+
arbre = ast.parse("".join(lignes))
|
|
268
|
+
except SyntaxError:
|
|
269
|
+
continue
|
|
270
|
+
|
|
271
|
+
extracteur = ConceptExtractor(rel, lignes)
|
|
272
|
+
extracteur.visit(arbre)
|
|
273
|
+
|
|
274
|
+
for concept, occs in extracteur.concepts.items():
|
|
275
|
+
tous[concept].extend(occs)
|
|
276
|
+
|
|
277
|
+
return dict(tous)
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Validateur d'exercices : execute le fichier, verifie la sortie,
|
|
3
|
+
et donne un feedback sans reveler la solution.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import ast
|
|
7
|
+
import sys
|
|
8
|
+
import io
|
|
9
|
+
import traceback
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def validate_exercise(fichier, concept=None):
|
|
14
|
+
"""
|
|
15
|
+
Valide un fichier exercice.
|
|
16
|
+
Retourne un dict avec status, message, hint.
|
|
17
|
+
"""
|
|
18
|
+
path = Path(fichier)
|
|
19
|
+
if not path.exists():
|
|
20
|
+
return {
|
|
21
|
+
"status": "error",
|
|
22
|
+
"message": f"Fichier '{fichier}' introuvable.",
|
|
23
|
+
"hint": "Cree le fichier avec les variables demandees."
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
28
|
+
code = f.read()
|
|
29
|
+
except Exception as e:
|
|
30
|
+
return {
|
|
31
|
+
"status": "error",
|
|
32
|
+
"message": f"Impossible de lire le fichier: {e}",
|
|
33
|
+
"hint": "Verifie que le fichier est accessible."
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
# Verifie la syntaxe
|
|
37
|
+
try:
|
|
38
|
+
tree = ast.parse(code)
|
|
39
|
+
except SyntaxError as e:
|
|
40
|
+
return {
|
|
41
|
+
"status": "error",
|
|
42
|
+
"message": f"Erreur de syntaxe ligne {e.lineno}: {e.msg}",
|
|
43
|
+
"hint": f"Verifie les parentheses, les guillemets, et l'indentation vers la ligne {e.lineno}."
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# Execute le code
|
|
47
|
+
namespace = {}
|
|
48
|
+
old_stdout = sys.stdout
|
|
49
|
+
sys.stdout = io.StringIO()
|
|
50
|
+
try:
|
|
51
|
+
exec(code, namespace)
|
|
52
|
+
output = sys.stdout.getvalue()
|
|
53
|
+
except Exception as e:
|
|
54
|
+
sys.stdout = old_stdout
|
|
55
|
+
tb = traceback.format_exc()
|
|
56
|
+
return {
|
|
57
|
+
"status": "error",
|
|
58
|
+
"message": f"Erreur d'execution: {type(e).__name__}: {e}",
|
|
59
|
+
"debug": tb.split("\n")[-4:-1],
|
|
60
|
+
"hint": "Lis la derniere ligne de l'erreur. Que dit-elle ?"
|
|
61
|
+
}
|
|
62
|
+
finally:
|
|
63
|
+
sys.stdout = old_stdout
|
|
64
|
+
|
|
65
|
+
# Verifications generiques
|
|
66
|
+
erreurs = []
|
|
67
|
+
reussites = []
|
|
68
|
+
|
|
69
|
+
# Verifie qu'il y a des variables definies
|
|
70
|
+
user_vars = {k: v for k, v in namespace.items()
|
|
71
|
+
if not k.startswith("_") and not callable(v)}
|
|
72
|
+
if user_vars:
|
|
73
|
+
reussites.append(f"{len(user_vars)} variable(s) definie(s)")
|
|
74
|
+
else:
|
|
75
|
+
erreurs.append("Aucune variable definie")
|
|
76
|
+
|
|
77
|
+
# Verifie qu'il y a un print ou une sortie
|
|
78
|
+
if output.strip():
|
|
79
|
+
reussites.append(f"Sortie produite ({len(output)} caracteres)")
|
|
80
|
+
else:
|
|
81
|
+
erreurs.append("Aucune sortie (as-tu utilise print() ?)")
|
|
82
|
+
|
|
83
|
+
# Verifie les types de base si presents
|
|
84
|
+
for nom, val in user_vars.items():
|
|
85
|
+
if isinstance(val, str):
|
|
86
|
+
reussites.append(f" {nom}: str OK")
|
|
87
|
+
elif isinstance(val, int):
|
|
88
|
+
reussites.append(f" {nom}: int OK")
|
|
89
|
+
elif isinstance(val, float):
|
|
90
|
+
reussites.append(f" {nom}: float OK")
|
|
91
|
+
elif isinstance(val, bool):
|
|
92
|
+
reussites.append(f" {nom}: bool OK")
|
|
93
|
+
elif isinstance(val, list):
|
|
94
|
+
reussites.append(f" {nom}: list[{len(val)} elements] OK")
|
|
95
|
+
elif isinstance(val, dict):
|
|
96
|
+
reussites.append(f" {nom}: dict[{len(val)} cles] OK")
|
|
97
|
+
elif isinstance(val, set):
|
|
98
|
+
reussites.append(f" {nom}: set[{len(val)} elements] OK")
|
|
99
|
+
elif isinstance(val, tuple):
|
|
100
|
+
reussites.append(f" {nom}: tuple[{len(val)} elements] OK")
|
|
101
|
+
|
|
102
|
+
if erreurs and not reussites:
|
|
103
|
+
return {
|
|
104
|
+
"status": "error",
|
|
105
|
+
"message": "\n".join(erreurs),
|
|
106
|
+
"hint": "Ajoute des variables et un print() pour voir le resultat."
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if erreurs:
|
|
110
|
+
return {
|
|
111
|
+
"status": "partial",
|
|
112
|
+
"reussites": reussites,
|
|
113
|
+
"erreurs": erreurs,
|
|
114
|
+
"hint": "Relis la lecon et verifie ce qui manque."
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
"status": "success",
|
|
119
|
+
"reussites": reussites,
|
|
120
|
+
"message": "Tout est correct ! Passe au concept suivant.",
|
|
121
|
+
"output": output.strip()
|
|
122
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vibetodev
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Transforme n'importe quel projet vibecode en parcours d'apprentissage
|
|
5
|
+
Author: ConnectPro
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/connectpro/vibetodev
|
|
8
|
+
Project-URL: Source, https://github.com/connectpro/vibetodev
|
|
9
|
+
Keywords: learning,education,code-analysis,obsidian,ast
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Intended Audience :: Education
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Education
|
|
21
|
+
Classifier: Topic :: Software Development :: Code Generators
|
|
22
|
+
Requires-Python: >=3.8
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
Dynamic: requires-python
|
|
25
|
+
|
|
26
|
+
# VibeToDev
|
|
27
|
+
|
|
28
|
+
Transforme n'importe quel projet Python vibecodé en parcours d'apprentissage structuré.
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install vibetodev
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Ou depuis les sources :
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
git clone https://github.com/connectpro/vibetodev
|
|
40
|
+
cd vibetodev
|
|
41
|
+
pip install .
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Utilisation
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# Scanner un projet et generer un vault Obsidian
|
|
48
|
+
vibetodev scan /chemin/vers/mon_projet --output /chemin/vers/mon_vault
|
|
49
|
+
|
|
50
|
+
# Valider un exercice
|
|
51
|
+
vibetodev check mon_exercice.py
|
|
52
|
+
|
|
53
|
+
# Initialiser vibetodev dans un projet
|
|
54
|
+
vibetodev init /chemin/vers/mon_projet
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Comment ca marche
|
|
58
|
+
|
|
59
|
+
1. **Scan** — Analyse AST de tous les fichiers Python, extrait 50+ concepts avec leur position exacte
|
|
60
|
+
2. **Categorisation** — 9 modules pedagogiques (variables, fonctions, classes, etc.)
|
|
61
|
+
3. **Generation** — Cree un vault Obsidian avec lecons, exemples de code, et exercices
|
|
62
|
+
4. **Validation** — Execute et verifie les exercices sans donner la solution
|
|
63
|
+
|
|
64
|
+
## Licence
|
|
65
|
+
|
|
66
|
+
MIT
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
setup.py
|
|
4
|
+
vibetodev/__init__.py
|
|
5
|
+
vibetodev/__main__.py
|
|
6
|
+
vibetodev/categorizer.py
|
|
7
|
+
vibetodev/cli.py
|
|
8
|
+
vibetodev/generator.py
|
|
9
|
+
vibetodev/scanner.py
|
|
10
|
+
vibetodev/validator.py
|
|
11
|
+
vibetodev.egg-info/PKG-INFO
|
|
12
|
+
vibetodev.egg-info/SOURCES.txt
|
|
13
|
+
vibetodev.egg-info/dependency_links.txt
|
|
14
|
+
vibetodev.egg-info/entry_points.txt
|
|
15
|
+
vibetodev.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
vibetodev
|