From 1e68a785bad79245f0ff951a696db2ee9b01179f Mon Sep 17 00:00:00 2001 From: jbannon Date: Mon, 6 Jun 2022 22:24:35 +0000 Subject: [PATCH] logger levels working, linter and manual testing need to be tried --- src/ytdl_sub/cli/main_args_parser.py | 10 ++++ src/ytdl_sub/downloaders/downloader.py | 6 ++- src/ytdl_sub/main.py | 2 + src/ytdl_sub/utils/logger.py | 74 ++++++++++++++++++++------ tests/unit/utils/test_logger.py | 35 ++++++++++-- 5 files changed, 104 insertions(+), 23 deletions(-) diff --git a/src/ytdl_sub/cli/main_args_parser.py b/src/ytdl_sub/cli/main_args_parser.py index dff72a68..ed90ddf0 100644 --- a/src/ytdl_sub/cli/main_args_parser.py +++ b/src/ytdl_sub/cli/main_args_parser.py @@ -2,6 +2,8 @@ import argparse ################################################################################################### # GLOBAL PARSER +from ytdl_sub.utils.logger import LoggerLevels + parser = argparse.ArgumentParser( description="ytdl-sub: Automate download and adding metadata with YoutubeDL" ) @@ -13,6 +15,14 @@ parser.add_argument( help="path to the config yaml, uses config.yaml if not provided", default="config.yaml", ) +parser.add_argument( + "--log-level", + metavar="|".join(LoggerLevels.names()), + type=str, + help="level of logs to print to console, defaults to info", + default=LoggerLevels.name_of(LoggerLevels.INFO), + choices=LoggerLevels.names(), +) ################################################################################################### # SUBSCRIPTION PARSER subparsers = parser.add_subparsers(dest="subparser") diff --git a/src/ytdl_sub/downloaders/downloader.py b/src/ytdl_sub/downloaders/downloader.py index 7b388dfe..06dddd08 100644 --- a/src/ytdl_sub/downloaders/downloader.py +++ b/src/ytdl_sub/downloaders/downloader.py @@ -18,6 +18,7 @@ from yt_dlp.utils import RejectedVideoReached from ytdl_sub.config.preset_options import Overrides from ytdl_sub.entries.base_entry import BaseEntry from ytdl_sub.entries.entry import Entry +from ytdl_sub.utils.logger import Logger from ytdl_sub.validators.strict_dict_validator import StrictDictValidator @@ -115,8 +116,9 @@ class Downloader(Generic[DownloaderOptionsT, DownloaderEntryT], ABC): if ytdl_options_overrides is not None: ytdl_options = dict(ytdl_options, **ytdl_options_overrides) - with ytdl.YoutubeDL(ytdl_options) as ytdl_downloader: - yield ytdl_downloader + with Logger.handle_external_logs(name="yt-dlp"): + with ytdl.YoutubeDL(ytdl_options) as ytdl_downloader: + yield ytdl_downloader def extract_info(self, ytdl_options_overrides: Optional[Dict] = None, **kwargs) -> Dict: """ diff --git a/src/ytdl_sub/main.py b/src/ytdl_sub/main.py index 8a6bbcb9..fa83ed84 100644 --- a/src/ytdl_sub/main.py +++ b/src/ytdl_sub/main.py @@ -72,6 +72,8 @@ def _main(): args, extra_args = parser.parse_known_args() config: ConfigFile = ConfigFile.from_file_path(args.config).initialize() + Logger.set_log_level(log_level=args.log_level) + if args.subparser == "sub": _download_subscriptions_from_yaml_files(config=config, args=args) logger.info("Subscription download complete!") diff --git a/src/ytdl_sub/utils/logger.py b/src/ytdl_sub/utils/logger.py index b75fd08d..0874449b 100644 --- a/src/ytdl_sub/utils/logger.py +++ b/src/ytdl_sub/utils/logger.py @@ -4,6 +4,7 @@ import logging import os import sys import tempfile +from typing import List from typing import Optional @@ -44,14 +45,60 @@ class LoggerLevels: case _: return logging.INFO + @classmethod + def name_of(cls, log_level: int): + """ + Parameters + ---------- + log_level + The log level + + Returns + ------- + Name of the log levels + """ + match log_level: + case cls.QUIET: + return "quiet" + case cls.INFO: + return "info" + case cls.VERBOSE: + return "verbose" + case cls.DEBUG: + return "debug" + case _: + raise ValueError("Invalid log level") + + @classmethod + def all(cls) -> List[int]: + """ + Returns + ------- + All log levels + """ + return [cls.QUIET, cls.INFO, cls.VERBOSE, cls.DEBUG] + + @classmethod + def names(cls) -> List[str]: + """ + Returns + ------- + All log level names + """ + return [cls.name_of(log_level=log_level) for log_level in cls.all()] + class Logger: # The level set via CLI arguments - LEVEL = LoggerLevels.DEBUG + _LEVEL = LoggerLevels.DEBUG _DEBUG_LOGGER_FILE = None + @classmethod + def set_log_level(cls, log_level: int): + cls._LEVEL = log_level + @classmethod def _get_formatter(cls) -> logging.Formatter: """ @@ -69,7 +116,7 @@ class Logger: Logger handler """ handler = logging.StreamHandler(sys.stdout) - handler.setLevel(LoggerLevels.to_logging_level(cls.LEVEL)) + handler.setLevel(LoggerLevels.to_logging_level(cls._LEVEL)) handler.setFormatter(cls._get_formatter()) return handler @@ -95,7 +142,7 @@ class Logger: logger_name += f":{name}" logger = logging.Logger(name=logger_name, level=logging.DEBUG) - if stdout and cls.LEVEL >= LoggerLevels.INFO: + if stdout and cls._LEVEL >= LoggerLevels.INFO: logger.addHandler(cls._get_stdout_handler()) if debug_file: logger.addHandler(cls._get_debug_file_handler()) @@ -130,22 +177,15 @@ class Logger: Optional. Name of the logger which is included in the prefix like [ytdl-sub:]. If None, the prefix is just [ytdl-sub] """ - redirect_stream = io.StringIO() - redirect_handler = logging.StreamHandler(redirect_stream) - redirect_handler.setLevel(LoggerLevels.to_logging_level(cls.LEVEL)) - redirect_handler.setFormatter(cls._get_formatter()) + logger = cls._get(name=name, stdout=cls._LEVEL >= LoggerLevels.VERBOSE, debug_file=True) - write_to_stdout = cls.LEVEL >= LoggerLevels.VERBOSE - write_to_debug_file = True + with io.StringIO() as redirect_stream: + with contextlib.redirect_stdout(new_target=redirect_stream): + with contextlib.redirect_stderr(new_target=redirect_stream): + yield - logger = cls._get(name=name, stdout=write_to_stdout, debug_file=write_to_debug_file) - logger.addHandler(redirect_handler) - - with contextlib.redirect_stdout(new_target=redirect_stream): - with contextlib.redirect_stderr(new_target=redirect_stream): - yield - - redirect_stream.flush() + redirect_stream.flush() + logger.info(redirect_stream.getvalue()) @classmethod def cleanup(cls, delete_debug_file: bool = True): diff --git a/tests/unit/utils/test_logger.py b/tests/unit/utils/test_logger.py index 9575c7d6..310fd4d8 100644 --- a/tests/unit/utils/test_logger.py +++ b/tests/unit/utils/test_logger.py @@ -22,7 +22,7 @@ class TestLogger: ], ) def test_logger_info_stdout(self, capsys, log_level, outputs_to_stdout): - Logger.LEVEL = log_level + Logger._LEVEL = log_level logger = Logger.get(name="name_test") logger.info("test") @@ -43,7 +43,7 @@ class TestLogger: ], ) def test_logger_debug_stdout(self, capsys, log_level, outputs_to_stdout): - Logger.LEVEL = log_level + Logger._LEVEL = log_level logger = Logger.get(name="name_test") logger.debug("test") @@ -63,8 +63,8 @@ class TestLogger: LoggerLevels.DEBUG, ], ) - def test_logger_always_outputs_to_debug_file(self, capsys, log_level): - Logger.LEVEL = log_level + def test_logger_always_outputs_to_debug_file(self, log_level): + Logger._LEVEL = log_level logger = Logger.get(name="name_test") logger.info("info test") @@ -74,3 +74,30 @@ class TestLogger: lines = log_file.readlines() assert lines == ["[ytdl-sub:name_test] info test\n", "[ytdl-sub:name_test] debug test\n"] + + @pytest.mark.parametrize( + "log_level, expected_stdout", + [ + (LoggerLevels.QUIET, False), + (LoggerLevels.INFO, False), + (LoggerLevels.VERBOSE, True), + (LoggerLevels.DEBUG, True), + ], + ) + def test_handle_external_logs(self, capsys, log_level, expected_stdout): + Logger._LEVEL = log_level + with Logger.handle_external_logs(name="name_test"): + print("test line 1") + print("test line 2") + + # Ensure it goes to stdout only if it is expected to + captured = capsys.readouterr() + if expected_stdout: + assert captured.out == "[ytdl-sub:name_test] test line 1\ntest line 2\n\n" + else: + assert not captured.out + + # Ensure it always go to the debug file + with open(Logger._DEBUG_LOGGER_FILE.name, "r", encoding="utf-8") as log_file: + lines = log_file.readlines() + assert lines == ["[ytdl-sub:name_test] test line 1\n", "test line 2\n", "\n"]