Retour au cours

projet : application todo en ligne de commande avec fichiers

vous allez écrire une appli CLI pour gérer des tâches (todo). les tâches seront stockées dans un fichier json local.

objectifs

  • concevoir une interface CLI (add/list/done/rm/clear)
  • sérialiser et charger les tâches (json)
  • gérer des erreurs et des codes de sortie

explication détaillée

format

fichier todos.json :

[
  {"id": 1, "title": "acheter du lait", "done": false},
  {"id": 2, "title": "lire un livre", "done": true}
]

commandes

  • add "titre" → ajouter
  • list → afficher toutes les tâches
  • done ID → marquer terminée
  • rm ID → supprimer
  • clear → supprimer toutes

exemples exécutables

# todo.py
import json, sys
from pathlib import Path
import argparse

DB = Path("todos.json")

def load():
    if not DB.exists():
        return []
    return json.loads(DB.read_text(encoding="utf-8"))

def save(items):
    DB.write_text(json.dumps(items, ensure_ascii=False, indent=2), encoding="utf-8")

def next_id(items):
    return (max([it["id"] for it in items]) + 1) if items else 1

def cmd_add(title: str) -> None:
    items = load()
    items.append({"id": next_id(items), "title": title.strip(), "done": False})
    save(items)
    print("ok")

def cmd_list() -> None:
    items = load()
    for it in items:
        flag = "✔" if it["done"] else "·"
        print(f"{it['id']:>3} {flag} {it['title']}")

def cmd_done(i: int) -> int:
    items = load()
    for it in items:
        if it["id"] == i:
            it["done"] = True
            save(items)
            print("done")
            return 0
    print("id introuvable", file=sys.stderr)
    return 1

def cmd_rm(i: int) -> int:
    items = load()
    new = [it for it in items if it["id"] != i]
    if len(new) == len(items):
        print("id introuvable", file=sys.stderr)
        return 1
    save(new)
    print("removed")
    return 0

def cmd_clear() -> None:
    save([])
    print("cleared")

def main(argv=None):
    p = argparse.ArgumentParser(prog="todo", description="todo cli")
    sub = p.add_subparsers(dest="cmd", required=True)

    p_add = sub.add_parser("add")
    p_add.add_argument("title")

    sub.add_parser("list")

    p_done = sub.add_parser("done")
    p_done.add_argument("id", type=int)

    p_rm = sub.add_parser("rm")
    p_rm.add_argument("id", type=int)

    sub.add_parser("clear")

    args = p.parse_args(argv)

    if args.cmd == "add":
        cmd_add(args.title)
    elif args.cmd == "list":
        cmd_list()
    elif args.cmd == "done":
        sys.exit(cmd_done(args.id))
    elif args.cmd == "rm":
        sys.exit(cmd_rm(args.id))
    elif args.cmd == "clear":
        cmd_clear()

if __name__ == "__main__":
    main()

bonnes pratiques

  • valider les entrées (id numérique, title non vide)
  • messages clairs + code de sortie ≠ 0 en cas d’erreur
  • tests automatisés sur le fichier json (utiliser tmp_path)

pièges courants

  • écraser le fichier en cas d’erreur → écrire dans un fichier temporaire puis remplacer (bonus)
  • oublier ensure_ascii=False pour les accents

exercices

  1. ajouter une date de création ISO (created_at).
  2. ajouter un filtre list --done/--todo.
  3. écrire 3 tests pytest (add/list/done) avec tmp_path.