diff --git a/src/toisto/app.py b/src/toisto/app.py index 321ec56e..8567469f 100644 --- a/src/toisto/app.py +++ b/src/toisto/app.py @@ -41,7 +41,7 @@ def progress(self) -> Progress: concepts = self.build_in_concepts | self.loader.load_concepts(*self.args.file) filtered_concepts = filter_concepts(concepts, self.args.concepts, target_language, self.argument_parser) quizzes = create_quizzes(self.language_pair, *filtered_concepts) - return load_progress(target_language, quizzes, self.argument_parser, self.config["identity"]["uuid"]) + return load_progress(target_language, quizzes, self.argument_parser, self.config) def main() -> None: diff --git a/src/toisto/command/practice.py b/src/toisto/command/practice.py index 46a03e59..1453b564 100644 --- a/src/toisto/command/practice.py +++ b/src/toisto/command/practice.py @@ -60,11 +60,10 @@ def practice( """Practice a language.""" progress_update = ProgressUpdate(progress, progress_update_frequency) speech = Speech(config) - uuid = config["identity"]["uuid"] try: while quiz := progress.next_quiz(): do_quiz(write_output, language_pair, quiz, progress, speech) - save_progress(progress, uuid) + save_progress(progress, config) with dramatic.output.at_speed(120): # Turn off highlighting to work around https://github.com/treyhunner/dramatic/issues/8: write_output(progress_update(), end="", highlight=False) diff --git a/src/toisto/persistence/progress.py b/src/toisto/persistence/progress.py index 2cd5d3e7..636f6563 100644 --- a/src/toisto/persistence/progress.py +++ b/src/toisto/persistence/progress.py @@ -1,27 +1,31 @@ """Store and load progress data.""" from argparse import ArgumentParser +from configparser import ConfigParser from pathlib import Path -from ..metadata import NAME -from ..model.language import Language -from ..model.quiz.progress import Progress -from ..model.quiz.quiz import Quizzes -from .folder import home +from toisto.metadata import NAME +from toisto.model.language import Language +from toisto.model.quiz.progress import Progress +from toisto.model.quiz.quiz import Quizzes + from .json_file import dump_json, load_json -def get_progress_filepath(target_language: Language, uuid: str = "") -> Path: +def get_progress_filepath(target_language: Language, folder: Path, uuid: str = "") -> Path: """Return the filename of the progress file for the specified target language.""" - return home() / f".{NAME.lower()}{f'-{uuid}' if uuid else ''}-progress-{target_language}.json" + return folder / f".{NAME.lower()}{f'-{uuid}' if uuid else ''}-progress-{target_language}.json" -def load_progress(target_language: Language, quizzes: Quizzes, argument_parser: ArgumentParser, uuid: str) -> Progress: +def load_progress( + target_language: Language, quizzes: Quizzes, argument_parser: ArgumentParser, config: ConfigParser +) -> Progress: """Load the progress from the user's home folder.""" - progress_filepath = get_progress_filepath(target_language, uuid) + folder = Path(config["progress"]["folder"]) + progress_filepath = get_progress_filepath(target_language, folder, config["identity"]["uuid"]) if not progress_filepath.exists(): # Read the progress file without UUID as saved by Toisto <= v0.26.0: - progress_filepath = get_progress_filepath(target_language) + progress_filepath = get_progress_filepath(target_language, folder) try: progress_dict = load_json(progress_filepath, default={}) except Exception as reason: # noqa: BLE001 @@ -35,9 +39,10 @@ def load_progress(target_language: Language, quizzes: Quizzes, argument_parser: return Progress(progress_dict, target_language, quizzes) -def save_progress(progress: Progress, uuid: str) -> None: +def save_progress(progress: Progress, config: ConfigParser) -> None: """Save the progress to the user's home folder.""" - progress_filepath = get_progress_filepath(progress.target_language, uuid) + folder = Path(config["progress"]["folder"]) + progress_filepath = get_progress_filepath(progress.target_language, folder, config["identity"]["uuid"]) dump_json(progress_filepath, progress.as_dict()) # Remove the progress file without UUID as saved by Toisto <= v0.26.0 if it still exists: - get_progress_filepath(progress.target_language).unlink(missing_ok=True) + get_progress_filepath(progress.target_language, folder).unlink(missing_ok=True) diff --git a/tests/toisto/persistence/test_progress.py b/tests/toisto/persistence/test_progress.py index 6e332e09..b72288ea 100644 --- a/tests/toisto/persistence/test_progress.py +++ b/tests/toisto/persistence/test_progress.py @@ -1,6 +1,8 @@ """Unit tests for the persistence module.""" from argparse import ArgumentParser +from configparser import ConfigParser +from pathlib import Path from unittest.mock import MagicMock, Mock, patch from toisto.model.language import FI, Language @@ -12,14 +14,26 @@ from ...base import ToistoTestCase -class LoadProgressTest(ToistoTestCase): +class ProgressTestCase(ToistoTestCase): + """Base class for unit tests that test loading and saving progress.""" + + def setUp(self): + """Set up test fixtures.""" + self.config = ConfigParser() + self.config.add_section("identity") + self.config["identity"]["uuid"] = "uuid" + self.config.add_section("progress") + self.config["progress"]["folder"] = "/home/user" + + +class LoadProgressTest(ProgressTestCase): """Unit tests for loading progress.""" @patch("pathlib.Path.exists", Mock(return_value=False)) @patch("pathlib.Path.open", MagicMock()) def test_load_non_existing_progress(self) -> None: """Test that the default value is returned when the progress cannot be loaded.""" - self.assertEqual({}, load_progress(FI, Quizzes(), ArgumentParser(), "uuid").as_dict()) + self.assertEqual({}, load_progress(FI, Quizzes(), ArgumentParser(), self.config).as_dict()) @patch("pathlib.Path.exists", Mock(return_value=True)) @patch("sys.stderr.write") @@ -28,8 +42,8 @@ def test_load_invalid_progress(self, path_open: Mock, stderr_write: Mock) -> Non """Test that the the program exists if the progress cannot be loaded.""" path_open.return_value.__enter__.return_value.read.return_value = "" language = Language("en") - self.assertRaises(SystemExit, load_progress, language, Quizzes(), ArgumentParser(), "uuid") - progress_file = get_progress_filepath(language, "uuid") + self.assertRaises(SystemExit, load_progress, language, Quizzes(), ArgumentParser(), self.config) + progress_file = get_progress_filepath(language, Path(self.config["progress"]["folder"]), "uuid") self.assertIn(f"cannot parse the progress information in {progress_file}", stderr_write.call_args_list[1][0][0]) @patch("pathlib.Path.exists", Mock(return_value=True)) @@ -39,7 +53,7 @@ def test_load_existing_progress(self, path_open: Mock) -> None: path_open.return_value.__enter__.return_value.read.return_value = '{"quiz:read": {}}' self.assertEqual( {"quiz:read": Retention().as_dict()}, - load_progress(Language("nl"), Quizzes(), ArgumentParser(), "uuid").as_dict(), + load_progress(Language("nl"), Quizzes(), ArgumentParser(), self.config).as_dict(), ) @patch("pathlib.Path.exists", Mock(return_value=True)) @@ -49,11 +63,11 @@ def test_invalid_actions_are_ignored(self, path_open: Mock) -> None: path_open.return_value.__enter__.return_value.read.return_value = '{"quiz:read": {}, "quiz:invalid action": {}}' self.assertEqual( {"quiz:read": Retention().as_dict()}, - load_progress(Language("nl"), Quizzes(), ArgumentParser(), "uuid").as_dict(), + load_progress(Language("nl"), Quizzes(), ArgumentParser(), self.config).as_dict(), ) -class SaveProgressTest(ToistoTestCase): +class SaveProgressTest(ProgressTestCase): """Unit tests for saving progress.""" @patch("pathlib.Path.open") @@ -61,7 +75,7 @@ class SaveProgressTest(ToistoTestCase): def test_save_empty_progress(self, dump: Mock, path_open: Mock) -> None: """Test that the progress can be saved.""" path_open.return_value.__enter__.return_value = json_file = MagicMock() - save_progress(Progress({}, Language("fi"), Quizzes()), "uuid") + save_progress(Progress({}, Language("fi"), Quizzes()), self.config) dump.assert_called_once_with({}, json_file) @patch("pathlib.Path.open") @@ -69,7 +83,7 @@ def test_save_empty_progress(self, dump: Mock, path_open: Mock) -> None: def test_save_incorrect_only_progress(self, dump: Mock, path_open: Mock) -> None: """Test that the progress can be saved.""" path_open.return_value.__enter__.return_value = json_file = MagicMock() - save_progress(Progress({"quiz:read": {}}, Language("fi"), Quizzes()), "uuid") + save_progress(Progress({"quiz:read": {}}, Language("fi"), Quizzes()), self.config) dump.assert_called_once_with({"quiz:read": {}}, json_file) @patch("pathlib.Path.open") @@ -77,5 +91,5 @@ def test_save_incorrect_only_progress(self, dump: Mock, path_open: Mock) -> None def test_save_progress(self, dump: Mock, path_open: Mock) -> None: """Test that the progress can be saved.""" path_open.return_value.__enter__.return_value = json_file = MagicMock() - save_progress(Progress({"quiz:read": dict(skip_until="3000-01-01")}, Language("fi"), Quizzes()), "uuid") + save_progress(Progress({"quiz:read": dict(skip_until="3000-01-01")}, Language("fi"), Quizzes()), self.config) dump.assert_called_once_with({"quiz:read": dict(skip_until="3000-01-01T00:00:00")}, json_file)