Skip to content

Commit

Permalink
commands: add output helpers to WestCommand
Browse files Browse the repository at this point in the history
This is basically a duplicate of the west.log interface, except with:

- better names
- no use of global state
- support for a 'quiet' mode

The goal is to move over built-in commands to using this interface in
order to eliminate global state. That in turn may help us with our
goals of running more west tests in parallel and is just better design
anyway since the west APIs should be clean when used as a library
outside of any command.

We can move over extension functions in zephyr etc. over time as well,
probably also by using a helper that can detect older versions of west
and work anyway.

That will then allow us to deprecate west.log, removing it in the long
term, and adding support for a global "west --quiet <command> args"
style of invocation for the folks who just hate terminal output. I
want to get this conversion all done by the time Zephyr LTS3 is done,
so it's time to get the ball rolling now. I missed the boat on LTS2
and that makes me sad.

Part of: #149

Signed-off-by: Martí Bolívar <marti.bolivar@nordicsemi.no>
  • Loading branch information
mbolivar-nordic committed Aug 31, 2022
1 parent 2969181 commit b4d0b38
Showing 1 changed file with 165 additions and 2 deletions.
167 changes: 165 additions & 2 deletions src/west/commands.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
# Copyright 2018 Open Source Foundries Limited.
# Copyright 2019 Foundries.io Limited.
# Copyright 2022 Nordic Semiconductor ASA
#
# SPDX-License-Identifier: Apache-2.0

from abc import ABC, abstractmethod
import argparse
from collections import OrderedDict
from dataclasses import dataclass
from enum import IntEnum
import importlib.util
import itertools
import os
Expand All @@ -16,8 +18,9 @@
import subprocess
import sys
from types import ModuleType
from typing import Dict, List, Optional
from typing import Dict, List, NoReturn, Optional

import colorama
import pykwalify
import yaml

Expand Down Expand Up @@ -72,12 +75,50 @@ def _no_topdir_msg(cwd, name):
- Run "west init -h" for additional information.
'''

class Verbosity(IntEnum):
'''Verbosity levels for WestCommand instances.'''

#: No output is printed when WestCommand.dbg(), .inf(), etc.
#: are called.
QUIET = 0

#: Only error messages are printed.
ERR = 1

#: Only error and warnings are printed.
WRN = 2

#: Errors, warnings, and informational messages are printed.
INF = 3

#: Like INFO, but WestCommand.dbg(..., level=Verbosity.DBG) output
#: is also printed.
DBG = 4

#: Like DEBUG, but WestCommand.dbg(..., level=Verbosity.DBG_MORE)
#: output is also printed.
DBG_MORE = 5

#: Like DEBUG_MORE, but WestCommand.dbg(..., level=Verbosity.DBG_EXTREME)
#: output is also printed.
DBG_EXTREME = 6

#: Color used (when applicable) for printing with inf()
INF_COLOR = colorama.Fore.LIGHTGREEN_EX

#: Color used (when applicable) for printing with wrn()
WRN_COLOR = colorama.Fore.LIGHTYELLOW_EX

#: Color used (when applicable) for printing with err() and die()
ERR_COLOR = colorama.Fore.LIGHTRED_EX

class WestCommand(ABC):
'''Abstract superclass for a west command.'''

def __init__(self, name: str, help: str, description: str,
accepts_unknown_args: bool = False,
requires_workspace: bool = True):
requires_workspace: bool = True,
verbosity: Verbosity = Verbosity.INF):
'''Abstract superclass for a west command.
Some fields, such as *name*, *help*, and *description*,
Expand All @@ -96,12 +137,14 @@ def __init__(self, name: str, help: str, description: str,
:param requires_workspace: if true, the command requires a
west workspace to run, and running it outside of one is
a fatal error.
:param verbosity: command output verbosity level; can be changed later
'''
self.name: str = name
self.help: str = help
self.description: str = description
self.accepts_unknown_args: bool = accepts_unknown_args
self.requires_workspace = requires_workspace
self.verbosity = verbosity
self.topdir: Optional[str] = None
self.manifest = None
self.config = None
Expand Down Expand Up @@ -334,6 +377,126 @@ def _parse_git_version(raw_version):
return version
return version + (int(patch),)

def dbg(self, *args, level: Verbosity = Verbosity.DBG):
'''Print a verbose debug message.
The message is only printed if *self.verbosity* is at least *level*.
:param args: sequence of arguments to print
:param level: verbosity level of the message
'''
if level > self.verbosity:
return
print(*args)

def inf(self, *args, colorize: bool = False):
'''Print an informational message.
The message is only printed if *self.verbosity* is at least INF.
:param args: sequence of arguments to print.
:param colorize: If this is True, the configuration option ``color.ui``
is undefined or true, and stdout is a terminal, then
the message is printed in green.
'''
if not self.color_ui:
colorize = False

# This approach colorizes any sep= and end= text too, as expected.
#
# colorama automatically strips the ANSI escapes when stdout isn't a
# terminal (by wrapping sys.stdout).
if colorize:
print(INF_COLOR, end='')

print(*args)

if colorize:
self._reset_colors(sys.stdout)

def banner(self, *args):
'''Prints args as a "banner" using inf().
The args are prefixed with '=== ' and colorized by default.'''
self.inf('===', *args, colorize=True)

def small_banner(self, *args):
'''Prints args as a smaller banner(), i.e. prefixed with '-- ' and
not colorized.'''
self.inf('---', *args, colorize=False)

def wrn(self, *args):
'''Print a warning.
The message is only printed if *self.verbosity* is at least WRN.
The message is prefixed with the string ``"WARNING: "``.
If the configuration option ``color.ui`` is undefined or true and
stdout is a terminal, then the message is printed in yellow.
:param args: sequence of arguments to print.'''

if self.color_ui:
print(WRN_COLOR, end='', file=sys.stderr)

print('WARNING: ', end='', file=sys.stderr)
print(*args, file=sys.stderr)

if self.color_ui:
self._reset_colors(sys.stderr)

def err(self, *args, fatal: bool = False):
'''Print an error.
The message is only printed if *self.verbosity* is at least ERR.
This function does not abort the program. For that, use `die()`.
If the configuration option ``color.ui`` is undefined or true and
stdout is a terminal, then the message is printed in red.
:param args: sequence of arguments to print.
:param fatal: if True, the the message is prefixed with
"FATAL ERROR: "; otherwise, "ERROR: " is used.
'''

if self.color_ui:
print(ERR_COLOR, end='', file=sys.stderr)

print('FATAL ERROR: ' if fatal else 'ERROR: ', end='', file=sys.stderr)
print(*args, file=sys.stderr)

if self.color_ui:
self._reset_colors(sys.stderr)

def die(self, *args, exit_code: int = 1) -> NoReturn:
'''Print a fatal error using err(), and abort the program.
:param args: sequence of arguments to print.
:param exit_code: return code the program should use when aborting.
Equivalent to ``die(*args, fatal=True)``, followed by an attempt to
abort with the given *exit_code*.'''
self.err(*args, fatal=True)
sys.exit(exit_code)

@property
def color_ui(self) -> bool:
'''Should we colorize output?'''
return self.config.getboolean('color.ui', default=True)

#
# Internal APIs. Not for public consumption.
#

def _reset_colors(self, file):
# The flush=True avoids issues with unrelated output from
# commands (usually Git) becoming colorized, due to the final
# attribute reset ANSI escape getting line-buffered
print(colorama.Style.RESET_ALL, end='', file=file, flush=True)


#
# Private extension API
#
Expand Down

0 comments on commit b4d0b38

Please sign in to comment.