Presenting messages to the user¶
The main interface for the application to emit messages is the emit object. It
handles everything that goes to screen and to the log file, even interfacing with the
formal logging infrastructure to get messages from it.
It’s a singleton, just import it wherever it needs to be used:
from craft_cli import emit
Before using it, though, it must be initiated. For example:
emit.init(EmitterMode.BRIEF, "example-app", "Starting example app v1.")
After bootstrapping the library as shown before, and importing emit wherever is
needed, all its usage is just sending information to the user. The following sections
describe the different ways of doing that.
Regular messages¶
The message method is for the final output of the running command.
def message(self, text: str) -> None:
E.g.:
emit.message("The meaning of life is 42.")
Progress messages¶
The progress method is to present all the messages that provide information on what
the application is currently doing.
Messages shown this way are ephemeral in QUIET or BRIEF modes (overwritten by
the next line) and will be truncated to the terminal’s width in that case.
If a progress message is important enough that it should not be overwritten by the next
ones, use permanent=True.
def progress(self, text: str, permanent: bool = False) -> None:
E.g.:
emit.progress("Assembling stuff...")
Progress bar¶
The progress_bar method is to be used in a potentially long-running single step of a
command (e.g. a download or provisioning step).
It receives a text that should reflect the operation that is about to start, a
total that will be the number to reach when the operation is completed, and
optionally a delta=False to indicate that calls to .advance method should pass
the total so far (by default is True, which implies that calls to .advance indicates
the delta in the operation progress). Returns a context manager with the .advance
method to call on each progress.
def progress_bar(self, text: str, total: Union[int, float], delta: bool = True) -> _Progresser:
E.g.:
hasher = hashlib.sha256()
with emit.progress_bar("Hashing the file...", filepath.stat().st_size) as progress:
with filepath.open("rb") as fh:
while True:
data = fh.read(65536)
hasher.update(data)
progress.advance(len(data))
if not data:
break
Verbose messages¶
Verbose messages are useful to provide more information to the user that shouldn’t be exposed when in brief mode for clarity and simplicity.
def verbose(self, text: str) -> None:
E.g.:
emit.verbose("Deleted the temporary file.")
Debug messages¶
The debug method is to record everything that the user may not want to normally see
but useful for the app developers to understand why things are failing or performing
forensics on the produced logs.
def debug(self, text: str) -> None:
E.g.:
emit.debug(f"Hash calculated correctly: {hash_result}")
Trace messages¶
The trace method is a way to expose system-generated information, about the general
process or particular information, which in general would be too overwhelming for
debugging purposes but sometimes needed for particular analysis.
It only produces information to the screen and into the logs if the Emitters is set to TRACE mode.
def trace(self, text: str) -> None:
E.g.:
emit.trace(f"Headers of the server response: {response.headers}")
Get messages from subprocesses¶
The open_stream returns a context manager that can be used to get the standard
output and/or error from the executed subprocess.
This way all the outputs of the subprocess will be captured by craft-cli and shown
or not to the screen (according to verbosity setup) and always logged.
def open_stream(self, text: str) -> _StreamContextManager:
E.g.:
with emit.open_stream("Running ls") as stream:
subprocess.run(["ls", "-l"], stdout=stream, stderr=stream)
Emitter modes and startup¶
The emit singleton object is first configured with an explicit call init():
E.g.:
emit.init(
EmitterMode.BRIEF,
"craft",
f"Starting craft version {__version__}",
log_filepath=logpath,
)
It is only after this point that emit can be used for printing. Note that the mode
is typically initialised to EmitterMode.BRIEF. The user can control the emitter mode
through global arguments. The Dispatcher, as mentioned earlier, handles global
arguments (including help). However, the Dispatcher only applies emitter mode
changes during pre_parse_args() when parsing the global arguments (e.g.
--verbosity=trace) later on in the code.
E.g.:
dispatcher.pre_parse_args(sys.argv[1:])
The implication of the two step process above is that between init() and
pre_parse_args() tracing type messages will be dropped. If you wish to support
configurable message verbosity levels during early initialisation, only do that after
the dispatcher’s pre_parse_args().
Proposed emitter and dispatcher startup:
emit.init(...)
dispatcher = Dispatcher(...)
global_args = dispatcher.pre_parse_args(sys.argv[1:])
dispatcher.load_command(global_args)
<early initialisation with support for verbosity levels>
dispatcher.run()
How to easily try different message types¶
There is a collection of examples in the project, in the examples.py file. Some
examples are very simple, exercising only one message type, but others use different
combinations so it’s easy to explore more complex behaviours.
To run them using the library, a virtual environment needs to be setup:
python3 -m venv env
env/bin/pip install -e .[dev]
source env/bin/activate
After that, is just a matter of running the file specifying which example to use:
./examples.py 18
We encourage you to adapt/improve/hack the examples in the file to play with different combinations of message types to learn and “feel” how the output would be in the different cases.
Understanding which/how messages are shown/logged¶
This is how texts are exposed to the screen for the different situations according to the selected verbosity level by the user running the application.
The last column of the table though is not about the screen: it indicates if the information will be present in the log created automatically by Craft CLI.
QUIET |
BRIEF |
VERBOSE |
DEBUG |
TRACE |
also to logfile |
|
|---|---|---|---|---|---|---|
|
– |
stdout
permanent
plain
|
stdout
permanent
plain
|
stdout
permanent
plain
|
stdout
permanent
plain
|
yes |
|
– |
stderr
transient (*)
plain
|
stderr
permanent
plain
|
stderr
permanent
timestamp
|
stderr
permanent
timestamp
|
yes |
.progress(..., permanent=True) |
– |
stderr
permanent
plain
|
stderr
permanent
plain
|
stderr
permanent
timestamp
|
stderr
permanent
timestamp
|
yes |
|
– |
stderr
transient (*)
plain
|
stderr
permanent
plain
|
stderr
permanent
timestamp
|
stderr
permanent
timestamp
|
first line only,
without progress
|
|
– |
stderr
transient (*)
plain
|
stderr
permanent
plain
|
stderr
permanent
timestamp
|
stderr
permanent
timestamp
|
yes |
|
– |
– |
stderr
permanent
plain
|
stderr
permanent
timestamp
|
stderr
permanent
timestamp
|
yes |
|
– |
– |
– |
stderr
permanent
timestamp
|
stderr
permanent
timestamp
|
yes |
|
– |
– |
– |
– |
stderr
permanent
timestamp
|
only when
level=trace
|
captured logs
(level >
logging.DEBUG) |
– |
– |
stderr
permanent
plain
|
stderr
permanent
timestamp
|
stderr
permanent
timestamp
|
yes |
captured logs
(level ==
logging.DEBUG) |
– |
– |
– |
stderr
permanent
timestamp
|
stderr
permanent
timestamp
|
yes |
captured logs
(level <
logging.DEBUG) |
– |
– |
– |
– |
stderr
permanent
timestamp
|
only when
level=trace
|
(*) when redirected to a file it doesn’t make sense to have “transient” messages, so ‘progress’ messages will always end in a newline, and ‘progress_bar’ will just send its message line but without the progress indication.
When the application ends in error it should call the emit.error() method passing a
CraftError instance. According to the verbosity level some information will be
exposed or not. The following table details what happens in each case: which
CraftError attribute is exposed and how the information is shown (similar to the
other table above):
QUIET |
BRIEF |
VERBOSE |
DEBUG |
TRACE |
also to logfile |
|
|---|---|---|---|---|---|---|
the error message |
yes |
yes |
yes |
yes |
yes |
yes |
full tracebacks |
no |
no |
no |
yes |
yes |
yes |
|
no |
no |
no |
yes |
yes |
yes |
|
yes |
yes |
yes |
yes |
yes |
yes |
|
yes |
yes |
yes |
yes |
yes |
yes |
how is it shown |
stderr
permanent
plain
|
stderr
permanent
plain
|
stderr
permanent
plain
|
stderr
permanent
timestamp
|
stderr
permanent
timestamp
|
yes |