Coverage for pytest_recap/storage.py: 31%
36 statements
« prev ^ index » next coverage.py v7.4.1, created at 2025-05-05 06:24 -0600
« prev ^ index » next coverage.py v7.4.1, created at 2025-05-05 06:24 -0600
1import json
2from pathlib import Path
3from typing import List, Optional
5from filelock import FileLock
8class JSONStorage:
9 """
10 Stores test sessions in a local JSON file, supporting both single-session (dict) and multi-session (list) modes.
11 - Single-session mode (used by the pytest plugin): writes a single session as a dict, overwriting the file.
12 - Multi-session/archive mode: appends sessions to a list, allowing for archival of multiple sessions in one file.
13 - Thread/process-safe via file locking.
15 Args:
16 file_path (Optional[str]): Path to the JSON file. Defaults to ~/.pytest_recap/sessions.json
18 Methods:
19 save_session(session_data: dict, single: bool = False):
20 Appends session_data to the file as a list (default), or overwrites as a dict if single=True.
21 save_single_session(session_data: dict):
22 Overwrites the file with a single session dict (for plugin recap output).
23 load_sessions(lock: bool = True) -> List[dict]:
24 Loads all sessions as a list (returns [] if file is a dict or empty).
26 Example usage:
27 storage = JSONStorage(file_path="sessions.json")
28 storage.save_session(session_dict) # archive mode
29 storage.save_single_session(session_dict) # single recap file
30 """
32 def __init__(self, file_path: Optional[str] = None):
33 self.file_path = Path(file_path) if file_path else Path.home() / ".pytest_recap" / "sessions.json"
34 self.file_path.parent.mkdir(parents=True, exist_ok=True)
35 self.lock_path = f"{self.file_path}.lock"
36 if not self.file_path.exists():
37 # Only lock here if other processes could create at the same time
38 with FileLock(self.lock_path):
39 self._write_json([])
41 def save_session(self, session_data: dict, single: bool = False) -> None:
42 """
43 Save a session. If single=True, write as a dict (overwrite file). If False (default), append to list (archive mode).
44 """
45 with FileLock(self.lock_path):
46 if single:
47 self._write_json(session_data)
48 else:
49 sessions = self.load_sessions(lock=False)
50 sessions.append(session_data)
51 self._write_json(sessions)
53 def save_single_session(self, session_data: dict) -> None:
54 """
55 Save a single session as a dict (overwrite file). For plugin recap output.
56 """
57 self.save_session(session_data, single=True)
59 def load_sessions(self, lock: bool = True) -> List[dict]:
60 if lock:
61 with FileLock(self.lock_path):
62 return self._load_sessions_unlocked()
63 else:
64 return self._load_sessions_unlocked()
66 def _load_sessions_unlocked(self) -> List[dict]:
67 try:
68 with open(self.file_path, "r", encoding="utf-8") as f:
69 data = json.load(f)
70 return data if isinstance(data, list) else data.get("sessions", [])
71 except (FileNotFoundError, json.JSONDecodeError):
72 return []
74 def _write_json(self, data) -> None:
75 with open(self.file_path, "w", encoding="utf-8") as f:
76 json.dump(data, f, indent=2)