-
Notifications
You must be signed in to change notification settings - Fork 8
Expand file tree
/
Copy pathstate.py
More file actions
96 lines (76 loc) · 2.86 KB
/
Copy pathstate.py
File metadata and controls
96 lines (76 loc) · 2.86 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# pythonlings/core/state.py
from __future__ import annotations
import json
import sys
from collections.abc import Iterable
from dataclasses import dataclass, field
from pathlib import Path
from pythonlings.core.exercise import Exercise
FORMAT_VERSION = 2
@dataclass
class State:
completed: set[str] = field(default_factory=set)
seen_intro: bool = False
last_topic: str | None = None
last_exercise: str | None = None
def mark_done(self, name: str) -> None:
self.completed.add(name)
def record_resume(self, topic: str, exercise: str | None) -> None:
self.seen_intro = True
self.last_topic = topic
self.last_exercise = exercise
def clear_resume(self) -> None:
self.last_topic = None
self.last_exercise = None
def _state_path(root: Path) -> Path:
return root / ".pythonlings" / "state.json"
def load(root: Path) -> State:
path = _state_path(root)
if not path.exists():
return State()
try:
data = json.loads(path.read_text(encoding="utf-8"))
if data.get("format_version") != FORMAT_VERSION:
raise ValueError(
f"unsupported state format_version: {data.get('format_version')}"
)
return State(
completed=set(data.get("completed", [])),
seen_intro=bool(data.get("seen_intro", False)),
last_topic=data.get("last_topic"),
last_exercise=data.get("last_exercise"),
)
except (json.JSONDecodeError, ValueError, KeyError) as e:
backup = path.with_suffix(".json.bak")
path.rename(backup)
print(
f"pythonlings: state file unreadable ({e}); backed up to {backup} "
f"and starting fresh",
file=sys.stderr,
)
return State()
def save(root: Path, state: State) -> None:
path = _state_path(root)
path.parent.mkdir(parents=True, exist_ok=True)
payload = {
"format_version": FORMAT_VERSION,
"completed": sorted(state.completed),
"seen_intro": state.seen_intro,
"last_topic": state.last_topic,
"last_exercise": state.last_exercise,
}
tmp = path.with_suffix(".json.tmp")
tmp.write_text(json.dumps(payload, indent=2), encoding="utf-8")
tmp.replace(path)
def next_pending(exercises: list[Exercise], completed: set[str]) -> str | None:
"""First exercise name in `exercises` not in `completed`, or None."""
for ex in exercises:
if ex.name not in completed:
return ex.name
return None
def completed_count(exercise_names: Iterable[str], completed: set[str]) -> int:
"""How many of `exercise_names` are completed.
Counts only names that exist in the curriculum, so stale `completed`
entries (e.g. exercises renamed or removed) cannot inflate the total.
"""
return sum(1 for name in exercise_names if name in completed)