rOpenSci | Please Shut Up! Verbosity Control in Packages

We recently introduced a new paragraph to the development version of our dev guide

Provide a way for users to opt out of verbosity, preferably at the package level: make message creation dependent on an environment variable or option (like “usethis.quiet” in the usethis package), rather than on a function parameter. The control of messages could be on several levels (“none, “inform”, “debug”) rather than logical (no messages at all / all messages). Control of verbosity is useful for end users but also in tests. More interesting comments can be found in an issue of the tidyverse design guide.


This is a companion discussion topic for the original entry at https://ropensci.org/blog/2024/02/06/verbosity-control-packages
1 Like

Thanks rOpenSci folks! This post inspired me to implement package-level verbosity control in a current project: Implement package-level quiet option by bpbond · Pull Request #14 · COMPASS-DOE/whattheflux · GitHub

2 Likes

Nice read. I see a few exceptions to this otherwise very sensible guideline, which may be worth highlighting:

  1. When we actually want to control the verbosity of specific functions rather than of the entire package. This is relevant when multiple functions form a pipeline and we want to give package user control over verbosity in specific parts of the pipeline, e.g. start pipeline with function run_pipeline, which has arguments verbose_compute_step1, verbose_compute_step2 and verbose_plot to control the inner verbosity.
  2. Starting an R proces with a single command (function call) on a single line can be powerful as it does not require additional code comments or wrapper functions to clarify that the options() call and the function call act as one unit. I sometimes find it impractical when functions expect me to always run an options() call prior and another options() call to reset options afterward.
  3. When we want to send progress updates of a long running computation to the console. For this purpose I like function level verbosity with cat() because cat() is displayed in a neutral color aligned with the users’ own IDE color configuration instead of the alarming red as used for message(). All of this is important to me because I want errors and warnings to stand out in color in between all the trivial progress updates. My understanding is that ANSI sequences cannot query or adapt to the local IDE color settings as in we need to know whether user has black text on white background or white text on black background to decide whether to message() in black or white. Further, my understanding is that cat() cannot be controlled with options() in the way as shown in the blog post (please correct me if I am wrong!) therefore I see no other way then to use cat() and control verbosity at function level for this purpose.
2 Likes

For point 1, I see how that would be useful, have you considered an enum: verbose_compute = c("step1","step3"), that way you’d be able to keep your user interface constant when the number of steps change. In that same vein, you could still do this with options I suppose, but that might be less user friendly? Very interesting!

For point 2, have you considered withr? Options — with_options • withr, that’ll save you from setting and resetting options. Like you I am also a bit reluctant to change global state where I can avoid it.

For point 3, using cat() for user messaging is difficult to suppress and more difficult to write good tests for, which is why it is recommended against in the [rOpenSci Build Guide](Chapter 1 Packaging Guide | rOpenSci Packages: Development, Maintenance, and Peer Review. It’s certainly caused me issues before.

I believe you’d be better off using cli::cli_inform() and setting ANSI NO_COLOR as an option, possibly with withr.

What do you think?

2 Likes

Thanks a lot, I didn’t know about these tricks. You have taken away my concerns! Point 1 is not critical for me, but an attempt to identify what could be critical for others.

2 Likes

Thanks both @vincentvanhees @PietrH!
I guess you could also have an argument whose default value is the option, bla <- function(..., .verbose = getOption("mypkg.verbose", FALSE)). I’m adding a dot to the argument name so it’s clear it’s not necessary, but I feel it still makes the function signature longer to read.
I wonder what @mpadge thinks about this.

Hi maelle!

I agree it makes the signature more difficult to read. In general I think options are less well adopted or understood by users than boolean or enumerated arguments. However, I hope that most users wouldn’t need to change default verbosity levels all that much anyway.

What do you think about httr2::req_verbose(): Show extra output when request is performed — req_verbose • httr2 ? I quite like the idea of piping on a function that controls verbosity, that way it’s out of the way when you don’t need it, but has fine grained control that is documented on a single page, and can be maintained single file.

Tangent:

I feel required and non required arguments are better communicated by the presence or absence of a default value, if an argument is required, it should never have a default value, and if it has a default value, it should never be required. Tidy design principles - Required args shouldn’t have defaults got me fully on board.

There actually is a tidyverse design principle about the dot prefix: Tidy design principles - Dot prefix, but I haven’t fully grasped that one yet.

1 Like

Oh, httr2::req_verbose() is nice indeed!

Right, the dot prefix is only for that case, you are right. The thing I am still trying to fully grasp are those functions Function reference • rlang especially Check that dots are empty — check_dots_empty • rlang (sorry for making the tangent longer :smile_cat: )

1 Like

Super interesting, I’d never noticed those. Certainly something to keep in mind…

Nice read and I agree with most of what is mentioned in the post and the discussions. I only want to point out one feature the base R functions stop(), error(), warning() or stopifnot() share we should all love them for. It is the only place in R providing proper multilingual support! Of course this is a feature, if you use are working in an English language environment, you are likely not even aware of.

Error and warning messages often appear in German on my console, and especially for base R packages the support is excellent. For example (on a German console) stopifnot() translate messages whereas the plug-in replacement assert_that does not (yet??). How is the multilingual support for the suggested changes?

I agree warnings and error messages are often cryptic at best and there is also need to be better control. I tried once to translate the messages for one of my packages, but ended up improving all my messages instead. Translating messages is a good start contributing to and improving packages. And I would argue that messages end up less cryptic and a lot more useful for many users, if translations are provided. And less reasons to shut them up. This will also align nicely with rOpenSci effort in translations of documentation as mentioned in some recent blog posts, which I really appreciate.

Excellent point! For multilingual support I’d use potools: How to translate your package's messages with {potools} · Maëlle's R Blog

Thanks all for these insightful responses! A couple of thoughts from my side:

  • @KlausVigo your point is indeed extremely important, and (… fortunately …) the use of rlang functions entirely respects multi-lingual messaging. I’m not about other, derived packages like cli?

  • The use of options is essentially to ease workflows which rely on ps/withr. These (can) spawn sub-processes which inherit options by default but not necessarily environments. There are a book’s worth of interesting parallels with other languages and systems, but essentially resolve to R being designed such that options provide a safer and more controllable way to pass these kinds of things between instances than environments.

  • Function-level control is something we perhaps should at least have mentioned. Sorry that we didn’t, and thank you @vincentvanhees for also raising a very important point. I have no particular thoughts or insights there, but suspect also that req_verbose() would provide a useful analogy for a general recommendation. Any package could have a simple function to make calls verbose, because rlang::local_options() would be reset at the end of the chain.

1 Like

Thanks to the rOpenSci team for the guidance on this!

How should verbosity control via local or global options be documented? A section inside the documentation of each function that is concerned (e.g. #' @section Verbosity control:)? In the vignette?

Maybe in README together with other setup/installation information?

If you use the same section in all manual pages, you can re-use the content, see Reusing documentation • roxygen2

1 Like