Skip to content

Commit

Permalink
fixed logging
Browse files Browse the repository at this point in the history
  • Loading branch information
githuib committed Feb 18, 2024
1 parent 1fddfd9 commit d12864d
Show file tree
Hide file tree
Showing 5 changed files with 58 additions and 49 deletions.
33 changes: 21 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 <huib@proton.me>"]
maintainers = ["Huib Piguillet"]
Expand Down
23 changes: 8 additions & 15 deletions src/powerchord/cli.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import argparse
import asyncio
import logging
import sys
import tomllib
from collections.abc import Sequence
from dataclasses import dataclass, field
Expand All @@ -9,7 +10,6 @@

from chili import TypeDecoder, decode

from .formatting import bright
from .logging import LogLevel, LogLevels, logging_context
from .runner import TaskRunner

Expand All @@ -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:
Expand All @@ -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)


Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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()))
38 changes: 20 additions & 18 deletions src/powerchord/logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
11 changes: 8 additions & 3 deletions src/powerchord/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit d12864d

Please sign in to comment.