3.3 Application Projet : Modélisation et POO
Nous allons maintenant transformer pyCycloneFlow. Au lieu de manipuler des dictionnaires {"name": "...", "version": "..."}, nous allons créer de vrais objets Component.
1. Création de la classe Component
Cette classe représentera une brique de notre futur SBOM.
class Component:
def __init__(self, name: str, version: str):
self.name = name
self.version = version
def __repr__(self):
return f"Component(name='{self.name}', version='{self.version}')"
def to_dict(self):
"""Prépare l'export JSON futur."""
return {"name": self.name, "version": self.version}
Projet Fil Rouge : Refactoring de main.py
On fait évoluer le script vers la POO, on ajoute la classe Component.
import pathlib
import re
import logging
def load_scan_file(file_path: str) -> str:
"""Ouvre un fichier et renvoie son contenu, ou une chaĂźne vide en cas d'erreur."""
try:
path = pathlib.Path(file_path)
return path.read_text(encoding="utf-8")
except FileNotFoundError:
logging.error(f"Fichier introuvable : {file_path}")
return ""
except PermissionError:
logging.error(f"Permission refusée : {file_path}")
return ""
except Exception as e:
logging.error(f"Erreur inattendue : {e}")
return ""
def parse_scan_data(raw_data: str) -> list[dict]:
"""Transforme le text brut en liste de composants."""
components = []
if not raw_data: return components
lines = raw_data.strip().split('\n')
for line in lines:
if "Package" in line or "---" in line: continue
# Regex: capture le nom et la version
match = re.search(r"(\S+)\s+([\d\.]+)", line)
if match:
# Création de l'objet au lieu d'un dictionnaire
obj = Components(name=match.group(1),version=match.group(2))
components.append(obj)
return components
class Components:
def __init__(self, name: str, version: str):
self.name = name
self.version = version
def __str__(self):
return f"{self.name} == {self.version}"
def main():
logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')
print("--- pyCycloneFlow : Safe Opener ---")
raw = load_scan_file("scanner.txt")
inventory = parse_scan_data(raw)
print(f"--- Inventaire pyCycloneFlow ({len(inventory)} item)s ---")
for item in inventory:
print(f" â {item}")
if __name__ == "__main__":
main()
Résultat attendu en console :
$ python main.py
--- pyCycloneFlow : Safe Opener ---
--- Inventaire pyCycloneFlow (3 items) ---
â requests == 2.28.1
â flask == 2.2.2
â cryptography == 38.0.1
2. Validation des Tests (POO)
On met à jour tests/test_opener.py. Notez que l'on vérifie maintenant si le résultat est bien une instance de notre classe.
import unittest
from main import load_scan_file, parse_scan_data, Components
class TestCycloneFlow(unittest.TestCase):
# --- Phase 1 : Safe Opener ---
def test_file_not_found(self):
result = load_scan_file("fichier_imaginaire.txt")
self.assertEqual(result, "")
def test_load_success(self):
result = load_scan_file("scanner.txt")
self.assertIn("Package", result)
# --- Phase 2 : Parser ---
def test_parsing_retourne_composant(self):
result = parse_scan_data("my-lib 1.0.0")
self.assertEqual(len(result), 1)
self.assertIsInstance(result[0], Components)
self.assertEqual(result[0].name, "my-lib")
self.assertEqual(result[0].version, "1.0.0")
def test_parsing_ignore_header(self):
sample = "Package Version\n---\nrequests 2.28.1"
result = parse_scan_data(sample)
self.assertEqual(len(result), 1)
self.assertEqual(result[0].name, "requests")
def test_parsing_chaine_vide(self):
self.assertEqual(parse_scan_data(""), [])
# --- Components ---
def test_components_str(self):
c = Components("requests", "2.28.1")
self.assertEqual(str(c), "requests == 2.28.1")
if __name__ == '__main__':
unittest.main()
Résultat attendu du test :
$ python -m unittest tests.test_opener
.ERROR:root:Fichier introuvable : fichier_imaginaire.txt
.....
----------------------------------------------------------------------
Ran 6 tests in 0.001s
OK