From 43f1e1c7d2f1dbc6cc465fde4e05b754b9fea1cc Mon Sep 17 00:00:00 2001 From: Frank Niessink Date: Sun, 6 Oct 2024 19:48:41 +0200 Subject: [PATCH] Allow for configuring the progress save folder Prepares for #816. --- CHANGELOG.md | 6 ++++ README.md | 15 ++++++++++ src/toisto/command/configure.py | 3 ++ src/toisto/persistence/config.py | 3 ++ src/toisto/ui/cli.py | 27 +++++++++++++++++- tests/toisto/command/test_configure.py | 6 ++++ tests/toisto/ui/test_cli.py | 39 ++++++++++++++++++++++++-- 7 files changed, 96 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d71ceae1..44ad764d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to Toisto will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added + +- Make the folder configurable where Toisto saves progress. Use `toisto configure --progress-folder {folder}` to change the folder. Note that Toisto does not copy or move your existing progress save files for you. + ## 0.26.0 - 2024-10-05 ### Added diff --git a/README.md b/README.md index 1505cbd3..dcf9be83 100644 --- a/README.md +++ b/README.md @@ -207,6 +207,21 @@ my_concepts2.json > [!NOTE] > See the [software documentation](docs/software.md) on how to create extra concept files. +### How to configure the folder where to save progress + +By default, Toisto saves progress to your home folder. To save progress to a different folder, for example a cloud drive, configure the progress folder as follows: + +```console +$ toisto configure --progress-folder /home/user/drive +``` + +When running the previous command, Toisto creates a file `.toisto.cfg` in your home directory if it doesn't exist, adds the `progress` section if it doesn't exist, and adds the folder: + +```ini +[progress] +folder=/home/user/drive +``` + ### How to configure progress updates To prevent having to pass the desired progress update frequency as command-line argument each time you run Toisto, you can save the progress update frequency to Toisto's configuration file: diff --git a/src/toisto/command/configure.py b/src/toisto/command/configure.py index fdaf0ce7..7a1e56e5 100644 --- a/src/toisto/command/configure.py +++ b/src/toisto/command/configure.py @@ -16,6 +16,9 @@ def configure(argument_parser: ArgumentParser, config: ConfigParser, args: Names if language in args and getattr(args, language): ensure_section(config, "languages") config.set("languages", language.split("_")[0], getattr(args, language)) + if "progress_folder" in args: + ensure_section(config, "progress") + config.set("progress", "folder", str(args.progress_folder)) if "progress_update" in args: ensure_section(config, "practice") config.set("practice", "progress_update", str(args.progress_update)) diff --git a/src/toisto/persistence/config.py b/src/toisto/persistence/config.py index 1210b4a7..9ca5f684 100644 --- a/src/toisto/persistence/config.py +++ b/src/toisto/persistence/config.py @@ -50,6 +50,9 @@ class Option: practice=dict( progress_update=Option(Quantifier.INTEGER, ["0", "1", "2", "3", "..."], lambda value: value.isdigit(), "0"), ), + progress=dict( + folder=Option(Quantifier.ANY, default_value=str(home())), + ), identity=dict(uuid=Option(Quantifier.ANY, default_value=str(uuid1()))), files=[], ) diff --git a/src/toisto/ui/cli.py b/src/toisto/ui/cli.py index 2bbb2719..cd15968e 100644 --- a/src/toisto/ui/cli.py +++ b/src/toisto/ui/cli.py @@ -12,6 +12,7 @@ from toisto.metadata import BUILT_IN_LANGUAGES, README_URL, SUMMARY, VERSION, latest_version from toisto.model.language.concept import Concept from toisto.model.language.iana_language_subtag_registry import ALL_LANGUAGES, IANA_LANGUAGE_SUBTAG_REGISTRY_URL +from toisto.persistence.folder import home if TYPE_CHECKING: from argparse import _SubParsersAction @@ -25,6 +26,14 @@ def check_language(language: str) -> str: raise ArgumentTypeError(message) +def check_folder(folder: str) -> str: + """Check that the folder exists.""" + if Path(folder).is_dir(): + return folder + message = f"folder '{folder}' does not exist or is not a folder" + raise ArgumentTypeError(message) + + class CommandBuilder: """Command builder.""" @@ -83,6 +92,17 @@ def add_file_arguments(self, parser: ArgumentParser) -> None: type=Path, ) + def add_progress_folder_argument(self, parser: ArgumentParser) -> None: + """Add the progress folder argument to the command.""" + default = self.config.get("progress", "folder") + parser.add_argument( + "--progress-folder", + metavar="{folder}", + type=check_folder, + default=default, + help=f"folder where to save progress; default: {default}", + ) + def add_progress_update_argument(self, parser: ArgumentParser) -> None: """Add the progress update argument to the command.""" default = self.config.get("practice", "progress_update") @@ -118,9 +138,14 @@ def add_command(self) -> None: "configure options, for example `%(prog)s configure --target fi --source en` to make " "practicing Finnish from English the default" ) - parser = self._add_command("configure", "Configure options and save them in ~/.toisto.cfg.", command_help) + parser = self._add_command( + "configure", + f"Configure options and save them in {home()!s}/.toisto.cfg.", + command_help, + ) self.add_language_arguments(parser) self.add_file_arguments(parser) + self.add_progress_folder_argument(parser) self.add_progress_update_argument(parser) self.add_mp3player_argument(parser) diff --git a/tests/toisto/command/test_configure.py b/tests/toisto/command/test_configure.py index 34cb546a..b09050cc 100644 --- a/tests/toisto/command/test_configure.py +++ b/tests/toisto/command/test_configure.py @@ -59,3 +59,9 @@ def test_change_files(self, write_config: Mock) -> None: config = ConfigParser() configure(self.argument_parser, config, Namespace(file=[Path("/home/user/extra.json")])) self.assert_configured(write_config, config, ("files", "/home/user/extra.json", "")) + + def test_progress_folder(self, write_config: Mock) -> None: + """Test changing the progress folder.""" + config = ConfigParser() + configure(self.argument_parser, config, Namespace(progress_folder="/home/user/toisto")) + self.assert_configured(write_config, config, ("progress", "folder", "/home/user/toisto")) diff --git a/tests/toisto/ui/test_cli.py b/tests/toisto/ui/test_cli.py index 7cca1627..f5f03683 100644 --- a/tests/toisto/ui/test_cli.py +++ b/tests/toisto/ui/test_cli.py @@ -12,13 +12,16 @@ from toisto.model.language.concept import Concept, ConceptId from toisto.model.language.label import Label, Labels from toisto.persistence.config import default_config +from toisto.persistence.folder import home from toisto.ui.cli import create_argument_parser, parse_arguments CONFIGURE_USAGE = ( - "Usage: toisto configure [-h] [-t {language}] [-s {language}] [-f {file}] [-p {frequency}] [-m {mp3player}]" + "Usage: toisto configure [-h] [-t {language}] [-s {language}] [-f {file}] [--progress-folder {folder}] " + "[-p {frequency}]\n" + " [-m {mp3player}]" ) -CONFIGURE_DESCRIPTION = "Configure options and save them in ~/.toisto.cfg." PRACTICE_USAGE = "Usage: toisto practice [-h] -t {language} -s {language} [-f {file}] [-p {frequency}] [{concept} ...]" +CONFIGURE_DESCRIPTION = f"Configure options and save them in {home()!s}/.toisto.cfg." PRACTICE_USAGE_OPTIONAL_TARGET = ( "Usage: toisto practice [-h] [-t {language}] -s {language} [-f {file}] [-p {frequency}] [{concept} ...]" ) @@ -31,6 +34,8 @@ SOURCE_OPTION = """-s, --source {language} source language; languages available in built-in concepts: en, fi, nl""" FILE_OPTION = "-f, --file {file} file with extra concepts to read, can be repeated" +PROGRESS_FOLDER = f"""--progress-folder {{folder}} + folder where to save progress; default: {home()!s}""" PROGRESS_OPTION = """-p, --progress-update {frequency} show a progress update after each {frequency} quizzes; default: %s (0 means never)""" MP3PLAYER_OPTION = """-m, --mp3player {mp3player} @@ -92,12 +97,41 @@ def test_configure_command(self) -> None: command="configure", file=[], mp3player="afplay", + progress_folder=str(home()), progress_update=0, source_language="fi", target_language="nl", ) self.assertEqual(expected_namespace, parse_arguments(self.argument_parser())) + @patch("sys.platform", "darwin") + @patch("sys.argv", ["toisto", "configure", "--progress-folder", "/home/user/toisto"]) + @patch("toisto.ui.cli.Path.is_dir", Mock(return_value=True)) + def test_configure_progress_folder(self) -> None: + """Test that the progress folder can be configured.""" + expected_namespace = Namespace( + command="configure", + file=[], + mp3player="afplay", + progress_folder="/home/user/toisto", + progress_update=0, + source_language=None, + target_language=None, + ) + self.assertEqual(expected_namespace, parse_arguments(self.argument_parser())) + + @patch("sys.platform", "darwin") + @patch("sys.argv", ["toisto", "configure", "--progress-folder", "/home/user/toisto"]) + @patch("toisto.ui.cli.Path.is_dir", Mock(return_value=False)) + @patch("sys.stderr.write") + def test_configure_non_existing_progress_folder(self, sys_stderr_write: Mock) -> None: + """Test that the progress folder is checked for existence.""" + self.assertRaises(SystemExit, parse_arguments, self.argument_parser()) + self.assertIn( + "error: argument --progress-folder: folder '/home/user/toisto' does not exist or is not a folder", + sys_stderr_write.call_args_list[1][0][0], + ) + @patch("sys.platform", "darwin") @patch("sys.argv", ["toisto", "configure", "--help"]) @patch("sys.stdout.write") @@ -114,6 +148,7 @@ def test_configure_help(self, sys_stdout_write: Mock) -> None: {TARGET_OPTION % ""} {SOURCE_OPTION} {FILE_OPTION} + {PROGRESS_FOLDER} {PROGRESS_OPTION % "0"} {MP3PLAYER_OPTION} """,