From 4a57dbb70a9bfa9272c4034c1798895b32dd7248 Mon Sep 17 00:00:00 2001 From: shouzy <82171453+realshouzy@users.noreply.github.com> Date: Mon, 10 Jun 2024 19:30:49 +0200 Subject: [PATCH] Improve logging - Use logging.config.dictConfi - Make error message more similar to pip --- pip_manage/_logging.py | 80 ++++++++++++++++++++++++++++++---------- pip_manage/pip_purge.py | 27 ++++++++++---- pip_manage/pip_review.py | 25 +++++++++---- pyproject.toml | 1 + 4 files changed, 97 insertions(+), 36 deletions(-) diff --git a/pip_manage/_logging.py b/pip_manage/_logging.py index 3e580617..c57cdcb3 100644 --- a/pip_manage/_logging.py +++ b/pip_manage/_logging.py @@ -1,10 +1,11 @@ from __future__ import annotations -__all__: list[str] = ["setup_logging", "set_logging_level"] +__all__: list[str] = ["setup_logging"] import logging +import logging.config import sys -from typing import TextIO +from typing import ClassVar, Literal if sys.version_info >= (3, 12): # pragma: >=3.12 cover from typing import override @@ -18,24 +19,63 @@ def filter(self, record: logging.LogRecord) -> bool: return record.levelno <= logging.INFO -def setup_logging(logger_name: str) -> logging.Logger: - logger: logging.Logger = logging.getLogger(logger_name) +class _ColoredFormatter(logging.Formatter): + COLORS: ClassVar[dict[str, str]] = { + "DEBUG": "\033[0;37m", + "INFO": "\033[0;32m", + "WARNING": "\033[0;33m", + "ERROR": "\033[0;31m", + "CRITICAL": "\033[1;31m", + } + RESET: ClassVar[Literal["\033[0m"]] = "\033[0m" - stdout_handler: logging.StreamHandler[TextIO] = logging.StreamHandler(sys.stdout) - stdout_handler.set_name("stdout") - stdout_handler.addFilter(_StdOutFilter()) - stdout_handler.setFormatter(logging.Formatter("%(message)s")) - stdout_handler.setLevel(logging.DEBUG) - - stderr_handler: logging.StreamHandler[TextIO] = logging.StreamHandler(sys.stderr) - stderr_handler.set_name("stderr") - stderr_handler.setFormatter(logging.Formatter("%(levelname)s: %(message)s")) - stderr_handler.setLevel(logging.WARNING) - - logger.addHandler(stderr_handler) - logger.addHandler(stdout_handler) - return logger + @override + def format(self, record: logging.LogRecord) -> str: + log_color: str = self.COLORS.get(record.levelname, self.RESET) + record.msg = f"{log_color}{record.levelname}: {record.msg}{self.RESET}" + return super().format(record) -def set_logging_level(logger: logging.Logger, *, verbose: bool) -> None: - logger.setLevel(logging.DEBUG if verbose else logging.INFO) +def setup_logging(*, verbose: bool) -> None: + level: Literal["DEBUG", "INFO"] = "DEBUG" if verbose else "INFO" + logging.config.dictConfig( + { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "simple": { + "format": "%(message)s", + }, + "colored": { + "()": _ColoredFormatter, + }, + }, + "filters": { + "StdOutFilter": { + "()": _StdOutFilter, + }, + }, + "handlers": { + "stdout": { + "class": "logging.StreamHandler", + "stream": "ext://sys.stdout", + "formatter": "simple", + "filters": ["StdOutFilter"], + "level": "DEBUG", + }, + "stderr": { + "class": "logging.StreamHandler", + "stream": "ext://sys.stderr", + "formatter": "colored", + "level": "WARNING", + }, + }, + "loggers": { + "root": { + "level": level, + "handlers": ["stdout", "stderr"], + "propagate": True, + }, + }, + }, + ) diff --git a/pip_manage/pip_purge.py b/pip_manage/pip_purge.py index 1f0211fd..a54872b5 100755 --- a/pip_manage/pip_purge.py +++ b/pip_manage/pip_purge.py @@ -6,10 +6,11 @@ import argparse import importlib.metadata +import logging from pathlib import Path from typing import TYPE_CHECKING, Final, NamedTuple -from pip_manage._logging import set_logging_level, setup_logging +from pip_manage._logging import setup_logging from pip_manage._pip_interface import ( PIP_CMD, UNINSTALL_ONLY, @@ -18,7 +19,6 @@ ) if TYPE_CHECKING: - import logging from collections.abc import Sequence _EPILOG: Final[str] = ( @@ -29,7 +29,7 @@ """ ) -_logger: logging.Logger = setup_logging(__title__) +_logger: logging.Logger = logging.getLogger(__title__) def _parse_args( @@ -189,7 +189,8 @@ def main( # pylint: disable=R0914, R0915 # noqa: PLR0915 ) -> int: args, forwarded = _parse_args(argv) uninstall_args: list[str] = filter_forwards_include(forwarded, UNINSTALL_ONLY) - set_logging_level(_logger, verbose=args.verbose) + setup_logging(verbose=args.verbose) + _logger.debug("Forwarded arguments: %s", forwarded) _logger.debug("Arguments forwarded to 'pip uninstall': %s", uninstall_args) @@ -201,18 +202,24 @@ def main( # pylint: disable=R0914, R0915 # noqa: PLR0915 "Unrecognized arguments: %s", ", ".join(formatted_unrecognized_arg), ) + try: + requirements: list[str] = _read_from_requirements(args.requirements) + except OSError as err: + _logger.error("Could not open requirements file: %s", err) + return 1 - if not (packages := [*args.packages, *_read_from_requirements(args.requirements)]): - _logger.error("No packages provided") + if not (packages := [*args.packages, *requirements]): + _logger.error("You must give at least one requirement to uninstall") return 1 package_dependencies: dict[str, _DependencyInfo] = {} for package in packages: if not _is_installed(package): - _logger.warning("%s is not installed", package) + _logger.warning("Skipping %s as it is not installed", package) continue if package in args.exclude: + _logger.debug("Skipping %s", package) continue package_dependencies[package] = dependency_info = _get_dependencies_of_package( @@ -296,7 +303,11 @@ def main( # pylint: disable=R0914, R0915 # noqa: PLR0915 ) if args.freeze_purged_packages: - _freeze_packages(args.freeze_file, packages_to_uninstall) + try: + _freeze_packages(args.freeze_file, packages_to_uninstall) + except OSError as err: + _logger.error("Could not open requirements file: %s", err) + return 1 _logger.debug("Wrote packages to %s", args.freeze_file) joined_pip_cmd: str = " ".join(PIP_CMD) diff --git a/pip_manage/pip_review.py b/pip_manage/pip_review.py index 621ff385..c31084a6 100755 --- a/pip_manage/pip_review.py +++ b/pip_manage/pip_review.py @@ -5,11 +5,12 @@ __title__: Final[str] = "pip-review" import argparse +import logging import os from pathlib import Path from typing import TYPE_CHECKING, Final, NamedTuple -from pip_manage._logging import set_logging_level, setup_logging +from pip_manage._logging import setup_logging from pip_manage._pip_interface import ( INSTALL_ONLY, LIST_ONLY, @@ -19,7 +20,6 @@ ) if TYPE_CHECKING: - import logging from collections.abc import Callable, Sequence from pip_manage._pip_interface import _OutdatedPackage @@ -34,7 +34,7 @@ """ ) -_logger: logging.Logger = setup_logging(__title__) +_logger: logging.Logger = logging.getLogger(__title__) def _parse_args( @@ -239,7 +239,9 @@ def _format_table(columns: list[list[str]]) -> str: return "\n".join([head, ruler, *body, ruler]) -def main(argv: Sequence[str] | None = None) -> int: +def main( # pylint: disable=R0915 # noqa: PLR0915 + argv: Sequence[str] | None = None, +) -> int: args, forwarded = _parse_args(argv) list_args: list[str] = filter_forwards( forwarded, @@ -251,7 +253,7 @@ def main(argv: Sequence[str] | None = None) -> int: exclude=LIST_ONLY, include=INSTALL_ONLY, ) - set_logging_level(_logger, verbose=args.verbose) + setup_logging(verbose=args.verbose) _logger.debug("Forwarded arguments: %s", forwarded) _logger.debug("Arguments forwarded to 'pip list --outdated': %s", list_args) @@ -286,7 +288,11 @@ def main(argv: Sequence[str] | None = None) -> int: return 0 if args.freeze_outdated_packages: - _freeze_outdated_packages(args.freeze_file, outdated) + try: + _freeze_outdated_packages(args.freeze_file, outdated) + except OSError as err: + _logger.error("Could not open requirements file: %s", err) + return 1 _logger.debug("Wrote outdated packages to %s", args.freeze_file) if args.raw: @@ -296,8 +302,11 @@ def main(argv: Sequence[str] | None = None) -> int: constraints_files: list[Path] = _get_constraints_files(install_args) _logger.debug("Constraints files: %s", constraints_files) - - _set_constraints_of_outdated_pkgs(constraints_files, outdated) + try: + _set_constraints_of_outdated_pkgs(constraints_files, outdated) + except OSError as err: + _logger.error("Could not open requirements file: %s", err) + return 1 _logger.debug( "Outdated packages with new set constraints: %s", outdated, diff --git a/pyproject.toml b/pyproject.toml index f53b91e1..fe9a89f7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -128,6 +128,7 @@ lint.ignore = [ "B905", "TD002", "TD003", + "TRY400", ] lint.fixable = ["ALL"] lint.unfixable = []