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
Chapitre précédent Chapitre suivant : Refactorisation Projet