From 1d87679066f49415a451ee4bb7f4946791629791 Mon Sep 17 00:00:00 2001 From: Ryan Reed Date: Sat, 16 Sep 2023 23:01:19 -0400 Subject: [PATCH 1/6] Add support for 'apply-all' console command --- src/transpose/console.py | 49 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/src/transpose/console.py b/src/transpose/console.py index 0fef4f6..ffd14dd 100644 --- a/src/transpose/console.py +++ b/src/transpose/console.py @@ -21,6 +21,8 @@ def run(args, config_path) -> None: if args.action == "apply": t.apply(args.name, force=args.force) + if args.action == "apply-all": + run_apply_all(t, force=args.force) elif args.action == "restore": t.restore(args.name, force=args.force) elif args.action == "store": @@ -45,6 +47,34 @@ def run(args, config_path) -> None: t.config.save(config_path) +def run_apply_all(t: Transpose, force: bool = False) -> None: + """ + Loop over the entries and recreate the symlinks to the store location + + Useful after restoring the machine + + Args: + t: An instance of Transpose + force: If enabled and path already exists, move the path to '{path}.backup' first + + Returns: + None + """ + results = {} + for entry_name in t.config.entries: + try: + t.apply(entry_name, force) + results[entry_name] = {"status": "success", "error": ""} + except TransposeError as e: + results[entry_name] = {"status": "failure", "error": str(e)} + + for name in sorted(results): + if results[name]["status"] != "success": + print(f"\t{name:<30}: {results[name]['error']}") + else: + print(f"\t{name:<30}: success") + + def parse_arguments(args=None): base_parser = argparse.ArgumentParser(add_help=False) @@ -77,7 +107,24 @@ def parse_arguments(args=None): "name", help="The name of the stored entity to apply", ) - apply_parser.add_argument("--force", dest="force", action="store_true") + apply_parser.add_argument( + "--force", + dest="force", + help="If original path already exists, existing path to .backup and continue", + action="store_true", + ) + + apply_all_parser = subparsers.add_parser( + "apply-all", + help="Recreate the symlink for all entities", + parents=[base_parser], + ) + apply_all_parser.add_argument( + "--force", + dest="force", + help="If original path already exists, existing path to .backup and continue", + action="store_true", + ) restore_parser = subparsers.add_parser( "restore", -- 2.30.1 From 3eada4349af008e25120722138337bc01a48a75a Mon Sep 17 00:00:00 2001 From: Ryan Reed Date: Sat, 16 Sep 2023 23:02:00 -0400 Subject: [PATCH 2/6] Minor comment updates --- src/transpose/transpose.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/transpose/transpose.py b/src/transpose/transpose.py index 9f466d4..e10b1fd 100644 --- a/src/transpose/transpose.py +++ b/src/transpose/transpose.py @@ -152,7 +152,7 @@ class Transpose: Args: name: The name of the entry (must exist) - force: If enabled and path already exists, move the path to '{path}-bak' + force: If enabled and path already exists, move the path to '{path}.backup' first Returns: None @@ -182,7 +182,7 @@ class Transpose: Args: name: The name of the entry (must exist) - force: If enabled and path already exists, move the path to '{path}-bak' + force: If enabled and path already exists, move the path to '{path}.backup' first Returns: None -- 2.30.1 From 62012bfacf7ca5f1770d123129f793c42b77beec Mon Sep 17 00:00:00 2001 From: Ryan Reed Date: Sat, 16 Sep 2023 23:04:21 -0400 Subject: [PATCH 3/6] Move existing source paths to backup even if symlink --- src/transpose/transpose.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/transpose/transpose.py b/src/transpose/transpose.py index e10b1fd..0948644 100644 --- a/src/transpose/transpose.py +++ b/src/transpose/transpose.py @@ -8,7 +8,7 @@ import json from . import version as transpose_version from .exceptions import TransposeError -from .utils import move, remove, symlink +from .utils import move, symlink @dataclass @@ -162,9 +162,7 @@ class Transpose: entry_path = Path(self.config.entries[name].path) if entry_path.exists(): - if entry_path.is_symlink(): - remove(entry_path) - elif force: # Backup the existing path + if force: # Backup the existing path move(entry_path, entry_path.with_suffix(".backup")) else: raise TransposeError( @@ -192,9 +190,7 @@ class Transpose: entry_path = Path(self.config.entries[name].path) if entry_path.exists(): - if entry_path.is_symlink(): - remove(entry_path) - elif force: # Backup the existing path + if force: # Backup the existing path move(entry_path, entry_path.with_suffix(".backup")) else: raise TransposeError( -- 2.30.1 From 7a58647b4b5c5afe32da9c644f0788b07d0bff0a Mon Sep 17 00:00:00 2001 From: Ryan Reed Date: Sat, 16 Sep 2023 23:05:03 -0400 Subject: [PATCH 4/6] Exanduser when moving paths --- src/transpose/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/transpose/utils.py b/src/transpose/utils.py index be7a115..0c344ca 100644 --- a/src/transpose/utils.py +++ b/src/transpose/utils.py @@ -7,7 +7,7 @@ def move(source: Path, destination: Path) -> None: """ Move a file using pathlib """ - shutil.move(source, destination) + shutil.move(source.expanduser(), destination.expanduser()) def remove(path: Path) -> None: -- 2.30.1 From f72ead4fe6e8a88fdcd51247a51a24d1a5cb2e86 Mon Sep 17 00:00:00 2001 From: Ryan Reed Date: Sat, 16 Sep 2023 23:07:42 -0400 Subject: [PATCH 5/6] Tests: Adding tests for console apply-all command --- tests/test_console.py | 61 ++++++++++++++++++++++++++++++++++------- tests/test_transpose.py | 6 ---- tests/utils.py | 22 +++++++++++++-- 3 files changed, 70 insertions(+), 19 deletions(-) diff --git a/tests/test_console.py b/tests/test_console.py index d169b22..3ddc646 100644 --- a/tests/test_console.py +++ b/tests/test_console.py @@ -7,18 +7,31 @@ from transpose.console import parse_arguments, run as run_console from .utils import ( setup_restore, - setup_store, setup_apply, + ENTRY_NAME, + SECOND_ENTRY_NAME, STORE_PATH, TARGET_PATH, + SECOND_TARGET_PATH, TRANSPOSE_CONFIG_PATH, ) -class RunArgs: - name: str = "MyName" +class RunActionArgs: + name: str = ENTRY_NAME + path: str = str(TARGET_PATH) + action: str + force: bool + + def __init__(self, action: str, force: bool = False) -> None: + self.action = action + self.force = force + + +class RunConfigArgs: + name: str = ENTRY_NAME action: str = "config" - forced: bool = False + force: bool = False path: str = str(TARGET_PATH) config_action: str @@ -121,8 +134,36 @@ def test_parse_arguments_restore(): assert args.force is True +@setup_apply() def test_run_apply(): - pass + args = RunActionArgs("apply", False) + + run_console(args, TRANSPOSE_CONFIG_PATH) + + assert TARGET_PATH.is_symlink() + + +@setup_apply() +def test_run_apply_all(capsys): + args = RunActionArgs("apply-all", False) + + run_console(args, TRANSPOSE_CONFIG_PATH) + captured = capsys.readouterr() + + assert f"\t{ENTRY_NAME:<30}: success" in captured.out + assert f"\t{SECOND_ENTRY_NAME:<30}: Entry path already exists" in captured.out + assert TARGET_PATH.is_symlink() + assert SECOND_TARGET_PATH.is_dir() + + args.force = True + run_console(args, TRANSPOSE_CONFIG_PATH) + captured = capsys.readouterr() + + assert f"\t{ENTRY_NAME:<30}: success" in captured.out + assert f"\t{SECOND_ENTRY_NAME:<30}: success" in captured.out + + assert SECOND_TARGET_PATH.is_symlink() + assert SECOND_TARGET_PATH.with_suffix(".backup").is_dir() def test_run_restore(): @@ -135,7 +176,7 @@ def test_run_store(): @setup_restore() def test_run_config_add(): - args = RunArgs("add") + args = RunConfigArgs("add") args.name = "MyName2" run_console(args, TRANSPOSE_CONFIG_PATH) @@ -146,7 +187,7 @@ def test_run_config_add(): @setup_restore() def test_run_config_get(capsys): - args = RunArgs("get") + args = RunConfigArgs("get") run_console(args, TRANSPOSE_CONFIG_PATH) captured = capsys.readouterr() @@ -156,7 +197,7 @@ def test_run_config_get(capsys): @setup_restore() def test_run_config_list(capsys): - args = RunArgs("list") + args = RunConfigArgs("list") run_console(args, TRANSPOSE_CONFIG_PATH) captured = capsys.readouterr() @@ -166,7 +207,7 @@ def test_run_config_list(capsys): @setup_restore() def test_run_config_remove(): - args = RunArgs("remove") + args = RunConfigArgs("remove") run_console(args, TRANSPOSE_CONFIG_PATH) config = TransposeConfig().load(TRANSPOSE_CONFIG_PATH) @@ -176,7 +217,7 @@ def test_run_config_remove(): @setup_restore() def test_run_config_update(): - args = RunArgs("update") + args = RunConfigArgs("update") args.path = "/var/tmp/something" run_console(args, TRANSPOSE_CONFIG_PATH) diff --git a/tests/test_transpose.py b/tests/test_transpose.py index 0ad3784..06cd87f 100644 --- a/tests/test_transpose.py +++ b/tests/test_transpose.py @@ -38,12 +38,6 @@ def test_apply(): with pytest.raises(TransposeError, match="Entry does not exist"): t.apply("BadName") - # Will remove the symlink created above and reapply - # TODO: Check symlink path - t.apply(ENTRY_NAME) - assert TARGET_PATH.is_symlink() - assert ENTRY_STORE_PATH.is_dir() - # Target already exists, force not set TARGET_PATH.unlink() TARGET_PATH.mkdir() diff --git a/tests/utils.py b/tests/utils.py index 7d0417a..8d993c9 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -9,9 +9,11 @@ from transpose import version ENTRY_NAME = "MyName" +SECOND_ENTRY_NAME = "SecondEntry" TESTS_PATH = Path("tests-temp") STORE_PATH = TESTS_PATH.joinpath("store") TARGET_PATH = TESTS_PATH.joinpath("source") +SECOND_TARGET_PATH = TESTS_PATH.joinpath("second_source") SYMLINK_TEST_PATH = TESTS_PATH.joinpath("symlink_test") ENTRY_STORE_PATH = STORE_PATH.joinpath(ENTRY_NAME) @@ -19,8 +21,18 @@ TRANSPOSE_CONFIG_PATH = STORE_PATH.joinpath("transpose.json") TRANSPOSE_CONFIG = { "version": version, - "entries": {ENTRY_NAME: {"name": ENTRY_NAME, "path": str(TARGET_PATH)}}, - "created": "2023-01-21 01:02:03.1234567", + "entries": { + ENTRY_NAME: { + "name": ENTRY_NAME, + "path": str(TARGET_PATH), + "created": "2023-01-21 01:02:03.1234567", + }, + SECOND_ENTRY_NAME: { + "name": SECOND_ENTRY_NAME, + "path": str(SECOND_TARGET_PATH), + "created": "2023-02-23 01:02:03.1234567", + }, + }, } @@ -31,13 +43,17 @@ def setup_apply(): tests-temp/ ├── store/ │ ├── transpose.json - │ └── MyName/ + │ ├── MyName/ + │ └── SecondEntry/ + ├── second_source/ └── symlink_test/ -> source/ """ try: with TemporaryDirectory(str(TESTS_PATH)): STORE_PATH.mkdir(parents=True, exist_ok=True) ENTRY_STORE_PATH.mkdir(parents=True, exist_ok=True) + STORE_PATH.joinpath(SECOND_ENTRY_NAME).mkdir() + SECOND_TARGET_PATH.mkdir() SYMLINK_TEST_PATH.symlink_to(TARGET_PATH.resolve()) with open(str(TRANSPOSE_CONFIG_PATH), "w") as f: -- 2.30.1 From 215f719458c57a1bd71ac8937a109a490078821d Mon Sep 17 00:00:00 2001 From: Ryan Reed Date: Sat, 16 Sep 2023 23:10:22 -0400 Subject: [PATCH 6/6] Updating version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 982cdb3..6fb0def 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "transpose" -version = "2.0.2" +version = "2.1.0" description = "Move and symlink a path to a central location" authors = ["Ryan Reed"] license = "GPLv3" -- 2.30.1