Explanations¶
About the appropriate mode to initiate emit
¶
The first mandatory parameter of the emit
object is mode
, which controls the initial verbosity level of the system.
As the user can change the level later using global arguments when executing the application (this is the application default level), it’s recommended to use EmitterMode.BRIEF
, unless the application needs to honour any external configuration or indication (e.g. a DEBUG
environment variable).
The values for mode
are the following attributes of the EmitterMode
enumerator:
EmitterMode.QUIET
: to present only error messages, if they happenEmitterMode.BRIEF
: error and info messages, with nice progress indicationsEmitterMode.VERBOSE
: for more verbose outputs, showing extra information to the userEmitterMode.DEBUG
: aimed to provide useful information to the application developers; this includes timestamps on each lineEmitterMode.TRACE
: to also expose system-generated information (in general too overwhelming for debugging purposes but sometimes needed for particular analysis)
How Craft CLI manage the application logs¶
Unless overridden when emit
is initiated (see how to do that), the application logs will be managed by the Craft CLI library, according to the following rules:
one log file is always produced for each application run (only exposed to the user if the application ends in error or a verbose run was requested, for example by
--verbose
), naming the files with a timestamp so they are uniquelog files are located in a directory with the application name under the user’s log directory
only 5 files are kept, when reaching this limit the older file will be removed when creating the one for current run
Global and command specific arguments¶
One of the functionalities that the Dispatcher provides is global arguments handling: options that will be recognised and used no matter the position in the command line because they are not specific to any command, but global to all commands and the application itself.
For example, all these application executions are equivalent:
<app> –verbose <command> <command-parameter> <app> <command> –verbose <command-parameter> <app> <command> <command-parameter> –verbose
The Dispatcher automatically provides the following global arguments, but more can be specified through the extra_global_args
option (see how to do that):
-h
/--help
: provides a help text for the application or command-q
/--quiet
: sets theemit
output level to QUIET-v
/--verbose
: sets theemit
output level to VERBOSE--verbosity=LEVEL
: sets theemit
output level to the specified level (allowed arequiet
,brief
,verbose
,debug
andtrace
).
Each command can also specify its own arguments parsing rules using the fill_parser
method, which receives an ArgumentParser with all its features for parsing a command line argument. The parsing result will be passed to the command on execution, as the parsed_args
parameter of the run
method.
Group of commands¶
The Dispatcher’s command_groups
parameter is just a list CommandGroup
objects, each of one grouping different commands for the different types of functionalities that may offer the application. See its reference here, but its use is quite straightforward. E.g.:
CommandGroup("Basic", [LoginCommand, LogoutCommand])
A list of these command groups is what is passed to the Dispatcher
to run them as part of the application.
This grouping is uniquely for building the help exposed to the user, which improves the UX of the application.
When requesting the full application help, all commands will be grouped and presented in the order declared in each CommandGroup
and in the list given to the Dispatcher
, and when requesting help for one command, other commands from the same group are suggested to the user as related to the requested one.
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 |