Debutant 17 min de lecture · 3 552 mots

Programmation orientée objet en Python : Classes et héritage (Partie 2)

Projet pratique : Système de bibliothèque

"""
project/library.py - Système de gestion de bibliothèque complet
"""

from typing import List, Optional, Dict, Set
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from abc import ABC, abstractmethod
from enum import Enum
import json

class StatutEmprunt(Enum):
    """Statuts possibles d'un emprunt."""
    EN_COURS = "en cours"
    RETOURNE = "retourné"
    EN_RETARD = "en retard"

class TypeDocument(Enum):
    """Types de documents disponibles."""
    LIVRE = "livre"
    MAGAZINE = "magazine"
    DVD = "dvd"
    CD = "cd"

# Classes de base
class Document(ABC):
    """
    Classe abstraite représentant un document de bibliothèque.

    Attributes:
        id: Identifiant unique
        titre: Titre du document
        disponible: Si le document est disponible
    """

    def __init__(self, id_doc: str, titre: str) -> None:
        """
        Initialise un document.

        Args:
            id_doc: Identifiant unique
            titre: Titre du document
        """
        self.id = id_doc
        self.titre = titre
        self.disponible = True
        self.date_ajout = datetime.now()

    @abstractmethod
    def duree_emprunt(self) -> int:
        """Retourne la durée d'emprunt en jours."""
        pass

    @abstractmethod
    def type_document(self) -> TypeDocument:
        """Retourne le type du document."""
        pass

    def emprunter(self) -> bool:
        """
        Marque le document comme emprunté.

        Returns:
            True si l'emprunt est possible
        """
        if not self.disponible:
            return False
        self.disponible = False
        return True

    def retourner(self) -> bool:
        """
        Marque le document comme retourné.

        Returns:
            True si le retour est effectué
        """
        if self.disponible:
            return False
        self.disponible = True
        return True

    def __str__(self) -> str:
        """Représentation string du document."""
        statut = "Disponible" if self.disponible else "Emprunté"
        return f"[{self.id}] {self.titre} - {statut}"

    def __eq__(self, other: object) -> bool:
        """Comparaison d'égalité."""
        if not isinstance(other, Document):
            return NotImplemented
        return self.id == other.id

    def __hash__(self) -> int:
        """Hash pour utilisation dans sets et dict."""
        return hash(self.id)

class Livre(Document):
    """
    Livre de bibliothèque.

    Attributes:
        auteur: Auteur du livre
        isbn: Code ISBN
        nombre_pages: Nombre de pages
    """

    def __init__(
        self,
        id_doc: str,
        titre: str,
        auteur: str,
        isbn: str,
        nombre_pages: int
    ) -> None:
        """
        Initialise un livre.

        Args:
            id_doc: Identifiant unique
            titre: Titre du livre
            auteur: Auteur
            isbn: Code ISBN
            nombre_pages: Nombre de pages
        """
        super().__init__(id_doc, titre)
        self.auteur = auteur
        self.isbn = isbn
        self.nombre_pages = nombre_pages

    def duree_emprunt(self) -> int:
        """Durée d'emprunt d'un livre: 21 jours."""
        return 21

    def type_document(self) -> TypeDocument:
        """Type: livre."""
        return TypeDocument.LIVRE

    def __str__(self) -> str:
        """Représentation string du livre."""
        base = super().__str__()
        return f"{base} par {self.auteur}"

class Magazine(Document):
    """
    Magazine de bibliothèque.

    Attributes:
        numero: Numéro du magazine
        mois: Mois de publication
        annee: Année de publication
    """

    def __init__(
        self,
        id_doc: str,
        titre: str,
        numero: int,
        mois: str,
        annee: int
    ) -> None:
        """
        Initialise un magazine.

        Args:
            id_doc: Identifiant unique
            titre: Titre du magazine
            numero: Numéro
            mois: Mois de publication
            annee: Année de publication
        """
        super().__init__(id_doc, titre)
        self.numero = numero
        self.mois = mois
        self.annee = annee

    def duree_emprunt(self) -> int:
        """Durée d'emprunt d'un magazine: 7 jours."""
        return 7

    def type_document(self) -> TypeDocument:
        """Type: magazine."""
        return TypeDocument.MAGAZINE

    def __str__(self) -> str:
        """Représentation string du magazine."""
        base = super().__str__()
        return f"{base} - N°{self.numero} ({self.mois} {self.annee})"

class DVD(Document):
    """
    DVD de bibliothèque.

    Attributes:
        realisateur: Réalisateur du film
        duree_minutes: Durée en minutes
    """

    def __init__(
        self,
        id_doc: str,
        titre: str,
        realisateur: str,
        duree_minutes: int
    ) -> None:
        """
        Initialise un DVD.

        Args:
            id_doc: Identifiant unique
            titre: Titre du film
            realisateur: Réalisateur
            duree_minutes: Durée
        """
        super().__init__(id_doc, titre)
        self.realisateur = realisateur
        self.duree_minutes = duree_minutes

    def duree_emprunt(self) -> int:
        """Durée d'emprunt d'un DVD: 14 jours."""
        return 14

    def type_document(self) -> TypeDocument:
        """Type: DVD."""
        return TypeDocument.DVD

    def __str__(self) -> str:
        """Représentation string du DVD."""
        base = super().__str__()
        return f"{base} de {self.realisateur}"

@dataclass
class Emprunt:
    """
    Représente un emprunt de document.

    Attributes:
        document: Document emprunté
        date_emprunt: Date de l'emprunt
        date_retour_prevue: Date de retour prévue
        date_retour_effective: Date de retour effective (None si en cours)
    """
    document: Document
    date_emprunt: datetime = field(default_factory=datetime.now)
    date_retour_prevue: datetime = field(init=False)
    date_retour_effective: Optional[datetime] = None

    def __post_init__(self) -> None:
        """Calcule la date de retour prévue."""
        self.date_retour_prevue = (
            self.date_emprunt + timedelta(days=self.document.duree_emprunt())
        )

    @property
    def statut(self) -> StatutEmprunt:
        """Détermine le statut de l'emprunt."""
        if self.date_retour_effective:
            return StatutEmprunt.RETOURNE

        if datetime.now() > self.date_retour_prevue:
            return StatutEmprunt.EN_RETARD

        return StatutEmprunt.EN_COURS

    @property
    def jours_retard(self) -> int:
        """Calcule le nombre de jours de retard."""
        if self.statut != StatutEmprunt.EN_RETARD:
            return 0

        date_reference = self.date_retour_effective or datetime.now()
        delta = date_reference - self.date_retour_prevue
        return max(0, delta.days)

    def calculer_penalite(self, tarif_jour: float = 0.50) -> float:
        """
        Calcule la pénalité de retard.

        Args:
            tarif_jour: Tarif par jour de retard

        Returns:
            Montant de la pénalité
        """
        return self.jours_retard * tarif_jour

    def __str__(self) -> str:
        """Représentation string de l'emprunt."""
        retard_info = f" - {self.jours_retard}j de retard" if self.jours_retard > 0 else ""
        return (
            f"{self.document.titre} "
            f"(emprunté le {self.date_emprunt.strftime('%d/%m/%Y')}) "
            f"- {self.statut.value}{retard_info}"
        )

class Adherent:
    """
    Adhérent de la bibliothèque.

    Attributes:
        id: Identifiant unique
        nom: Nom de famille
        prenom: Prénom
        email: Adresse email
    """

    MAX_EMPRUNTS: int = 5  # Nombre max d'emprunts simultanés

    def __init__(
        self,
        id_adherent: str,
        nom: str,
        prenom: str,
        email: str
    ) -> None:
        """
        Initialise un adhérent.

        Args:
            id_adherent: Identifiant unique
            nom: Nom de famille
            prenom: Prénom
            email: Adresse email
        """
        self.id = id_adherent
        self.nom = nom
        self.prenom = prenom
        self.email = email
        self.date_inscription = datetime.now()
        self.emprunts_en_cours: List[Emprunt] = []
        self.historique_emprunts: List[Emprunt] = []

    @property
    def nom_complet(self) -> str:
        """Retourne le nom complet."""
        return f"{self.prenom} {self.nom}"

    def peut_emprunter(self) -> bool:
        """Vérifie si l'adhérent peut emprunter."""
        # Ne peut pas emprunter si nombre max atteint
        if len(self.emprunts_en_cours) >= self.MAX_EMPRUNTS:
            return False

        # Ne peut pas emprunter s'il a des retards
        for emprunt in self.emprunts_en_cours:
            if emprunt.statut == StatutEmprunt.EN_RETARD:
                return False

        return True

    def emprunter_document(self, document: Document) -> bool:
        """
        Emprunte un document.

        Args:
            document: Document à emprunter

        Returns:
            True si l'emprunt est effectué
        """
        if not self.peut_emprunter():
            return False

        if not document.emprunter():
            return False

        emprunt = Emprunt(document)
        self.emprunts_en_cours.append(emprunt)
        return True

    def retourner_document(self, document: Document) -> bool:
        """
        Retourne un document.

        Args:
            document: Document à retourner

        Returns:
            True si le retour est effectué
        """
        # Trouver l'emprunt correspondant
        emprunt_trouve = None
        for emprunt in self.emprunts_en_cours:
            if emprunt.document == document:
                emprunt_trouve = emprunt
                break

        if not emprunt_trouve:
            return False

        # Marquer comme retourné
        emprunt_trouve.date_retour_effective = datetime.now()
        document.retourner()

        # Déplacer dans l'historique
        self.emprunts_en_cours.remove(emprunt_trouve)
        self.historique_emprunts.append(emprunt_trouve)

        return True

    def penalites_totales(self) -> float:
        """Calcule le total des pénalités."""
        return sum(
            emprunt.calculer_penalite()
            for emprunt in self.emprunts_en_cours
            if emprunt.statut == StatutEmprunt.EN_RETARD
        )

    def __str__(self) -> str:
        """Représentation string de l'adhérent."""
        return (
            f"{self.nom_complet} ({self.id}) - "
            f"{len(self.emprunts_en_cours)} emprunt(s) en cours"
        )

class Bibliotheque:
    """
    Gestionnaire principal de la bibliothèque.

    Attributes:
        nom: Nom de la bibliothèque
        documents: Collection de documents
        adherents: Collection d'adhérents
    """

    def __init__(self, nom: str) -> None:
        """
        Initialise la bibliothèque.

        Args:
            nom: Nom de la bibliothèque
        """
        self.nom = nom
        self.documents: Dict[str, Document] = {}
        self.adherents: Dict[str, Adherent] = {}

    # Gestion des documents
    def ajouter_document(self, document: Document) -> None:
        """
        Ajoute un document au catalogue.

        Args:
            document: Document à ajouter

        Raises:
            ValueError: Si l'ID existe déjà
        """
        if document.id in self.documents:
            raise ValueError(f"Document {document.id} existe déjà")

        self.documents[document.id] = document
        print(f"✓ Document ajouté: {document}")

    def retirer_document(self, id_doc: str) -> bool:
        """
        Retire un document du catalogue.

        Args:
            id_doc: ID du document

        Returns:
            True si retiré avec succès
        """
        if id_doc not in self.documents:
            return False

        document = self.documents[id_doc]
        if not document.disponible:
            print(f"✗ Impossible de retirer {document}: document emprunté")
            return False

        del self.documents[id_doc]
        print(f"✓ Document retiré: {document}")
        return True

    def rechercher_documents(
        self,
        titre: Optional[str] = None,
        type_doc: Optional[TypeDocument] = None,
        disponible_seulement: bool = False
    ) -> List[Document]:
        """
        Recherche des documents.

        Args:
            titre: Recherche dans le titre (partielle)
            type_doc: Filtrer par type
            disponible_seulement: Uniquement les documents disponibles

        Returns:
            Liste des documents correspondants
        """
        resultats = list(self.documents.values())

        if titre:
            resultats = [
                d for d in resultats
                if titre.lower() in d.titre.lower()
            ]

        if type_doc:
            resultats = [
                d for d in resultats
                if d.type_document() == type_doc
            ]

        if disponible_seulement:
            resultats = [d for d in resultats if d.disponible]

        return resultats

    # Gestion des adhérents
    def inscrire_adherent(self, adherent: Adherent) -> None:
        """
        Inscrit un adhérent.

        Args:
            adherent: Adhérent à inscrire

        Raises:
            ValueError: Si l'ID existe déjà
        """
        if adherent.id in self.adherents:
            raise ValueError(f"Adhérent {adherent.id} existe déjà")

        self.adherents[adherent.id] = adherent
        print(f"✓ Adhérent inscrit: {adherent}")

    def desinscrire_adherent(self, id_adherent: str) -> bool:
        """
        Désinscrit un adhérent.

        Args:
            id_adherent: ID de l'adhérent

        Returns:
            True si désinscrit avec succès
        """
        if id_adherent not in self.adherents:
            return False

        adherent = self.adherents[id_adherent]
        if adherent.emprunts_en_cours:
            print(f"✗ Impossible de désinscrire {adherent}: emprunts en cours")
            return False

        del self.adherents[id_adherent]
        print(f"✓ Adhérent désinscrit: {adherent}")
        return True

    # Gestion des emprunts
    def emprunter(self, id_adherent: str, id_document: str) -> bool:
        """
        Effectue un emprunt.

        Args:
            id_adherent: ID de l'adhérent
            id_document: ID du document

        Returns:
            True si l'emprunt est effectué
        """
        if id_adherent not in self.adherents:
            print(f"✗ Adhérent {id_adherent} inconnu")
            return False

        if id_document not in self.documents:
            print(f"✗ Document {id_document} inconnu")
            return False

        adherent = self.adherents[id_adherent]
        document = self.documents[id_document]

        if not adherent.peut_emprunter():
            print(f"✗ {adherent.nom_complet} ne peut pas emprunter")
            return False

        if adherent.emprunter_document(document):
            print(f"✓ {adherent.nom_complet} a emprunté: {document.titre}")
            return True

        print(f"✗ Document {document.titre} non disponible")
        return False

    def retourner(self, id_adherent: str, id_document: str) -> bool:
        """
        Effectue un retour.

        Args:
            id_adherent: ID de l'adhérent
            id_document: ID du document

        Returns:
            True si le retour est effectué
        """
        if id_adherent not in self.adherents:
            return False

        if id_document not in self.documents:
            return False

        adherent = self.adherents[id_adherent]
        document = self.documents[id_document]

        if adherent.retourner_document(document):
            penalite = 0
            for emprunt in adherent.historique_emprunts:
                if emprunt.document == document:
                    penalite = emprunt.calculer_penalite()
                    break

            msg = f"✓ {adherent.nom_complet} a retourné: {document.titre}"
            if penalite > 0:
                msg += f" (pénalité: {penalite}€)"
            print(msg)
            return True

        print(f"✗ Erreur lors du retour")
        return False

    # Statistiques et rapports
    def statistiques(self) -> Dict[str, any]:
        """Génère des statistiques sur la bibliothèque."""
        total_documents = len(self.documents)
        documents_disponibles = sum(
            1 for d in self.documents.values() if d.disponible
        )

        emprunts_en_cours = sum(
            len(a.emprunts_en_cours) for a in self.adherents.values()
        )

        emprunts_en_retard = sum(
            1 for a in self.adherents.values()
            for e in a.emprunts_en_cours
            if e.statut == StatutEmprunt.EN_RETARD
        )

        penalites_totales = sum(
            a.penalites_totales() for a in self.adherents.values()
        )

        return {
            "total_documents": total_documents,
            "documents_disponibles": documents_disponibles,
            "documents_empruntes": total_documents - documents_disponibles,
            "total_adherents": len(self.adherents),
            "emprunts_en_cours": emprunts_en_cours,
            "emprunts_en_retard": emprunts_en_retard,
            "penalites_totales": penalites_totales
        }

    def afficher_statistiques(self) -> None:
        """Affiche les statistiques."""
        stats = self.statistiques()

        print(f"n=== Statistiques - {self.nom} ===")
        print(f"Documents: {stats['total_documents']} "
              f"({stats['documents_disponibles']} disponibles)")
        print(f"Adhérents: {stats['total_adherents']}")
        print(f"Emprunts en cours: {stats['emprunts_en_cours']}")
        print(f"Emprunts en retard: {stats['emprunts_en_retard']}")
        print(f"Pénalités totales: {stats['penalites_totales']}€")

    def documents_populaires(self, limite: int = 5) -> List[tuple]:
        """
        Retourne les documents les plus empruntés.

        Args:
            limite: Nombre de documents à retourner

        Returns:
            Liste de tuples (document, nombre_emprunts)
        """
        compteur: Dict[Document, int] = {}

        for adherent in self.adherents.values():
            tous_emprunts = (
                adherent.emprunts_en_cours +
                adherent.historique_emprunts
            )
            for emprunt in tous_emprunts:
                doc = emprunt.document
                compteur[doc] = compteur.get(doc, 0) + 1

        # Trier par nombre d'emprunts décroissant
        populaires = sorted(
            compteur.items(),
            key=lambda x: x[1],
            reverse=True
        )

        return populaires[:limite]

def demo_bibliotheque() -> None:
    """Démonstration du système de bibliothèque."""

    # Création de la bibliothèque
    biblio = Bibliotheque("Bibliothèque Municipale")

    print("=== Ajout de documents ===n")

    # Ajout de livres
    livre1 = Livre(
        "L001",
        "1984",
        "George Orwell",
        "978-0451524935",
        328
    )
    livre2 = Livre(
        "L002",
        "Le Petit Prince",
        "Antoine de Saint-Exupéry",
        "978-0156012195",
        96
    )

    biblio.ajouter_document(livre1)
    biblio.ajouter_document(livre2)

    # Ajout de magazines
    mag1 = Magazine("M001", "Science & Vie", 1250, "Janvier", 2024)
    biblio.ajouter_document(mag1)

    # Ajout de DVDs
    dvd1 = DVD("D001", "Inception", "Christopher Nolan", 148)
    biblio.ajouter_document(dvd1)

    # Inscription d'adhérents
    print("n=== Inscription d'adhérents ===n")

    adherent1 = Adherent("A001", "Dupont", "Marie", "marie.dupont@example.com")
    adherent2 = Adherent("A002", "Martin", "Pierre", "pierre.martin@example.com")

    biblio.inscrire_adherent(adherent1)
    biblio.inscrire_adherent(adherent2)

    # Emprunts
    print("n=== Emprunts ===n")

    biblio.emprunter("A001", "L001")
    biblio.emprunter("A001", "M001")
    biblio.emprunter("A002", "D001")

    # Recherche
    print("n=== Recherche de documents ===n")

    livres_dispo = biblio.rechercher_documents(
        type_doc=TypeDocument.LIVRE,
        disponible_seulement=True
    )
    print(f"Livres disponibles: {len(livres_dispo)}")
    for livre in livres_dispo:
        print(f"  - {livre}")

    # Statistiques
    biblio.afficher_statistiques()

    # Retour
    print("n=== Retours ===n")

    biblio.retourner("A001", "L001")

    # Statistiques finales
    biblio.afficher_statistiques()

    # Documents populaires
    print("n=== Documents populaires ===n")
    for doc, nombre in biblio.documents_populaires(3):
        print(f"{doc.titre}: {nombre} emprunt(s)")

if __name__ == "__main__":
    demo_bibliotheque()

Tests unitaires

"""
tests/test_library.py - Tests pour le système de bibliothèque
"""

import pytest
from datetime import datetime, timedelta
from project.library import (
    Livre,
    Magazine,
    DVD,
    Adherent,
    Bibliotheque,
    Emprunt,
    StatutEmprunt,
    TypeDocument
)

class TestDocument:
    """Tests pour les classes de documents."""

    def test_creation_livre(self):
        """Test de création d'un livre."""
        livre = Livre("L001", "Test", "Auteur", "123456", 200)

        assert livre.id == "L001"
        assert livre.titre == "Test"
        assert livre.auteur == "Auteur"
        assert livre.disponible is True
        assert livre.duree_emprunt() == 21
        assert livre.type_document() == TypeDocument.LIVRE

    def test_creation_magazine(self):
        """Test de création d'un magazine."""
        mag = Magazine("M001", "Test Mag", 1, "Janvier", 2024)

        assert mag.numero == 1
        assert mag.duree_emprunt() == 7
        assert mag.type_document() == TypeDocument.MAGAZINE

    def test_creation_dvd(self):
        """Test de création d'un DVD."""
        dvd = DVD("D001", "Test Film", "Réalisateur", 120)

        assert dvd.realisateur == "Réalisateur"
        assert dvd.duree_emprunt() == 14
        assert dvd.type_document() == TypeDocument.DVD

    def test_emprunt_document(self):
        """Test de l'emprunt d'un document."""
        livre = Livre("L001", "Test", "Auteur", "123456", 200)

        assert livre.emprunter() is True
        assert livre.disponible is False
        assert livre.emprunter() is False  # Déjà emprunté

    def test_retour_document(self):
        """Test du retour d'un document."""
        livre = Livre("L001", "Test", "Auteur", "123456", 200)

        livre.emprunter()
        assert livre.retourner() is True
        assert livre.disponible is True

class TestEmprunt:
    """Tests pour la classe Emprunt."""

    def test_creation_emprunt(self):
        """Test de création d'un emprunt."""
        livre = Livre("L001", "Test", "Auteur", "123456", 200)
        emprunt = Emprunt(livre)

        assert emprunt.document == livre
        assert emprunt.statut == StatutEmprunt.EN_COURS

    def test_calcul_date_retour(self):
        """Test du calcul de la date de retour."""
        livre = Livre("L001", "Test", "Auteur", "123456", 200)
        emprunt = Emprunt(livre)

        expected_date = datetime.now() + timedelta(days=21)
        assert emprunt.date_retour_prevue.date() == expected_date.date()

    def test_statut_en_retard(self):
        """Test du statut en retard."""
        livre = Livre("L001", "Test", "Auteur", "123456", 200)
        emprunt = Emprunt(livre)

        # Simuler un emprunt en retard
        emprunt.date_emprunt = datetime.now() - timedelta(days=30)
        emprunt.date_retour_prevue = datetime.now() - timedelta(days=9)

        assert emprunt.statut == StatutEmprunt.EN_RETARD
        assert emprunt.jours_retard > 0

    def test_calcul_penalite(self):
        """Test du calcul des pénalités."""
        livre = Livre("L001", "Test", "Auteur", "123456", 200)
        emprunt = Emprunt(livre)

        # Simuler 10 jours de retard
        emprunt.date_emprunt = datetime.now() - timedelta(days=31)
        emprunt.date_retour_prevue = datetime.now() - timedelta(days=10)

        penalite = emprunt.calculer_penalite(0.50)
        assert penalite == 10 * 0.50

class TestAdherent:
    """Tests pour la classe Adhérent."""

    @pytest.fixture
    def adherent(self):
        """Fixture pour créer un adhérent."""
        return Adherent("A001", "Dupont", "Marie", "marie@example.com")

    @pytest.fixture
    def livre(self):
        """Fixture pour créer un livre."""
        return Livre("L001", "Test", "Auteur", "123456", 200)

    def test_creation_adherent(self, adherent):
        """Test de création d'un adhérent."""
        assert adherent.id == "A001"
        assert adherent.nom == "Dupont"
        assert adherent.nom_complet == "Marie Dupont"
        assert len(adherent.emprunts_en_cours) == 0

    def test_emprunt_document(self, adherent, livre):
        """Test d'emprunt de document."""
        assert adherent.emprunter_document(livre) is True
        assert len(adherent.emprunts_en_cours) == 1
        assert livre.disponible is False

    def test_limite_emprunts(self, adherent):
        """Test de la limite d'emprunts."""
        # Emprunter 5 livres (limite max)
        for i in range(5):
            livre = Livre(f"L{i}", f"Livre {i}", "Auteur", f"ISBN{i}", 200)
            adherent.emprunter_document(livre)

        # Le 6ème emprunt doit échouer
        livre6 = Livre("L006", "Livre 6", "Auteur", "ISBN6", 200)
        assert adherent.peut_emprunter() is False
        assert adherent.emprunter_document(livre6) is False

    def test_retour_document(self, adherent, livre):
        """Test de retour de document."""
        adherent.emprunter_document(livre)
        assert adherent.retourner_document(livre) is True
        assert len(adherent.emprunts_en_cours) == 0
        assert len(adherent.historique_emprunts) == 1
        assert livre.disponible is True

class TestBibliotheque:
    """Tests pour la classe Bibliothèque."""

    @pytest.fixture
    def bibliotheque(self):
        """Fixture pour créer une bibliothèque."""
        return Bibliotheque("Test Library")

    @pytest.fixture
    def livre(self):
        """Fixture pour créer un livre."""
        return Livre("L001", "Test", "Auteur", "123456", 200)

    @pytest.fixture
    def adherent(self):
        """Fixture pour créer un adhérent."""
        return Adherent("A001", "Dupont", "Marie", "marie@example.com")

    def test_creation_bibliotheque(self, bibliotheque):
        """Test de création de bibliothèque."""
        assert bibliotheque.nom == "Test Library"
        assert len(bibliotheque.documents) == 0
        assert len(bibliotheque.adherents) == 0

    def test_ajouter_document(self, bibliotheque, livre):
        """Test d'ajout de document."""
        bibliotheque.ajouter_document(livre)
        assert len(bibliotheque.documents) == 1
        assert "L001" in bibliotheque.documents

    def test_ajouter_document_doublon(self, bibliotheque, livre):
        """Test d'ajout de document avec ID existant."""
        bibliotheque.ajouter_document(livre)

        with pytest.raises(ValueError):
            bibliotheque.ajouter_document(livre)

    def test_retirer_document(self, bibliotheque, livre):
        """Test de retrait de document."""
        bibliotheque.ajouter_document(livre)
        assert bibliotheque.retirer_document("L001") is True
        assert len(bibliotheque.documents) == 0

    def test_rechercher_documents(self, bibliotheque):
        """Test de recherche de documents."""
        livre1 = Livre("L001", "Python Programming", "Author1", "123", 200)
        livre2 = Livre("L002", "Java Programming", "Author2", "456", 300)
        mag = Magazine("M001", "Python Magazine", 1, "Jan", 2024)

        bibliotheque.ajouter_document(livre1)
        bibliotheque.ajouter_document(livre2)
        bibliotheque.ajouter_document(mag)

        # Recherche par titre
        resultats = bibliotheque.rechercher_documents(titre="Python")
        assert len(resultats) == 2

        # Recherche par type
        livres = bibliotheque.rechercher_documents(type_doc=TypeDocument.LIVRE)
        assert len(livres) == 2

    def test_inscrire_adherent(self, bibliotheque, adherent):
        """Test d'inscription d'adhérent."""
        bibliotheque.inscrire_adherent(adherent)
        assert len(bibliotheque.adherents) == 1
        assert "A001" in bibliotheque.adherents

    def test_emprunt_complet(self, bibliotheque, livre, adherent):
        """Test du processus d'emprunt complet."""
        bibliotheque.ajouter_document(livre)
        bibliotheque.inscrire_adherent(adherent)

        assert bibliotheque.emprunter("A001", "L001") is True
        assert livre.disponible is False
        assert len(adherent.emprunts_en_cours) == 1

    def test_retour_complet(self, bibliotheque, livre, adherent):
        """Test du processus de retour complet."""
        bibliotheque.ajouter_document(livre)
        bibliotheque.inscrire_adherent(adherent)
        bibliotheque.emprunter("A001", "L001")

        assert bibliotheque.retourner("A001", "L001") is True
        assert livre.disponible is True
        assert len(adherent.emprunts_en_cours) == 0

    def test_statistiques(self, bibliotheque, livre, adherent):
        """Test des statistiques."""
        bibliotheque.ajouter_document(livre)
        bibliotheque.inscrire_adherent(adherent)
        bibliotheque.emprunter("A001", "L001")

        stats = bibliotheque.statistiques()

        assert stats["total_documents"] == 1
        assert stats["documents_disponibles"] == 0
        assert stats["documents_empruntes"] == 1
        assert stats["total_adherents"] == 1
        assert stats["emprunts_en_cours"] == 1

if __name__ == "__main__":
    pytest.main([__file__, "-v", "--cov=project.library"])

Conclusion

Dans cet article, vous avez découvert la programmation orientée objet en Python avec:

  • Classes de base: Constructeurs, méthodes d’instance, de classe et statiques
  • Encapsulation: Properties, attributs privés, getters/setters
  • Héritage: Classes abstraites, héritage simple et multiple
  • Polymorphisme: Méthodes abstraites, surcharge, duck typing
  • Composition: Préférer la composition à l’héritage
  • Design patterns: Singleton, Factory, Observer, Decorator, Strategy
  • Projet complet: Système de bibliothèque avec tests
  • Bonnes pratiques POO

  • Respecter SOLID:
  • – Single Responsibility: Une classe = une responsabilité
    – Open/Closed: Ouvert à l’extension, fermé à la modification
    – Liskov Substitution: Les sous-classes doivent être substituables
    – Interface Segregation: Interfaces spécifiques plutôt que génériques
    – Dependency Inversion: Dépendre d’abstractions, pas d’implémentations

  • Préférer la composition à l’héritage quand c’est possible
  • Utiliser les dataclasses pour réduire le boilerplate
  • Documenter avec docstrings (PEP 257)
  • Typer avec type hints (PEP 484)
  • Tester chaque classe avec pytest
  • Prochaines étapes

    Continuez votre apprentissage avec:

  • FastAPI pour créer des APIs REST
  • Django pour des applications web complètes
  • SQLAlchemy pour la gestion de bases de données
  • asyncio pour la programmation asynchrone
  • Pandas pour le Data Science
  • Ressources

  • Python Documentation: https://docs.python.org/3/tutorial/classes.html
  • Real Python OOP: https://realpython.com/python3-object-oriented-programming/
  • Design Patterns in Python: https://refactoring.guru/design-patterns/python

Une remarque, un retour ?

Cet article est vivant - corrections, contre-arguments et retours de production sont les bienvenus. Trois canaux, choisissez celui qui vous convient.

Laisser un commentaire