From d12864d87ac645fbce8b3b75cb4812a6befb8e08 Mon Sep 17 00:00:00 2001 From: Huib Piguillet Date: Sun, 18 Feb 2024 15:12:13 +0100 Subject: [PATCH] fixed logging --- README.md | 33 +++++++++++++++++++++------------ pyproject.toml | 2 +- src/powerchord/cli.py | 23 ++++++++--------------- src/powerchord/logging.py | 38 ++++++++++++++++++++------------------ src/powerchord/runner.py | 11 ++++++++--- 5 files changed, 58 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index d8848f4..83171e4 100644 --- a/README.md +++ b/README.md @@ -6,25 +6,34 @@ ## Installation -```sh -python3 -m pip install -U powerchord +```commandline +pip install powerchord ``` ## Usage -Currently, tasks need to be specified in `pyproject.toml`: +Run a number of tasks: + +```commandline +powerchord -t task="command --foo bar /path/to/happiness" other-task="..." +``` + +For all options, see + +```commandline +powerchord -h +``` + +Config can also be specified in `pyproject.toml`: ```toml -# tasks to do [tool.powerchord.tasks] -do-something = "command --foo bar /path/to/happiness" -do-something-else = "..." +task = "command --foo bar /path/to/happiness" +other-task = "..." you-get-the-idea = "..." -# config -[tool.powerchord.verbosity] -# show output of successful tasks -success = ["info", "error"] # default [] -# show output of failed tasks -fail = ["info", "error"] # default ["info", "error"] +[tool.powerchord.log_levels] +all = "debug" | "info" | "warning" | "error" | "critical" | "" +success = "" # log level of successful task output +fail = "info" # log level of failed task output ``` diff --git a/pyproject.toml b/pyproject.toml index 79d704a..2efba21 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "powerchord" -version = "0.0.8" +version = "0.0.9" description = "Concurrent CLI task runner" authors = ["Huib Piguillet "] maintainers = ["Huib Piguillet"] diff --git a/src/powerchord/cli.py b/src/powerchord/cli.py index a90ff3c..30192bd 100644 --- a/src/powerchord/cli.py +++ b/src/powerchord/cli.py @@ -1,6 +1,7 @@ import argparse import asyncio import logging +import sys import tomllib from collections.abc import Sequence from dataclasses import dataclass, field @@ -9,7 +10,6 @@ from chili import TypeDecoder, decode -from .formatting import bright from .logging import LogLevel, LogLevels, logging_context from .runner import TaskRunner @@ -36,14 +36,16 @@ def __call__( option_string: str = None, ) -> None: value_seq = [values] if isinstance(values, str) else [str(v) for v in values or []] + d = getattr(namespace, self.dest) or {} try: pairs = (item.split('=', 1) for item in value_seq) - d = {key.strip(): value for key, value in pairs} + d |= {key.strip(): value for key, value in pairs} except ValueError: parser.error( f'argument {option_string}: not matching key1="some val" [key2="another val" ...]', ) - setattr(namespace, self.dest, d) + else: + setattr(namespace, self.dest, d) def config_from_args() -> Config | None: @@ -63,7 +65,7 @@ def config_from_args() -> Config | None: class FatalError(SystemExit): def __init__(self, *args): - log.error(f'💀 {" ".join(str(arg) for arg in args)}') + log.critical(f'💀 {" ".join(str(arg) for arg in args)}') super().__init__(1) @@ -77,11 +79,6 @@ def __init__(self, config_source: str = None, cause: str = None): super().__init__(message) -class FailedTasksError(FatalError): - def __init__(self, failed_tasks): - super().__init__(bright('Failed tasks:'), failed_tasks) - - def config_from_pyproject() -> Config | None: pyproject_file = 'pyproject.toml' try: @@ -92,7 +89,7 @@ def config_from_pyproject() -> Config | None: try: return decode(config_dict, Config, decoders={LogLevel: LogLevelDecoder()}) except ValueError as exc: - raise ConfigError(pyproject_file, str(exc)) from exc + raise ConfigError(pyproject_file, ' '.join(exc.args)) from exc def load_config() -> Config: @@ -107,8 +104,4 @@ def main() -> None: config = load_config() task_runner = TaskRunner(config.tasks) with logging_context(config.log_levels): - results = asyncio.run(task_runner.run_tasks()) - failed_tasks = [task for task, ok in results if not ok] - if failed_tasks: - log.error('') - raise FailedTasksError(failed_tasks) + sys.exit(not asyncio.run(task_runner.run_tasks())) diff --git a/src/powerchord/logging.py b/src/powerchord/logging.py index 1ba0a9b..5ab8d34 100644 --- a/src/powerchord/logging.py +++ b/src/powerchord/logging.py @@ -18,49 +18,51 @@ def task_log(success: bool) -> logging.Logger: class LogLevel(IntEnum): + NEVER = 100 CRITICAL = logging.CRITICAL ERROR = logging.ERROR WARNING = logging.WARNING INFO = logging.INFO DEBUG = logging.DEBUG - NOTSET = logging.NOTSET @classmethod def decode(cls, value: str) -> 'LogLevel': - return LogLevel(logging.getLevelName(value.upper())) if value else LogLevel.NOTSET + if not value: + return LogLevel.NEVER + try: + return LogLevel[value.upper()] + except KeyError as exc: + raise ValueError('Invalid log level:', value) from exc @dataclass class LogLevels: - all: LogLevel | None = LogLevel.INFO - success: LogLevel | None = None - fail: LogLevel | None = LogLevel.INFO + all: LogLevel = LogLevel.INFO + success: LogLevel = LogLevel.NEVER + fail: LogLevel = LogLevel.INFO -def queue_listeners(levels: LogLevels) -> Iterator[QueueListener]: - if not levels.all: - return +def queue_listener(levels: LogLevels) -> QueueListener | None: + if levels.all == LogLevel.NEVER: + return None console = logging.StreamHandler(sys.stdout) logging.basicConfig(handlers=[console], level=levels.all, format='%(message)s') + queue: Queue[logging.LogRecord] = Queue() for name, level in asdict(levels).items(): logger = logging.getLogger('powerchord.' + name) + logger.setLevel(max(level, levels.all)) + logger.addHandler(QueueHandler(queue)) logger.propagate = False - if level: - queue: Queue[logging.LogRecord] = Queue() - queue_handler = QueueHandler(queue) - queue_handler.setLevel(level) - logger.addHandler(queue_handler) - listener = QueueListener(queue, console) - yield listener + return QueueListener(queue, console) @contextmanager def logging_context(levels: LogLevels) -> Iterator[None]: - listeners = list(queue_listeners(levels)) - for listener in listeners: + listener = queue_listener(levels) + if listener: listener.start() try: yield finally: - for listener in listeners: + if listener: listener.stop() diff --git a/src/powerchord/runner.py b/src/powerchord/runner.py index 579c822..b209362 100644 --- a/src/powerchord/runner.py +++ b/src/powerchord/runner.py @@ -20,12 +20,17 @@ async def run_task(self, name: str, task: str) -> tuple[str, bool]: task_log(success).log(level, stream.decode()) return name, success - async def run_tasks(self) -> list[tuple[str, bool]]: + async def run_tasks(self) -> bool: if not self.tasks: log.warning('Nothing to do. Getting bored...\n') - return [] + return True tasks = self.tasks.items() summary = [f'• {name.ljust(self.max_name_length)} {dim(task)}' for name, task in tasks] for line in (bright('To do:'), *summary, '', bright('Results:')): log.info(line) - return await concurrent_call(self.run_task, tasks) + results = await concurrent_call(self.run_task, tasks) + failed_tasks = [task for task, ok in results if not ok] + if failed_tasks: + log.error('') + log.error(f'{bright("Failed tasks:")} {failed_tasks}') + return not failed_tasks