From 1cb70e4520daa047e2a0f1d43b38720661da6a9a Mon Sep 17 00:00:00 2001 From: Jesse Bannon Date: Wed, 18 Oct 2023 23:33:25 -0700 Subject: [PATCH] [DEV] Refactor entrypoint file (#765) --- src/ytdl_sub/cli/{main.py => entrypoint.py} | 126 ++---------------- src/ytdl_sub/cli/output_summary.py | 96 +++++++++++++ src/ytdl_sub/cli/output_transaction_log.py | 56 ++++++++ src/ytdl_sub/cli/parsers/__init__.py | 0 .../dl.py} | 2 +- .../{main_args_parser.py => parsers/main.py} | 0 src/ytdl_sub/main.py | 6 +- tests/e2e/conftest.py | 2 +- tests/unit/cli/test_download_args_parser.py | 6 +- .../cli/{test_main.py => test_entrypoint.py} | 16 ++- tests/unit/main/test_main.py | 8 +- 11 files changed, 181 insertions(+), 137 deletions(-) rename src/ytdl_sub/cli/{main.py => entrypoint.py} (67%) create mode 100644 src/ytdl_sub/cli/output_summary.py create mode 100644 src/ytdl_sub/cli/output_transaction_log.py create mode 100644 src/ytdl_sub/cli/parsers/__init__.py rename src/ytdl_sub/cli/{download_args_parser.py => parsers/dl.py} (99%) rename src/ytdl_sub/cli/{main_args_parser.py => parsers/main.py} (100%) rename tests/unit/cli/{test_main.py => test_entrypoint.py} (94%) diff --git a/src/ytdl_sub/cli/main.py b/src/ytdl_sub/cli/entrypoint.py similarity index 67% rename from src/ytdl_sub/cli/main.py rename to src/ytdl_sub/cli/entrypoint.py index 4ed5d6a3..f2a4aa0a 100644 --- a/src/ytdl_sub/cli/main.py +++ b/src/ytdl_sub/cli/entrypoint.py @@ -7,12 +7,14 @@ from typing import List from typing import Optional from typing import Tuple -from colorama import Fore from yt_dlp.utils import sanitize_filename -from ytdl_sub.cli.download_args_parser import DownloadArgsParser -from ytdl_sub.cli.main_args_parser import DEFAULT_CONFIG_FILE_NAME -from ytdl_sub.cli.main_args_parser import parser +from ytdl_sub.cli.output_summary import output_summary +from ytdl_sub.cli.output_transaction_log import _maybe_validate_transaction_log_file +from ytdl_sub.cli.output_transaction_log import output_transaction_log +from ytdl_sub.cli.parsers.dl import DownloadArgsParser +from ytdl_sub.cli.parsers.main import DEFAULT_CONFIG_FILE_NAME +from ytdl_sub.cli.parsers.main import parser from ytdl_sub.config.config_file import ConfigFile from ytdl_sub.subscriptions.subscription import Subscription from ytdl_sub.utils.exceptions import ExperimentalFeatureNotEnabled @@ -181,117 +183,6 @@ def _view_url_from_cli( return subscription, subscription.download(dry_run=True) -def _maybe_validate_transaction_log_file(transaction_log_file_path: Optional[str]) -> None: - if transaction_log_file_path: - try: - with open(transaction_log_file_path, "w", encoding="utf-8"): - pass - except Exception as exc: - raise ValidationException( - f"Transaction log file '{transaction_log_file_path}' cannot be written to. " - f"Reason: {str(exc)}" - ) from exc - - -def _output_transaction_log( - transaction_logs: List[Tuple[Subscription, FileHandlerTransactionLog]], - transaction_log_file_path: str, -) -> None: - transaction_log_file_contents = "" - for subscription, transaction_log in transaction_logs: - if transaction_log.is_empty: - transaction_log_contents = f"\nNo files changed for {subscription.name}" - else: - transaction_log_contents = ( - f"Transaction log for {subscription.name}:\n" - f"{transaction_log.to_output_message(subscription.output_directory)}" - ) - - if transaction_log_file_path: - transaction_log_file_contents += transaction_log_contents - else: - logger.info(transaction_log_contents) - - if transaction_log_file_contents: - with open(transaction_log_file_path, "w", encoding="utf-8") as transaction_log_file: - transaction_log_file.write(transaction_log_file_contents) - - -def _green(value: str) -> str: - return Fore.GREEN + value + Fore.RESET - - -def _red(value: str) -> str: - return Fore.RED + value + Fore.RESET - - -def _no_color(value: str) -> str: - return Fore.RESET + value + Fore.RESET - - -def _str_int(value: int) -> str: - if value > 0: - return f"+{value}" - return str(value) - - -def _color_int(value: int) -> str: - str_int = _str_int(value) - if value > 0: - return _green(str_int) - if value < 0: - return _red(str_int) - return _no_color(str_int) - - -def _output_summary(transaction_logs: List[Tuple[Subscription, FileHandlerTransactionLog]]): - summary: List[str] = [] - - # Initialize widths to 0 - width_sub_name: int = 0 - width_num_entries_added: int = 0 - width_num_entries_modified: int = 0 - width_num_entries_removed: int = 0 - width_num_entries: int = 0 - - # Calculate min width needed - for subscription, _ in transaction_logs: - width_sub_name = max(width_sub_name, len(subscription.name)) - width_num_entries_added = max( - width_num_entries_added, len(_color_int(subscription.num_entries_added)) - ) - width_num_entries_modified = max( - width_num_entries_modified, len(_color_int(subscription.num_entries_modified)) - ) - width_num_entries_removed = max( - width_num_entries_removed, len(_color_int(subscription.num_entries_removed * -1)) - ) - width_num_entries = max(width_num_entries, len(str(subscription.num_entries))) - - # Add spacing for aesthetics - width_sub_name += 4 - width_num_entries += 4 - - # Build the summary - for subscription, _ in transaction_logs: - num_entries_added = _color_int(subscription.num_entries_added) - num_entries_modified = _color_int(subscription.num_entries_modified) - num_entries_removed = _color_int(subscription.num_entries_removed * -1) - num_entries = str(subscription.num_entries) - status = _green("success") - - summary.append( - f"{subscription.name:<{width_sub_name}} " - f"{num_entries_added:>{width_num_entries_added}} " - f"{num_entries_modified:>{width_num_entries_modified}} " - f"{num_entries_removed:>{width_num_entries_removed}} " - f"{num_entries:>{width_num_entries}} " - f"{status}" - ) - - return "\n".join(summary) - - def main() -> List[Tuple[Subscription, FileHandlerTransactionLog]]: """ Entrypoint for ytdl-sub, without the error handling @@ -353,12 +244,11 @@ def main() -> List[Tuple[Subscription, FileHandlerTransactionLog]]: raise ValidationException("Must provide one of the commands: sub, dl, view") if not args.suppress_transaction_log: - _output_transaction_log( + output_transaction_log( transaction_logs=transaction_logs, transaction_log_file_path=args.transaction_log, ) - # Hack to always show download summary, even if logs are set to quiet - logger.warning("Download Summary:\n%s", _output_summary(transaction_logs)) + output_summary(transaction_logs) return transaction_logs diff --git a/src/ytdl_sub/cli/output_summary.py b/src/ytdl_sub/cli/output_summary.py new file mode 100644 index 00000000..607f0bf6 --- /dev/null +++ b/src/ytdl_sub/cli/output_summary.py @@ -0,0 +1,96 @@ +from typing import List +from typing import Tuple + +from colorama import Fore + +from ytdl_sub.subscriptions.subscription import Subscription +from ytdl_sub.utils.file_handler import FileHandlerTransactionLog +from ytdl_sub.utils.logger import Logger + +logger = Logger.get() + + +def _green(value: str) -> str: + return Fore.GREEN + value + Fore.RESET + + +def _red(value: str) -> str: + return Fore.RED + value + Fore.RESET + + +def _no_color(value: str) -> str: + return Fore.RESET + value + Fore.RESET + + +def _str_int(value: int) -> str: + if value > 0: + return f"+{value}" + return str(value) + + +def _color_int(value: int) -> str: + str_int = _str_int(value) + if value > 0: + return _green(str_int) + if value < 0: + return _red(str_int) + return _no_color(str_int) + + +def output_summary(transaction_logs: List[Tuple[Subscription, FileHandlerTransactionLog]]) -> str: + """ + Parameters + ---------- + transaction_logs + Transaction logs from downloaded subscriptions + + Returns + ------- + Output summary to print + """ + summary: List[str] = [] + + # Initialize widths to 0 + width_sub_name: int = 0 + width_num_entries_added: int = 0 + width_num_entries_modified: int = 0 + width_num_entries_removed: int = 0 + width_num_entries: int = 0 + + # Calculate min width needed + for subscription, _ in transaction_logs: + width_sub_name = max(width_sub_name, len(subscription.name)) + width_num_entries_added = max( + width_num_entries_added, len(_color_int(subscription.num_entries_added)) + ) + width_num_entries_modified = max( + width_num_entries_modified, len(_color_int(subscription.num_entries_modified)) + ) + width_num_entries_removed = max( + width_num_entries_removed, len(_color_int(subscription.num_entries_removed * -1)) + ) + width_num_entries = max(width_num_entries, len(str(subscription.num_entries))) + + # Add spacing for aesthetics + width_sub_name += 4 + width_num_entries += 4 + + # Build the summary + for subscription, _ in transaction_logs: + num_entries_added = _color_int(subscription.num_entries_added) + num_entries_modified = _color_int(subscription.num_entries_modified) + num_entries_removed = _color_int(subscription.num_entries_removed * -1) + num_entries = str(subscription.num_entries) + status = _green("success") + + summary.append( + f"{subscription.name:<{width_sub_name}} " + f"{num_entries_added:>{width_num_entries_added}} " + f"{num_entries_modified:>{width_num_entries_modified}} " + f"{num_entries_removed:>{width_num_entries_removed}} " + f"{num_entries:>{width_num_entries}} " + f"{status}" + ) + + # Hack to always show download summary, even if logs are set to quiet + logger.warning("Download Summary:\n%s", "\n".join(summary)) diff --git a/src/ytdl_sub/cli/output_transaction_log.py b/src/ytdl_sub/cli/output_transaction_log.py new file mode 100644 index 00000000..6bf5e07b --- /dev/null +++ b/src/ytdl_sub/cli/output_transaction_log.py @@ -0,0 +1,56 @@ +from typing import List +from typing import Optional +from typing import Tuple + +from ytdl_sub.subscriptions.subscription import Subscription +from ytdl_sub.utils.exceptions import ValidationException +from ytdl_sub.utils.file_handler import FileHandlerTransactionLog +from ytdl_sub.utils.logger import Logger + +logger = Logger.get() + + +def _maybe_validate_transaction_log_file(transaction_log_file_path: Optional[str]) -> None: + if transaction_log_file_path: + try: + with open(transaction_log_file_path, "w", encoding="utf-8"): + pass + except Exception as exc: + raise ValidationException( + f"Transaction log file '{transaction_log_file_path}' cannot be written to. " + f"Reason: {str(exc)}" + ) from exc + + +def output_transaction_log( + transaction_logs: List[Tuple[Subscription, FileHandlerTransactionLog]], + transaction_log_file_path: Optional[str], +) -> None: + """ + Maybe print and/or write transaction logs to a file + + Parameters + ---------- + transaction_logs + The transaction logs from downloaded subscriptions + transaction_log_file_path + Optional file path to write to + """ + transaction_log_file_contents = "" + for subscription, transaction_log in transaction_logs: + if transaction_log.is_empty: + transaction_log_contents = f"\nNo files changed for {subscription.name}" + else: + transaction_log_contents = ( + f"Transaction log for {subscription.name}:\n" + f"{transaction_log.to_output_message(subscription.output_directory)}" + ) + + if transaction_log_file_path: + transaction_log_file_contents += transaction_log_contents + else: + logger.info(transaction_log_contents) + + if transaction_log_file_contents: + with open(transaction_log_file_path, "w", encoding="utf-8") as transaction_log_file: + transaction_log_file.write(transaction_log_file_contents) diff --git a/src/ytdl_sub/cli/parsers/__init__.py b/src/ytdl_sub/cli/parsers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/ytdl_sub/cli/download_args_parser.py b/src/ytdl_sub/cli/parsers/dl.py similarity index 99% rename from src/ytdl_sub/cli/download_args_parser.py rename to src/ytdl_sub/cli/parsers/dl.py index bac6bf15..157a156f 100644 --- a/src/ytdl_sub/cli/download_args_parser.py +++ b/src/ytdl_sub/cli/parsers/dl.py @@ -7,7 +7,7 @@ from typing import Tuple from mergedeep import mergedeep -from ytdl_sub.cli.main_args_parser import MainArguments +from ytdl_sub.cli.parsers.main import MainArguments from ytdl_sub.config.config_validator import ConfigOptions from ytdl_sub.utils.exceptions import InvalidDlArguments diff --git a/src/ytdl_sub/cli/main_args_parser.py b/src/ytdl_sub/cli/parsers/main.py similarity index 100% rename from src/ytdl_sub/cli/main_args_parser.py rename to src/ytdl_sub/cli/parsers/main.py diff --git a/src/ytdl_sub/main.py b/src/ytdl_sub/main.py index 6a5fd38c..d522049a 100644 --- a/src/ytdl_sub/main.py +++ b/src/ytdl_sub/main.py @@ -1,6 +1,6 @@ import sys -from ytdl_sub.cli.main_args_parser import parser +from ytdl_sub.cli.parsers.main import parser from ytdl_sub.utils.logger import Logger @@ -11,11 +11,11 @@ def _main(): Logger.set_log_level(log_level_name=args.ytdl_sub_log_level) # pylint: disable=import-outside-toplevel - import ytdl_sub.cli.main + import ytdl_sub.cli.entrypoint # pylint: enable=import-outside-toplevel - ytdl_sub.cli.main.main() + ytdl_sub.cli.entrypoint.main() def main(): diff --git a/tests/e2e/conftest.py b/tests/e2e/conftest.py index 0cb271bb..8cb6b9cd 100644 --- a/tests/e2e/conftest.py +++ b/tests/e2e/conftest.py @@ -7,7 +7,7 @@ from unittest.mock import patch import pytest -from ytdl_sub.cli.main import main +from ytdl_sub.cli.entrypoint import main from ytdl_sub.subscriptions.subscription import Subscription from ytdl_sub.utils.file_handler import FileHandler from ytdl_sub.utils.file_handler import FileHandlerTransactionLog diff --git a/tests/unit/cli/test_download_args_parser.py b/tests/unit/cli/test_download_args_parser.py index bb03d090..8e699ef9 100644 --- a/tests/unit/cli/test_download_args_parser.py +++ b/tests/unit/cli/test_download_args_parser.py @@ -6,9 +6,9 @@ from typing import Optional import pytest -from ytdl_sub.cli.download_args_parser import DownloadArgsParser -from ytdl_sub.cli.main_args_parser import MainArguments -from ytdl_sub.cli.main_args_parser import parser +from ytdl_sub.cli.parsers.dl import DownloadArgsParser +from ytdl_sub.cli.parsers.main import MainArguments +from ytdl_sub.cli.parsers.main import parser from ytdl_sub.config.config_validator import ConfigOptions from ytdl_sub.utils.exceptions import InvalidDlArguments diff --git a/tests/unit/cli/test_main.py b/tests/unit/cli/test_entrypoint.py similarity index 94% rename from tests/unit/cli/test_main.py rename to tests/unit/cli/test_entrypoint.py index b783843d..bc6cdb68 100644 --- a/tests/unit/cli/test_main.py +++ b/tests/unit/cli/test_entrypoint.py @@ -17,10 +17,10 @@ import mergedeep import pytest from conftest import assert_logs -from ytdl_sub.cli.main import _download_subscriptions_from_yaml_files -from ytdl_sub.cli.main import _output_summary -from ytdl_sub.cli.main import logger as main_logger -from ytdl_sub.cli.main import main +from ytdl_sub.cli.entrypoint import _download_subscriptions_from_yaml_files +from ytdl_sub.cli.entrypoint import main +from ytdl_sub.cli.output_summary import output_summary +from ytdl_sub.cli.output_transaction_log import logger as transaction_logger from ytdl_sub.config.config_file import ConfigFile from ytdl_sub.subscriptions.subscription import Subscription from ytdl_sub.utils.exceptions import ExperimentalFeatureNotEnabled @@ -202,7 +202,7 @@ def test_suppress_transaction_log( "--suppress-transaction-log", ] + (["--transaction-log", file_transaction_log] if file_transaction_log else []), - ), patch("ytdl_sub.cli.main._output_transaction_log") as mock_transaction_log: + ), patch("ytdl_sub.cli.output_transaction_log.output_transaction_log") as mock_transaction_log: transaction_logs = main() assert transaction_logs @@ -251,7 +251,9 @@ def test_transaction_log_to_logger( str(music_video_subscription_path), ], ), assert_logs( - logger=main_logger, expected_message="Transaction log for john_smith:\n", log_level="info" + logger=transaction_logger, + expected_message="Transaction log for john_smith:\n", + log_level="info", ): transaction_logs = main() assert transaction_logs @@ -277,7 +279,7 @@ def test_output_summary(): mock_subscriptions.append((sub, FileHandlerTransactionLog())) - _ = _output_summary(transaction_logs=mock_subscriptions) + _ = output_summary(transaction_logs=mock_subscriptions) assert True # Test used for manual inspection - too hard to test ansi color codes diff --git a/tests/unit/main/test_main.py b/tests/unit/main/test_main.py index 230ea3b2..cea92901 100644 --- a/tests/unit/main/test_main.py +++ b/tests/unit/main/test_main.py @@ -8,7 +8,7 @@ import pytest from src.ytdl_sub import __local_version__ from src.ytdl_sub.main import main -from ytdl_sub.cli.main_args_parser import DEFAULT_CONFIG_FILE_NAME +from ytdl_sub.cli.parsers.main import DEFAULT_CONFIG_FILE_NAME from ytdl_sub.config.config_file import ConfigFile from ytdl_sub.utils.exceptions import ValidationException from ytdl_sub.utils.file_handler import FileHandler @@ -93,7 +93,7 @@ def test_args_after_sub_work(mock_sys_exit): sys, "argv", ["ytdl-sub", "-c", "examples/tv_show_config.yaml", "sub", "--log-level", "verbose"], - ), patch("ytdl_sub.cli.main._download_subscriptions_from_yaml_files") as mock_sub: + ), patch("ytdl_sub.cli.entrypoint._download_subscriptions_from_yaml_files") as mock_sub: main() assert mock_sub.call_count == 1 @@ -107,7 +107,7 @@ def test_no_config_works(mock_sys_exit): sys, "argv", ["ytdl-sub", "sub", "--log-level", "verbose"], - ), patch("ytdl_sub.cli.main._download_subscriptions_from_yaml_files") as mock_sub: + ), patch("ytdl_sub.cli.entrypoint._download_subscriptions_from_yaml_files") as mock_sub: main() assert mock_sub.call_count == 1 @@ -128,7 +128,7 @@ def test_uses_default_config_if_present(mock_sys_exit): "argv", ["ytdl-sub", "sub", "--log-level", "verbose"], ), patch( - "ytdl_sub.cli.main._download_subscriptions_from_yaml_files" + "ytdl_sub.cli.entrypoint._download_subscriptions_from_yaml_files" ) as mock_sub, patch.object( ConfigFile, "from_file_path", new=lambda _: ConfigFile(name="test default", value={}) ):