How to Control PINT Logging Output

If you have run PINT, you have probably noticed that PINT can emit a generous amount of information in the form of log messages. These come in two forms: warnings emitted through the python warnings module, and messages sent through the python logging mechanism (some of which are also warnings). The amount of information emitted can result in the messages of interest being lost among routine messages, or one could wish for further detail (for example for debugging PINT code). There are tools for managing this information flow; although PINT is a library and thus simply emits messages, a user with a notebook, script, or GUI application can use these tools to manage this output.

Controlling log messages

Python’s logging is somewhat complicated and confusing. In PINT’s case we use the loguru to reconfigure it and make it easier to use, with some additional code in pint.logging to adapt it to our purposes (things like changing the format, adding colors, capturing warnings, and preventing duplicate messages from overwhelming users). It is worth explaining a design principle: libraries simply emit messages, while applications, notebooks, and scripts configure what to do with those messages. You can (re)configure the logging output:

import pint.logging
pint.logging.setup(level="DEBUG")

You can optionally pass other options to the setup() function, such as a destination, level, formats, custom filters, colors, etc. See documentation for pint.logging.setup().

level can be any of the existing loguru levels: TRACE, DEBUG, INFO, WARNING, ERROR, or you can define new ones.

The format can be something new, or you can use pint.logging.format. A full format that might be useful as a reference is:

format = "<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | <level>{level: <8}</level> | <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>"

while the default for pint.logging is:

format = "<level>{level: <8}</level> ({name: <30}): <level>{message}</level>"

If you want to use command-line arguments in a script to set the level you can do that like:

parser.add_argument("--log-level",type=str,choices=("TRACE", "DEBUG", "INFO", "WARNING", "ERROR"),default=pint.logging.script_level,help="Logging level",dest="loglevel")
args = parser.parse_args(argv)
pint.logging.setup(level=args.loglevel)

Note that loguru does not allow you to change the properties of an existing logger. Instead it’s better to remove it and make another (e.g., if you want to change the level). This is done by default, but if instead you want to add another logger (say to a file) you can run setup() with removeprior=False.

Defaults can be changed with environment variables like: $LOGURU_LEVEL, $LOGURU_FORMAT, $LOGURU_DEBUG_COLOR.

See loguru documentation for full set of options.

Warnings versus logging

The logging HOWTO describes the difference between warnings.warn and logging.warning thus:

  • warnings.warn() in library code if the issue is avoidable and the client application should be modified to eliminate the warning

  • logging.warning() if there is nothing the client application can do about the situation, but the event should still be noted

Although PINT does not follow these rules perfectly, it does emit both kinds of warning, and users may quite reasonably want to handle them in various ways. By default setup() will capture warnings and emit them through the logging module, but this can be turned off by setting capturewarnings=False.

Users can control the handling of warnings with “warning filters”; in the simplest arrangements, users can just use warnings.simplefilter("ignore") or similar to arrange for all warnings to be ignored or treated as exceptions; users can use the more sophisticated warnings.filterwarnings() to control warnings based on their module of origin and/or the class supplied to the warnings.warn() call. Of particular note is the confusingly named warnings.catch_warnings(), which is a context manager that supports temporary changes in how warnings are handled:

with warnings.catch_warnings():
    warnings.simplefilter("ignore")
    fitter.fit_toas()

For further details on the management of warnings, see the documentation of the module warnings.