[OK] CHEATSHEET COMPLET - CONTEXT MANAGERS (GESTIONNAIRES DE CONTEXTE) PYTHON


[OK] 1. CONCEPT DE CONTEXT MANAGER
[OK] Gère automatiquement les ressources (ouverture/fermeture, acquisition/libération)
# Problème sans context manager
file = open('data.txt', 'r')
data = file.read()
file.close()  # On peut oublier de fermer!

# Solution avec context manager (with)
with open('data.txt', 'r') as file:
    data = file.read()
# Le fichier est automatiquement fermé à la sortie du bloc

# Avantages:
# - Pas d'oubli de libération des ressources
# - Gestion automatique même en cas d'erreur
# - Code plus propre et lisible


[OK] 2. WITH STATEMENT - UTILISATION BASIQUE
[OK] Syntaxe: with expression as variable:

# Lecture de fichier
with open('data.txt', 'r') as f:
    content = f.read()
    print(content)
# f est automatiquement fermé ici

# Écriture de fichier
with open('output.txt', 'w') as f:
    f.write('Hello World')
# f est automatiquement fermé et données sauvegardées

# Sans assigner à une variable (si pas besoin)
with open('log.txt', 'a') as _:
    print("Log entry", file=_)


[OK] 3. MULTIPLES CONTEXT MANAGERS
[OK] Gérer plusieurs ressources simultanément

# Méthode 1: Imbrication
with open('input.txt', 'r') as input_file:
    with open('output.txt', 'w') as output_file:
        content = input_file.read()
        output_file.write(content.upper())

# Méthode 2: Sur une ligne (Python 2.7+)
with open('input.txt', 'r') as input_file, open('output.txt', 'w') as output_file:
    content = input_file.read()
    output_file.write(content.upper())

# Méthode 3: Avec parenthèses (plus lisible pour beaucoup de managers)
with (
    open('file1.txt', 'r') as f1,
    open('file2.txt', 'r') as f2,
    open('output.txt', 'w') as out
):
    data1 = f1.read()
    data2 = f2.read()
    out.write(data1 + data2)


[OK] 4. CONTEXT MANAGERS BUILT-IN COURANTS

# 1. Fichiers (open)
with open('data.txt', 'r') as f:
    data = f.read()

# 2. Verrous threading (Lock)
from threading import Lock

lock = Lock()
with lock:
    # Section critique - un seul thread à la fois
    shared_resource += 1

# 3. Changement de répertoire (chdir via contextlib)
import os
from contextlib import chdir

with chdir('/tmp'):
    # Travaille dans /tmp
    print(os.getcwd())
# Revient au répertoire d'origine

# 4. Suppression temporaire d'attribut
from contextlib import suppress

with suppress(FileNotFoundError):
    os.remove('fichier_inexistant.txt')  # Ne lève pas d'erreur

# 5. Gestion de bases de données
import sqlite3

with sqlite3.connect('database.db') as conn:
    cursor = conn.cursor()
    cursor.execute('SELECT * FROM users')
    # Commit automatique si succès, rollback si erreur

# 6. Gestion de connexions réseau
import urllib.request

with urllib.request.urlopen('https://example.com') as response:
    html = response.read()


[OK] 5. GESTION D'ERREURS AVEC CONTEXT MANAGERS
[OK] Les ressources sont libérées même en cas d'exception

# Sans context manager (risqué)
f = open('data.txt', 'r')
try:
    data = f.read()
    process(data)  # Peut lever une exception
finally:
    f.close()  # Doit fermer manuellement

# Avec context manager (automatique)
try:
    with open('data.txt', 'r') as f:
        data = f.read()
        process(data)  # Si exception, f est quand même fermé
except Exception as e:
    print(f"Erreur: {e}")
# f est déjà fermé ici

# Exemple pratique
def safe_read(filename):
    """Lit un fichier en gérant les erreurs"""
    try:
        with open(filename, 'r') as f:
            return f.read()
    except FileNotFoundError:
        print(f"Fichier {filename} introuvable")
        return None
    except PermissionError:
        print(f"Pas de permission pour lire {filename}")
        return None


[OK] 6. CRÉER UN CONTEXT MANAGER - MÉTHODE CLASSE
[OK] Implémenter __enter__ et __exit__

class FileManager:
    """Context manager personnalisé pour fichiers"""
    
    def __init__(self, filename, mode):
        self.filename = filename
        self.mode = mode
        self.file = None
    
    def __enter__(self):
        """Appelé au début du bloc with"""
        print(f"Ouverture de {self.filename}")
        self.file = open(self.filename, self.mode)
        return self.file  # Retourné à 'as variable'
    
    def __exit__(self, exc_type, exc_value, exc_traceback):
        """Appelé à la fin du bloc with (même si exception)"""
        print(f"Fermeture de {self.filename}")
        if self.file:
            self.file.close()
        
        # Paramètres d'exception:
        # exc_type: type de l'exception (None si pas d'erreur)
        # exc_value: instance de l'exception
        # exc_traceback: traceback de l'exception
        
        # Return False: propage l'exception
        # Return True: supprime l'exception
        return False

# Utilisation
with FileManager('test.txt', 'w') as f:
    f.write('Hello World')


# Exemple 2: Timer pour mesurer le temps d'exécution
import time

class Timer:
    """Mesure le temps d'exécution d'un bloc de code"""
    
    def __init__(self, name="Opération"):
        self.name = name
        self.start_time = None
    
    def __enter__(self):
        self.start_time = time.time()
        return self
    
    def __exit__(self, exc_type, exc_value, exc_traceback):
        elapsed = time.time() - self.start_time
        print(f"{self.name} a pris {elapsed:.4f} secondes")
        return False

# Utilisation
with Timer("Calcul complexe"):
    result = sum(range(1000000))


# Exemple 3: Connexion base de données
class DatabaseConnection:
    """Gère une connexion à une base de données"""
    
    def __init__(self, db_name):
        self.db_name = db_name
        self.connection = None
    
    def __enter__(self):
        print(f"Connexion à {self.db_name}")
        # Simuler connexion
        self.connection = f"Connection to {self.db_name}"
        return self.connection
    
    def __exit__(self, exc_type, exc_value, exc_traceback):
        print(f"Déconnexion de {self.db_name}")
        if exc_type is not None:
            print(f"Erreur survenue: {exc_value}")
            # Rollback en cas d'erreur
            print("Rollback des changements")
        else:
            # Commit si tout s'est bien passé
            print("Commit des changements")
        self.connection = None
        return False

# Utilisation
with DatabaseConnection("mydb") as conn:
    print(f"Travaille avec {conn}")


[OK] 7. CRÉER UN CONTEXT MANAGER - MÉTHODE GÉNÉRATEUR
[OK] Plus simple avec @contextmanager de contextlib

from contextlib import contextmanager

@contextmanager
def file_manager(filename, mode):
    """Context manager simple avec générateur"""
    print(f"Ouverture de {filename}")
    f = open(filename, mode)
    try:
        yield f  # Donne le contrôle au bloc with
    finally:
        print(f"Fermeture de {filename}")
        f.close()

# Utilisation identique
with file_manager('test.txt', 'w') as f:
    f.write('Hello')


# Exemple 2: Changement temporaire de répertoire
import os

@contextmanager
def change_directory(path):
    """Change temporairement de répertoire"""
    original_dir = os.getcwd()
    os.chdir(path)
    print(f"Changé vers: {path}")
    try:
        yield
    finally:
        os.chdir(original_dir)
        print(f"Revenu à: {original_dir}")

# Utilisation
with change_directory('/tmp'):
    print(f"Répertoire actuel: {os.getcwd()}")


# Exemple 3: Timer amélioré
@contextmanager
def timer(operation_name):
    """Mesure le temps avec générateur"""
    start = time.time()
    yield
    elapsed = time.time() - start
    print(f"{operation_name}: {elapsed:.4f}s")

with timer("Calcul"):
    sum(range(1000000))


# Exemple 4: Modification temporaire d'attribut
@contextmanager
def temp_attr(obj, attr, value):
    """Modifie temporairement un attribut d'objet"""
    original_value = getattr(obj, attr)
    setattr(obj, attr, value)
    try:
        yield
    finally:
        setattr(obj, attr, original_value)

class Config:
    debug = False

config = Config()
with temp_attr(config, 'debug', True):
    print(config.debug)  # True
print(config.debug)  # False (restauré)


[OK] 8. CONTEXTLIB - MODULE D'UTILITAIRES

from contextlib import (
    contextmanager,
    closing,
    suppress,
    redirect_stdout,
    redirect_stderr,
    nullcontext,
    ExitStack
)

# 1. suppress: Supprime les exceptions spécifiées
with suppress(FileNotFoundError, PermissionError):
    os.remove('fichier_inexistant.txt')  # Pas d'erreur

# 2. redirect_stdout: Redirige la sortie standard
import io

output = io.StringIO()
with redirect_stdout(output):
    print("Ceci va dans output")
    print("Et ceci aussi")
captured = output.getvalue()
print(f"Capturé: {captured}")

# 3. redirect_stderr: Redirige la sortie d'erreur
error_output = io.StringIO()
with redirect_stderr(error_output):
    import sys
    print("Erreur!", file=sys.stderr)

# 4. closing: Ferme automatiquement un objet
from urllib.request import urlopen
with closing(urlopen('https://example.com')) as page:
    data = page.read()

# 5. nullcontext: Context manager "vide" (utile pour conditions)
def process_file(filename, use_context=True):
    cm = open(filename, 'r') if use_context else nullcontext(filename)
    with cm as f:
        if use_context:
            data = f.read()
        else:
            data = f  # filename comme string

# 6. ExitStack: Gérer un nombre dynamique de context managers
with ExitStack() as stack:
    files = [stack.enter_context(open(f'file{i}.txt', 'w')) for i in range(5)]
    # Tous les fichiers sont automatiquement fermés


[OK] 9. EXITSTACK - GESTION DYNAMIQUE
[OK] Gérer plusieurs context managers déterminés à l'exécution

from contextlib import ExitStack

# Exemple 1: Nombre variable de fichiers
def process_multiple_files(filenames):
    with ExitStack() as stack:
        # Ouvrir tous les fichiers
        files = [stack.enter_context(open(fname, 'r')) for fname in filenames]
        
        # Traiter tous les fichiers
        for f in files:
            print(f.read())
        # Tous automatiquement fermés

# Exemple 2: Conditionnellement ajouter des managers
def backup_database(db_name, create_backup=True):
    with ExitStack() as stack:
        # Connexion principale
        conn = stack.enter_context(DatabaseConnection(db_name))
        
        # Backup optionnel
        if create_backup:
            backup_conn = stack.enter_context(DatabaseConnection(f"{db_name}_backup"))
        
        # Faire des opérations...

# Exemple 3: Cleanup dynamique
def process_with_cleanup(items):
    with ExitStack() as stack:
        # Enregistrer des callbacks de nettoyage
        for item in items:
            stack.callback(cleanup_item, item)
        
        # Faire le traitement
        for item in items:
            process_item(item)
        # Tous les cleanups sont appelés

# Exemple 4: Combiner plusieurs context managers
def combined_managers(*managers):
    with ExitStack() as stack:
        results = [stack.enter_context(mgr) for mgr in managers]
        return results


[OK] 10. NESTED CONTEXT MANAGERS AVANCÉS

# Pattern 1: Context managers imbriqués avec dépendances
@contextmanager
def database_transaction(conn):
    """Gère une transaction"""
    print("Début transaction")
    try:
        yield conn
        print("Commit transaction")
        conn.commit()
    except Exception as e:
        print(f"Rollback transaction: {e}")
        conn.rollback()
        raise

# Utilisation
with DatabaseConnection("mydb") as conn:
    with database_transaction(conn):
        # Faire des opérations
        pass


# Pattern 2: Context manager avec configuration
@contextmanager
def configured_operation(config):
    """Opération avec configuration temporaire"""
    # Setup
    old_config = save_config()
    apply_config(config)
    
    try:
        yield
    finally:
        # Teardown
        restore_config(old_config)

# Utilisation
with configured_operation({'debug': True, 'verbose': True}):
    # Code avec config spéciale
    pass


# Pattern 3: Lock hiérarchique
@contextmanager
def hierarchical_lock(*locks):
    """Acquiert plusieurs locks dans l'ordre"""
    acquired = []
    try:
        for lock in locks:
            lock.acquire()
            acquired.append(lock)
        yield
    finally:
        for lock in reversed(acquired):
            lock.release()


[OK] 11. ASYNC CONTEXT MANAGERS
[OK] Context managers pour code asynchrone (async/await)

# Définition avec classe
class AsyncDatabaseConnection:
    """Context manager asynchrone"""
    
    def __init__(self, db_name):
        self.db_name = db_name
        self.connection = None
    
    async def __aenter__(self):
        """Méthode asynchrone d'entrée"""
        print(f"Connexion async à {self.db_name}")
        # Simuler connexion async
        await asyncio.sleep(0.1)
        self.connection = f"Async connection to {self.db_name}"
        return self.connection
    
    async def __aexit__(self, exc_type, exc_value, exc_traceback):
        """Méthode asynchrone de sortie"""
        print(f"Déconnexion async de {self.db_name}")
        await asyncio.sleep(0.1)
        self.connection = None
        return False

# Utilisation
import asyncio

async def main():
    async with AsyncDatabaseConnection("mydb") as conn:
        print(f"Travaille avec {conn}")

asyncio.run(main())


# Définition avec générateur
from contextlib import asynccontextmanager

@asynccontextmanager
async def async_timer(name):
    """Timer asynchrone"""
    start = time.time()
    print(f"Début: {name}")
    try:
        yield
    finally:
        elapsed = time.time() - start
        print(f"{name}: {elapsed:.4f}s")

# Utilisation
async def process():
    async with async_timer("Opération async"):
        await asyncio.sleep(1)

asyncio.run(process())


# Exemple pratique: Pool de connexions async
@asynccontextmanager
async def connection_pool(size=5):
    """Pool de connexions asynchrone"""
    print(f"Création pool de {size} connexions")
    connections = [f"conn_{i}" for i in range(size)]
    try:
        yield connections
    finally:
        print("Fermeture pool")
        for conn in connections:
            print(f"Fermeture {conn}")


[OK] 12. EXEMPLES PRATIQUES COMPLETS

# Exemple 1: Logger avec contexte
@contextmanager
def log_operation(operation_name):
    """Log le début et la fin d'une opération"""
    print(f"[DÉBUT] {operation_name}")
    start_time = time.time()
    try:
        yield
    except Exception as e:
        print(f"[ERREUR] {operation_name}: {e}")
        raise
    else:
        elapsed = time.time() - start_time
        print(f"[FIN] {operation_name} ({elapsed:.2f}s)")

# Utilisation
with log_operation("Traitement des données"):
    # Faire quelque chose
    time.sleep(1)


# Exemple 2: Gestion de fichiers temporaires
import tempfile
import shutil

@contextmanager
def temp_directory():
    """Crée un répertoire temporaire"""
    temp_dir = tempfile.mkdtemp()
    print(f"Répertoire temporaire: {temp_dir}")
    try:
        yield temp_dir
    finally:
        shutil.rmtree(temp_dir)
        print(f"Répertoire supprimé: {temp_dir}")

# Utilisation
with temp_directory() as tmpdir:
    # Travailler avec le répertoire temporaire
    filepath = os.path.join(tmpdir, 'temp_file.txt')
    with open(filepath, 'w') as f:
        f.write('Données temporaires')


# Exemple 3: Mesure de performance avec détails
class PerformanceMonitor:
    """Moniteur de performance détaillé"""
    
    def __init__(self, operation_name):
        self.operation_name = operation_name
        self.start_time = None
        self.start_memory = None
    
    def __enter__(self):
        import tracemalloc
        tracemalloc.start()
        self.start_time = time.time()
        self.start_memory = tracemalloc.get_traced_memory()[0]
        print(f"[START] {self.operation_name}")
        return self
    
    def __exit__(self, exc_type, exc_value, exc_traceback):
        import tracemalloc
        
        elapsed_time = time.time() - self.start_time
        current, peak = tracemalloc.get_traced_memory()
        memory_used = current - self.start_memory
        
        print(f"[END] {self.operation_name}")
        print(f"  Temps: {elapsed_time:.4f}s")
        print(f"  Mémoire utilisée: {memory_used / 1024:.2f} KB")
        print(f"  Pic mémoire: {peak / 1024:.2f} KB")
        
        tracemalloc.stop()
        return False

# Utilisation
with PerformanceMonitor("Traitement massif"):
    data = [i**2 for i in range(100000)]


# Exemple 4: Gestion de transaction avec retry
@contextmanager
def transaction_with_retry(connection, max_retries=3):
    """Transaction avec retry automatique"""
    retries = 0
    while retries < max_retries:
        try:
            connection.begin()
            yield connection
            connection.commit()
            break
        except Exception as e:
            connection.rollback()
            retries += 1
            if retries >= max_retries:
                print(f"Échec après {max_retries} tentatives")
                raise
            print(f"Retry {retries}/{max_retries}: {e}")
            time.sleep(0.1 * retries)


# Exemple 5: Configuration temporaire d'application
@contextmanager
def app_config(**kwargs):
    """Modifie temporairement la configuration de l'app"""
    # Sauvegarder config actuelle
    old_config = {}
    for key, value in kwargs.items():
        if hasattr(Config, key):
            old_config[key] = getattr(Config, key)
            setattr(Config, key, value)
    
    try:
        yield Config
    finally:
        # Restaurer config
        for key, value in old_config.items():
            setattr(Config, key, value)

# Utilisation
with app_config(debug=True, verbose=True, timeout=30):
    # Code avec configuration spéciale
    pass


# Exemple 6: Atomic file writer
@contextmanager
def atomic_write(filepath):
    """Écrit dans un fichier de façon atomique"""
    temp_path = f"{filepath}.tmp"
    try:
        with open(temp_path, 'w') as f:
            yield f
        # Si succès, remplacer le fichier original
        os.replace(temp_path, filepath)
    except Exception:
        # En cas d'erreur, supprimer le fichier temporaire
        if os.path.exists(temp_path):
            os.remove(temp_path)
        raise

# Utilisation
with atomic_write('important_data.txt') as f:
    f.write('Données critiques')
    # Si erreur ici, le fichier original n'est pas modifié


[OK] 13. CONTEXT MANAGERS POUR TESTING

# 1. Mock temporaire
@contextmanager
def mock_function(module, func_name, mock_func):
    """Remplace temporairement une fonction"""
    original = getattr(module, func_name)
    setattr(module, func_name, mock_func)
    try:
        yield
    finally:
        setattr(module, func_name, original)

# 2. Capture de sortie pour tests
@contextmanager
def captured_output():
    """Capture stdout et stderr"""
    import sys
    from io import StringIO
    
    new_out, new_err = StringIO(), StringIO()
    old_out, old_err = sys.stdout, sys.stderr
    try:
        sys.stdout, sys.stderr = new_out, new_err
        yield sys.stdout, sys.stderr
    finally:
        sys.stdout, sys.stderr = old_out, old_err

# Utilisation
with captured_output() as (out, err):
    print("Test output")
    
captured = out.getvalue()
assert "Test output" in captured


# 3. Variables d'environnement temporaires
@contextmanager
def env_vars(**kwargs):
    """Définit temporairement des variables d'environnement"""
    old_environ = dict(os.environ)
    os.environ.update(kwargs)
    try:
        yield
    finally:
        os.environ.clear()
        os.environ.update(old_environ)

# Utilisation
with env_vars(DEBUG='true', API_KEY='test123'):
    # Code avec env vars spéciales
    pass


[OK] 14. PATTERNS AVANCÉS

# Pattern 1: Reentrant context manager
class ReentrantLock:
    """Lock réentrant (peut être acquis plusieurs fois par le même thread)"""
    
    def __init__(self):
        import threading
        self.lock = threading.Lock()
        self.owner = None
        self.count = 0
    
    def __enter__(self):
        import threading
        current_thread = threading.current_thread()
        
        if self.owner == current_thread:
            self.count += 1
        else:
            self.lock.acquire()
            self.owner = current_thread
            self.count = 1
        return self
    
    def __exit__(self, exc_type, exc_value, exc_traceback):
        self.count -= 1
        if self.count == 0:
            self.owner = None
            self.lock.release()
        return False


# Pattern 2: Context manager factory
def create_context_manager(enter_func, exit_func):
    """Crée un context manager à partir de deux fonctions"""
    @contextmanager
    def cm(*args, **kwargs):
        resource = enter_func(*args, **kwargs)
        try:
            yield resource
        finally:
            exit_func(resource)
    return cm

# Utilisation
def connect_db(name):
    print(f"Connexion à {name}")
    return f"Connection({name})"

def disconnect_db(conn):
    print(f"Déconnexion de {conn}")

db_context = create_context_manager(connect_db, disconnect_db)

with db_context("mydb") as conn:
    print(f"Utilise {conn}")


# Pattern 3: Chainable context managers
class ChainableContext:
    """Context manager qui peut être chaîné"""
    
    def __init__(self, name):
        self.name = name
        self.next_context = None
    
    def __enter__(self):
        print(f"Enter {self.name}")
        return self
    
    def __exit__(self, exc_type, exc_value, exc_traceback):
        print(f"Exit {self.name}")
        return False
    
    def chain(self, next_context):
        self.next_context = next_context
        return next_context

# Utilisation
ctx1 = ChainableContext("First")
ctx2 = ctx1.chain(ChainableContext("Second"))
ctx3 = ctx2.chain(ChainableContext("Third"))


[OK] 15. BONNES PRATIQUES

# [OK] 1. Toujours utiliser with pour les ressources
# Préférer
with open('file.txt', 'r') as f:
    data = f.read()

# Au lieu de
f = open('file.txt', 'r')
data = f.read()
f.close()


# [OK] 2. Garder les blocs with courts et focalisés
# Préférer
with open('data.txt', 'r') as f:
    data = f.read()

result = process(data)  # Hors du contexte

# Au lieu de
with open('data.txt', 'r') as f:
    data = f.read()
    result = process(data)  # Fichier ouvert inutilement


# [OK] 3. Gérer les erreurs explicitement dans __exit__
def __exit__(self, exc_type, exc_value, exc_traceback):
    # Nettoyer les ressources
    self.cleanup()
    
    # Logger si erreur
    if exc_type is not None:
        logger.error(f"Erreur: {exc_value}")
    
    # Ne pas supprimer l'exception
    return False


# [OK] 4. Utiliser @contextmanager pour les cas simples
# Plus simple qu'une classe
@contextmanager
def simple_context():
    # Setup
    yield
    # Cleanup


# [OK] 5. Documenter le comportement du context manager
@contextmanager
def my_context():
    """
    Context manager qui fait X.
    
    Yields:
        Resource: La ressource gérée
    
    Example:
        >>> with my_context() as resource:
        ...     use(resource)
    """
    pass


# [OK] 6. Rendre les context managers réutilisables
class ReusableContext:
    """Peut être utilisé plusieurs fois"""
    
    def __enter__(self):
        # Setup à chaque utilisation
        return self
    
    def __exit__(self, exc_type, exc_value, exc_traceback):
        # Cleanup à chaque utilisation
        return False

ctx = ReusableContext()
with ctx:
    pass
with ctx:  # Peut être réutilisé
    pass


# [OK] 7. Ne pas oublier le return False dans __exit__
# Pour propager les exceptions
def __exit__(self, exc_type, exc_value, exc_traceback):
    cleanup()
    return False  # Important!


# [OK] 8. Utiliser suppress pour ignorer des exceptions spécifiques
from contextlib import suppress

with suppress(FileNotFoundError):
    os.remove('maybe_exists.txt')


# [OK] 9. Préférer ExitStack pour un nombre dynamique de ressources
with ExitStack() as stack:
    files = [stack.enter_context(open(f)) for f in filenames]


# [OK] 10. Logger les opérations importantes
@contextmanager
def logged_operation(name):
    logger.info(f"Début: {name}")
    try:
        yield
    except Exception as e:
        logger.error(f"Erreur dans {name}: {e}")
        raise
    else:
        logger.info(f"Succès: {name}")


[OK] 16. RÉSUMÉ DES CONCEPTS CLÉS

"""
UTILISATION BASIQUE:
    with expression as variable:
        # Code qui utilise la ressource
    # Ressource automatiquement libérée

CRÉATION - CLASSE:
    class MyContext:
        def __enter__(self): return resource
        def __exit__(self, exc_type, exc_value, traceback): return False

CRÉATION - GÉNÉRATEUR:
    @contextmanager
    def my_context():
        # Setup
        yield resource
        # Cleanup

CONTEXT MANAGERS BUILT-IN:
    - open(): fichiers
    - Lock(): verrous threading
    - suppress(): ignorer exceptions
    - redirect_stdout/stderr(): rediriger sorties
    - ExitStack(): gérer dynamiquement plusieurs managers

ASYNC CONTEXT MANAGERS:
    - __aenter__ et __aexit__ pour async
    - @asynccontextmanager pour générateurs async

BONNES PRATIQUES:
    - Toujours utiliser with pour les ressources
    - Garder les blocs courts et focalisés
    - Return False dans __exit__ pour propager exceptions
    - Utiliser @contextmanager pour cas simples
    - ExitStack pour nombre dynamique de ressources
    - Logger les opérations importantes
    - Documenter le comportement

AVANTAGES:
    - Pas d'oubli de libération de ressources
    - Gestion automatique des erreurs
    - Code plus propre et lisible
    - Garantit le cleanup même en cas d'exception
    - Évite les fuites de ressources

CAS D'USAGE COURANTS:
    - Gestion de fichiers
    - Connexions base de données
    - Verrous (locks) et synchronisation
    - Transactions
    - Changements temporaires de configuration
    - Mesure de performance
    - Tests et mocking
    - Ressources réseau
"""


[OK] 17. ERREURS COURANTES À ÉVITER

# [X] 1. Oublier de retourner la ressource dans __enter__
class BadContext:
    def __enter__(self):
        self.resource = acquire_resource()
        # Oublie de return self.resource
    
    def __exit__(self, *args):
        release_resource(self.resource)

# [OK] Correct
class GoodContext:
    def __enter__(self):
        self.resource = acquire_resource()
        return self.resource  # Important!
    
    def __exit__(self, *args):
        release_resource(self.resource)
        return False


# [X] 2. Oublier le return dans __exit__
class BadContext2:
    def __exit__(self, exc_type, exc_value, exc_traceback):
        cleanup()
        # Pas de return - équivaut à return None (== False)

# [OK] Explicite est mieux
class GoodContext2:
    def __exit__(self, exc_type, exc_value, exc_traceback):
        cleanup()
        return False  # Propage l'exception


# [X] 3. Supprimer les exceptions par accident
class BadContext3:
    def __exit__(self, exc_type, exc_value, exc_traceback):
        cleanup()
        return True  # Supprime TOUTES les exceptions!

# [OK] Supprimer seulement si intentionnel et spécifique
class GoodContext3:
    def __exit__(self, exc_type, exc_value, exc_traceback):
        cleanup()
        # Supprimer seulement certaines exceptions
        if exc_type is ValueError:
            return True  # Supprime ValueError seulement
        return False  # Propage les autres


# [X] 4. Lever une exception dans __exit__
class BadContext4:
    def __exit__(self, exc_type, exc_value, exc_traceback):
        if some_condition:
            raise RuntimeError("Erreur!")  # Masque l'exception originale!

# [OK] Logger plutôt que lever
class GoodContext4:
    def __exit__(self, exc_type, exc_value, exc_traceback):
        try:
            cleanup()
        except Exception as e:
            logger.error(f"Erreur cleanup: {e}")
            # Ne pas lever, retourner False
        return False


# [X] 5. Oublier le try-finally dans @contextmanager
@contextmanager
def bad_context():
    setup()
    yield
    cleanup()  # Ne sera pas appelé si exception!

# [OK] Toujours utiliser try-finally
@contextmanager
def good_context():
    setup()
    try:
        yield
    finally:
        cleanup()  # Toujours appelé


# [X] 6. Référencer des variables non initialisées
class BadContext5:
    def __enter__(self):
        self.resource = acquire()
        return self.resource
    
    def __exit__(self, *args):
        self.resource.close()  # Si __enter__ a échoué, resource n'existe pas!

# [OK] Vérifier l'existence
class GoodContext5:
    def __init__(self):
        self.resource = None
    
    def __enter__(self):
        self.resource = acquire()
        return self.resource
    
    def __exit__(self, *args):
        if self.resource is not None:
            self.resource.close()
        return False


# [X] 7. Garder la ressource ouverte trop longtemps
# Mauvais
with open('huge_file.txt', 'r') as f:
    data = f.read()
    process_data(data)  # Long traitement avec fichier ouvert
    analyze_data(data)
    save_results(data)

# [OK] Fermer dès que possible
with open('huge_file.txt', 'r') as f:
    data = f.read()

# Fichier fermé, traitement sans ressource bloquée
process_data(data)
analyze_data(data)
save_results(data)


[OK] 18. PATTERNS DE NESTING ET COMPOSITION

# Pattern 1: Context managers composés
class ComposedContext:
    """Combine plusieurs context managers"""
    
    def __init__(self, *contexts):
        self.contexts = contexts
        self.entered = []
    
    def __enter__(self):
        for ctx in self.contexts:
            self.entered.append(ctx.__enter__())
        return self.entered
    
    def __exit__(self, exc_type, exc_value, exc_traceback):
        # Sortir dans l'ordre inverse
        for ctx in reversed(self.contexts):
            try:
                ctx.__exit__(exc_type, exc_value, exc_traceback)
            except Exception as e:
                # Logger mais continuer à fermer les autres
                print(f"Erreur lors de la sortie: {e}")
        return False

# Utilisation
ctx1 = Timer("Operation 1")
ctx2 = Timer("Operation 2")
with ComposedContext(ctx1, ctx2) as (t1, t2):
    # Code
    pass


# Pattern 2: Decorateur avec context manager
def with_context(context_manager):
    """Décorateur qui wrap une fonction dans un context manager"""
    def decorator(func):
        def wrapper(*args, **kwargs):
            with context_manager:
                return func(*args, **kwargs)
        return wrapper
    return decorator

# Utilisation
@with_context(Timer("Database operation"))
def query_database():
    # Code
    pass


# Pattern 3: Context manager conditionnel
@contextmanager
def conditional_context(condition, context_manager):
    """Utilise un context manager seulement si condition vraie"""
    if condition:
        with context_manager as resource:
            yield resource
    else:
        yield None

# Utilisation
with conditional_context(debug_mode, Timer("Debug operation")) as timer:
    # Code
    pass


# Pattern 4: Context manager en tant que décorateur
class ContextDecorator:
    """Context manager qui peut aussi être un décorateur"""
    
    def __enter__(self):
        print("Enter context")
        return self
    
    def __exit__(self, *args):
        print("Exit context")
        return False
    
    def __call__(self, func):
        """Permet d'utiliser comme décorateur"""
        def wrapper(*args, **kwargs):
            with self:
                return func(*args, **kwargs)
        return wrapper

# Utilisation comme context manager
with ContextDecorator():
    print("Dans le contexte")

# Utilisation comme décorateur
@ContextDecorator()
def my_function():
    print("Dans la fonction")


[OK] 19. CONTEXT MANAGERS POUR RESSOURCES SYSTÈME

# 1. Gestion de processus
import subprocess

@contextmanager
def managed_process(command):
    """Gère un processus avec cleanup automatique"""
    process = subprocess.Popen(command, shell=True)
    try:
        yield process
    finally:
        if process.poll() is None:  # Si encore en cours
            process.terminate()
            process.wait(timeout=5)

# Utilisation
with managed_process("python server.py") as proc:
    # Faire des tests
    pass
# Processus automatiquement terminé


# 2. Gestion de signaux
import signal

@contextmanager
def timeout_context(seconds):
    """Limite le temps d'exécution"""
    def timeout_handler(signum, frame):
        raise TimeoutError(f"Opération timeout après {seconds}s")
    
    # Installer le handler
    old_handler = signal.signal(signal.SIGALRM, timeout_handler)
    signal.alarm(seconds)
    
    try:
        yield
    finally:
        # Restaurer
        signal.alarm(0)
        signal.signal(signal.SIGALRM, old_handler)

# Utilisation
try:
    with timeout_context(5):
        # Code qui doit finir en 5 secondes
        long_operation()
except TimeoutError as e:
    print(f"Timeout: {e}")


# 3. Gestion de permissions de fichiers
@contextmanager
def with_permissions(filepath, mode):
    """Modifie temporairement les permissions"""
    original_mode = os.stat(filepath).st_mode
    os.chmod(filepath, mode)
    try:
        yield filepath
    finally:
        os.chmod(filepath, original_mode)

# Utilisation
with with_permissions('secret.txt', 0o600):
    # Fichier accessible seulement par le propriétaire
    with open('secret.txt', 'w') as f:
        f.write('Secret data')


# 4. Gestion de umask
@contextmanager
def with_umask(mask):
    """Modifie temporairement le umask"""
    old_mask = os.umask(mask)
    try:
        yield
    finally:
        os.umask(old_mask)

# Utilisation
with with_umask(0o077):  # Fichiers créés avec permissions strictes
    with open('private.txt', 'w') as f:
        f.write('Private data')


[OK] 20. CONTEXT MANAGERS POUR TESTING AVANCÉ

# 1. Mock de temps
import time as time_module

@contextmanager
def frozen_time(frozen_timestamp):
    """Gèle le temps pour les tests"""
    original_time = time_module.time
    time_module.time = lambda: frozen_timestamp
    try:
        yield
    finally:
        time_module.time = original_time

# Utilisation
with frozen_time(1609459200):  # 2021-01-01 00:00:00
    print(time.time())  # Toujours 1609459200


# 2. Capture de logs
import logging

@contextmanager
def captured_logs(level=logging.INFO):
    """Capture les logs pour vérification"""
    from io import StringIO
    
    log_stream = StringIO()
    handler = logging.StreamHandler(log_stream)
    handler.setLevel(level)
    
    logger = logging.getLogger()
    logger.addHandler(handler)
    old_level = logger.level
    logger.setLevel(level)
    
    try:
        yield log_stream
    finally:
        logger.removeHandler(handler)
        logger.setLevel(old_level)

# Utilisation
with captured_logs() as logs:
    logging.info("Test message")
    assert "Test message" in logs.getvalue()


# 3. Assertions temporaires désactivées
@contextmanager
def no_assertions():
    """Désactive temporairement les assertions"""
    import sys
    original_flags = sys.flags
    sys.flags = type(sys.flags)([1] * len(sys.flags))  # Simule -O
    try:
        yield
    finally:
        sys.flags = original_flags

# Note: Ceci est pour démonstration, pas recommandé en production


# 4. Mocking de modules entiers
@contextmanager
def mock_module(module_name, mock_dict):
    """Remplace temporairement un module"""
    import sys
    original_module = sys.modules.get(module_name)
    
    # Créer un faux module
    from types import ModuleType
    fake_module = ModuleType(module_name)
    for key, value in mock_dict.items():
        setattr(fake_module, key, value)
    
    sys.modules[module_name] = fake_module
    try:
        yield fake_module
    finally:
        if original_module:
            sys.modules[module_name] = original_module
        else:
            del sys.modules[module_name]


[OK] 21. CONTEXT MANAGERS POUR PERFORMANCE

# 1. Cache temporaire
@contextmanager
def cached_computation(cache_key):
    """Active un cache temporaire pour des calculs"""
    cache = {}
    
    def compute_with_cache(func):
        def wrapper(*args):
            key = (cache_key, args)
            if key not in cache:
                cache[key] = func(*args)
            return cache[key]
        return wrapper
    
    yield compute_with_cache
    # Cache automatiquement vidé à la sortie

# Utilisation
with cached_computation("my_operation") as cache:
    @cache
    def expensive_function(n):
        return sum(range(n))
    
    expensive_function(1000000)  # Calculé
    expensive_function(1000000)  # Depuis cache


# 2. Profilage détaillé
import cProfile
import pstats

@contextmanager
def profiled(filename=None):
    """Profile le code et affiche les stats"""
    profiler = cProfile.Profile()
    profiler.enable()
    
    try:
        yield profiler
    finally:
        profiler.disable()
        stats = pstats.Stats(profiler)
        stats.sort_stats('cumulative')
        
        if filename:
            stats.dump_stats(filename)
        else:
            stats.print_stats(10)  # Top 10

# Utilisation
with profiled("profile_output.prof"):
    # Code à profiler
    for i in range(1000):
        sum(range(i))


# 3. Batch operations
@contextmanager
def batch_operations(batch_size=100):
    """Accumule des opérations en batch"""
    operations = []
    
    def add_operation(op):
        operations.append(op)
        if len(operations) >= batch_size:
            execute_batch(operations)
            operations.clear()
    
    yield add_operation
    
    # Exécuter les opérations restantes
    if operations:
        execute_batch(operations)

def execute_batch(ops):
    print(f"Exécution de {len(ops)} opérations")

# Utilisation
with batch_operations(batch_size=50) as add_op:
    for i in range(200):
        add_op(lambda: print(f"Op {i}"))


[OK] 22. DEBUGGING AVEC CONTEXT MANAGERS

# 1. Breakpoint contextuel
@contextmanager
def debug_on_error():
    """Lance le debugger si une erreur survient"""
    try:
        yield
    except Exception as e:
        import pdb
        print(f"Erreur détectée: {e}")
        pdb.post_mortem()
        raise

# Utilisation
with debug_on_error():
    # Code qui peut planter
    risky_operation()


# 2. Verbose execution
@contextmanager
def verbose_execution(prefix=">>>"):
    """Affiche chaque ligne exécutée"""
    import sys
    
    def trace_calls(frame, event, arg):
        if event == 'line':
            filename = frame.f_code.co_filename
            lineno = frame.f_lineno
            print(f"{prefix} {filename}:{lineno}")
        return trace_calls
    
    sys.settrace(trace_calls)
    try:
        yield
    finally:
        sys.settrace(None)

# Utilisation
with verbose_execution():
    a = 1
    b = 2
    c = a + b


# 3. Stack trace capture
@contextmanager
def capture_stack_trace():
    """Capture la stack trace pour debugging"""
    import traceback
    stack_traces = []
    
    original_excepthook = sys.excepthook
    
    def custom_excepthook(exc_type, exc_value, exc_traceback):
        stack_traces.append({
            'type': exc_type,
            'value': exc_value,
            'traceback': traceback.format_tb(exc_traceback)
        })
        original_excepthook(exc_type, exc_value, exc_traceback)
    
    sys.excepthook = custom_excepthook
    try:
        yield stack_traces
    finally:
        sys.excepthook = original_excepthook


[OK] 23. EXEMPLE COMPLET: APPLICATION RÉELLE

# Application complète avec plusieurs context managers

class Application:
    """Application avec context managers pour toutes les ressources"""
    
    def __init__(self, config_file):
        self.config_file = config_file
        self.db = None
        self.cache = None
        self.logger = None
    
    def __enter__(self):
        """Initialise toutes les ressources"""
        print("Démarrage application...")
        
        # Charger configuration
        with open(self.config_file, 'r') as f:
            self.config = json.load(f)
        
        # Initialiser logger
        self.logger = self._setup_logger()
        self.logger.info("Configuration chargée")
        
        # Connecter à la base de données
        self.db = self._connect_database()
        self.logger.info("Base de données connectée")
        
        # Initialiser cache
        self.cache = {}
        self.logger.info("Cache initialisé")
        
        return self
    
    def __exit__(self, exc_type, exc_value, exc_traceback):
        """Nettoie toutes les ressources"""
        print("Arrêt application...")
        
        if exc_type is not None:
            self.logger.error(f"Erreur: {exc_value}")
        
        # Fermer dans l'ordre inverse
        if self.cache is not None:
            self.cache.clear()
            self.logger.info("Cache nettoyé")
        
        if self.db is not None:
            self.db.close()
            self.logger.info("Base de données fermée")
        
        self.logger.info("Application arrêtée")
        
        return False
    
    def _setup_logger(self):
        logger = logging.getLogger("App")
        logger.setLevel(logging.INFO)
        handler = logging.StreamHandler()
        logger.addHandler(handler)
        return logger
    
    def _connect_database(self):
        # Simuler connexion DB
        class FakeDB:
            def close(self):
                pass
        return FakeDB()
    
    @contextmanager
    def transaction(self):
        """Context manager pour transactions"""
        self.logger.info("Début transaction")
        try:
            yield self.db
            self.logger.info("Commit transaction")
        except Exception as e:
            self.logger.error(f"Rollback transaction: {e}")
            raise
    
    @contextmanager
    def timed_operation(self, operation_name):
        """Mesure le temps d'une opération"""
        start = time.time()
        self.logger.info(f"Début: {operation_name}")
        try:
            yield
        finally:
            elapsed = time.time() - start
            self.logger.info(f"Fin: {operation_name} ({elapsed:.2f}s)")

# Utilisation complète
import json
import logging
import time

# Créer un fichier de config
with open('config.json', 'w') as f:
    json.dump({'db': 'mydb', 'debug': True}, f)

# Utiliser l'application
with Application('config.json') as app:
    with app.timed_operation("Traitement principal"):
        with app.transaction():
            # Faire des opérations
            time.sleep(0.5)
            print("Opération réussie")

# Tout est automatiquement nettoyé!


[OK] RÉSUMÉ FINAL

"""
CONTEXT MANAGERS = Gestion automatique des ressources

SYNTAXE:
    with expression as variable:
        # Utiliser la ressource

CRÉATION:
    1. Classe: __enter__ et __exit__
    2. Générateur: @contextmanager avec yield
    3. Async: __aenter__ et __aexit__

OUTILS CONTEXTLIB:
    - contextmanager: créer avec générateur
    - suppress: ignorer exceptions
    - redirect_stdout/stderr: rediriger sorties
    - ExitStack: gérer dynamiquement
    - closing: fermer automatiquement

AVANTAGES:
    [OK] Pas d'oubli de cleanup
    [OK] Gestion automatique des erreurs
    [OK] Code plus propre
    [OK] Évite les fuites de ressources
    [OK] Garanties même en cas d'exception

ERREURS À ÉVITER:
    [X] Oublier return dans __enter__
    [X] Lever exception dans __exit__
    [X] Oublier try-finally avec yield
    [X] Return True par accident
    [X] Garder ressources ouvertes trop longtemps

BONNES PRATIQUES:
    [OK] Toujours utiliser with pour ressources
    [OK] Blocs courts et focalisés
    [OK] Return False dans __exit__
    [OK] @contextmanager pour cas simples
    [OK] ExitStack pour nombre dynamique
    [OK] Logger les opérations importantes
    [OK] Documenter le comportement
    [OK] Tester avec et sans erreurs

USE CASES:
    • Fichiers et I/O
    • Bases de données et transactions
    • Locks et synchronisation
    • Connexions réseau
    • Configuration temporaire
    • Mesure de performance
    • Tests et mocking
    • Gestion de ressources système
"""