Skip to content

Commit

Permalink
Use the configured progress folder to save progress.
Browse files Browse the repository at this point in the history
  • Loading branch information
fniessink committed Oct 8, 2024
1 parent 43f1e1c commit 91f903d
Show file tree
Hide file tree
Showing 4 changed files with 44 additions and 26 deletions.
2 changes: 1 addition & 1 deletion src/toisto/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
3 changes: 1 addition & 2 deletions src/toisto/command/practice.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
31 changes: 18 additions & 13 deletions src/toisto/persistence/progress.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)
34 changes: 24 additions & 10 deletions tests/toisto/persistence/test_progress.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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")
Expand All @@ -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))
Expand All @@ -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))
Expand All @@ -49,33 +63,33 @@ 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")
@patch("json.dump")
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")
@patch("json.dump")
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")
@patch("json.dump")
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)

0 comments on commit 91f903d

Please sign in to comment.