From a986da5cef915680f17d0f4bf3e19564691e3a7d Mon Sep 17 00:00:00 2001 From: Tyler D Date: Mon, 11 Dec 2023 13:04:15 -0800 Subject: [PATCH] chore: Improve terminal output (#335) Implementing the following changes: 1. Add debug log level (colored cyan) 2. Make error messages print to stderr instead of stdout 3. include "[seCureLI] [\] " prefix to messages 4. Update default log level from ERROR to WARN 5. Move log level enum to separate class and use more consistently --- secureli/abstractions/echo.py | 70 ++++++++++++++++++--------- secureli/repositories/settings.py | 20 ++------ secureli/utilities/logging.py | 15 ++++++ tests/abstractions/test_typer_echo.py | 9 ++-- 4 files changed, 70 insertions(+), 44 deletions(-) create mode 100644 secureli/utilities/logging.py diff --git a/secureli/abstractions/echo.py b/secureli/abstractions/echo.py index a44e3d80..4140b0b6 100644 --- a/secureli/abstractions/echo.py +++ b/secureli/abstractions/echo.py @@ -1,9 +1,12 @@ from abc import ABC, abstractmethod from enum import Enum -from typing import Optional +from typing import IO, Optional +import sys import typer +from secureli.utilities.logging import EchoLevel + class Color(str, Enum): BLACK = "black" @@ -25,18 +28,31 @@ class EchoAbstraction(ABC): """ def __init__(self, level: str): - self.print_enabled = level != "OFF" - self.info_enabled = level in ["DEBUG", "INFO"] - self.warn_enabled = level in ["DEBUG", "INFO", "WARN"] - self.error_enabled = level in ["DEBUG", "INFO", "WARN", "ERROR"] + self.print_enabled = level != EchoLevel.off + self.debug_enabled = level == EchoLevel.debug + self.info_enabled = level in [EchoLevel.debug, EchoLevel.info] + self.warn_enabled = level in [EchoLevel.debug, EchoLevel.info, EchoLevel.warn] + self.error_enabled = level in [ + EchoLevel.debug, + EchoLevel.info, + EchoLevel.warn, + EchoLevel.error, + ] @abstractmethod - def _echo(self, message: str, color: Optional[Color] = None, bold: bool = False): + def _echo( + self, + message: str, + color: Optional[Color] = None, + bold: bool = False, + fd: IO = sys.stdout, + ): """ Print the provided message to the terminal with the associated color and weight :param message: The message to print :param color: The color to use :param bold: Whether to make this message appear bold or not + :param fd: A file descriptor, defaults to stdout """ pass @@ -59,10 +75,16 @@ def print(self, message: str, color: Optional[Color] = None, bold: bool = False) :param color: The color to use :param bold: Whether to make this message appear bold or not """ - if not self.print_enabled: - return + if self.print_enabled: + self._echo(message, color, bold) - self._echo(message, color, bold) + def debug(self, message: str) -> None: + """ + Prints the message to the terminal in light blue and bold + :param message: The debug message to print + """ + if self.debug_enabled: + self._echo(f"[DEBUG] {message}", color=Color.CYAN, bold=True) def info(self, message: str, color: Optional[Color] = None, bold: bool = False): """ @@ -71,28 +93,24 @@ def info(self, message: str, color: Optional[Color] = None, bold: bool = False): :param color: The color to use :param bold: Whether to make this message appear bold or not """ - if not self.info_enabled: - return - - self._echo(message, color, bold) + if self.info_enabled: + self._echo(f"[INFO] {message}", color, bold) def error(self, message: str): """ Prints the provided message to the terminal in red and bold :param message: The error message to print """ - if not self.error_enabled: - return - self._echo(message, color=Color.RED, bold=True) + if self.error_enabled: + self._echo(f"[ERROR] {message}", color=Color.RED, bold=True, fd=sys.stderr) def warning(self, message: str): """ Prints the provided message to the terminal in red and bold :param message: The error message to print """ - if not self.warn_enabled: - return - self._echo(message, color=Color.YELLOW, bold=False) + if self.warn_enabled: + self._echo(f"[WARN] {message}", color=Color.YELLOW, bold=False) class TyperEcho(EchoAbstraction): @@ -100,13 +118,19 @@ class TyperEcho(EchoAbstraction): Encapsulates the Typer dependency for printing purposes, allows us to render stuff to the screen. """ - def __init__(self, level: str): + def __init__(self, level: str) -> None: super().__init__(level) - def _echo(self, message: str, color: Optional[Color] = None, bold: bool = False): + def _echo( + self, + message: str, + color: Optional[Color] = None, + bold: bool = False, + fd: IO = sys.stdout, + ) -> None: fg = color.value if color else None - message = typer.style(message, fg=fg, bold=bold) - typer.echo(message) + message = typer.style(f"[seCureLI] {message}", fg=fg, bold=bold) + typer.echo(message, file=fd) def confirm(self, message: str, default_response: Optional[bool] = False) -> bool: return typer.confirm(message, default=default_response, show_default=True) diff --git a/secureli/repositories/settings.py b/secureli/repositories/settings.py index 1b326aae..8aeee9d6 100644 --- a/secureli/repositories/settings.py +++ b/secureli/repositories/settings.py @@ -1,9 +1,10 @@ -from enum import Enum from pathlib import Path from typing import Optional +from pydantic import BaseModel, BaseSettings, Field +from secureli.utilities.logging import EchoLevel import yaml -from pydantic import BaseModel, BaseSettings, Field + default_ignored_extensions = [ # Images @@ -68,25 +69,12 @@ class RepoFilesSettings(BaseSettings): exclude_file_patterns: list[str] = Field(default=[]) -class EchoLevel(str, Enum): - debug = "DEBUG" - info = "INFO" - warn = "WARN" - error = "ERROR" - - def __str__(self) -> str: - return self.value - - def __repr__(self) -> str: - return self.__str__() - - class EchoSettings(BaseSettings): """ Settings that affect how seCureLI provides information to the user. """ - level: EchoLevel = Field(default=EchoLevel.error) + level: EchoLevel = Field(default=EchoLevel.warn) class LanguageSupportSettings(BaseSettings): diff --git a/secureli/utilities/logging.py b/secureli/utilities/logging.py new file mode 100644 index 00000000..49642cae --- /dev/null +++ b/secureli/utilities/logging.py @@ -0,0 +1,15 @@ +from enum import Enum + + +class EchoLevel(str, Enum): + debug = "DEBUG" + info = "INFO" + warn = "WARN" + error = "ERROR" + off = "OFF" + + def __str__(self) -> str: + return self.value + + def __repr__(self) -> str: + return self.__str__() diff --git a/tests/abstractions/test_typer_echo.py b/tests/abstractions/test_typer_echo.py index 9420eba3..67320f97 100644 --- a/tests/abstractions/test_typer_echo.py +++ b/tests/abstractions/test_typer_echo.py @@ -1,9 +1,8 @@ +from pytest_mock import MockerFixture +from secureli.abstractions.echo import TyperEcho, Color from unittest.mock import MagicMock, ANY import pytest -from pytest_mock import MockerFixture - -from secureli.abstractions.echo import TyperEcho, Color @pytest.fixture() @@ -55,7 +54,7 @@ def test_that_typer_echo_stylizes_message( typer_echo.info(mock_echo_text) mock_typer_style.assert_called_once() - mock_typer_echo.assert_called_once_with(mock_echo_text) + mock_typer_echo.assert_called_once_with(mock_echo_text, file=ANY) def test_that_typer_echo_stylizes_message_when_printing( @@ -67,7 +66,7 @@ def test_that_typer_echo_stylizes_message_when_printing( typer_echo.print(mock_echo_text) mock_typer_style.assert_called_once() - mock_typer_echo.assert_called_once_with(mock_echo_text) + mock_typer_echo.assert_called_once_with(mock_echo_text, file=ANY) def test_that_typer_echo_does_not_even_print_when_off(