Capture `{cli}` output for reporting - r

I would like to capture the dynamic output produced by {cli} to use in error reporting/logging.
Here's an example:
my_function <- function(val, return_message = TRUE) {
if(val == 'a'){
if (return_message) cli::cli_alert("your value {val} = a!")
} else {
if (return_message) cli::cli_alert("your value {val} is not equal to a!")
}
if (return_message) cli::cli_h2("processing now complete")
return(val)
}
Using my_function, it returns val and also prints a message that is dynamic, based on the input value:
→ your value x is not equal to a!
── processing now complete ──
[1] "x"
Is there any way to capture the dynamic output from {cli} functions, ideally by appending to a list or a similar method?
Ideal output would be something like this:
my_data <- list(val = "x", message = c("your value x is not equal to a!", "processing now complete"))

I don't think cli_X() returns the text, but you could imagine a wrapper that might do what you want. Consider this as a start:
cli_wrapper <- function(str, type="alert", return_str = TRUE, return_message=TRUE, ...){
str <- with(list(...), glue::glue(str))
if(return_message){
cmd <- glue::glue('cli::cli_{type}("{str}")')
eval(parse(text=cmd))
}
if(return_str){
invisible(str)
}
}
In the function above, return_message indicates whether the cli_X() function should be run and the return_str indicates whether the string should be returned (invisibly). You could then rewrite your function with the cli_wrapper() function:
my_function <- function(val, return_message = TRUE) {
message <- NULL
if(val == 'a'){
message <- c(message, cli_wrapper("your value {val} = a!", type="alert", val = val, return_message = return_messasge))
}else{
message <- c(message, cli_wrapper("your value {val} is not equal to a!", type="alert", val = val, return_message = return_messasge))
}
message <- c(message, cli_wrapper("processing now complete", type="h2", return_message = return_messasge))
ret <- list(val=val, message = message)
invisible(ret)
}
Running the function a couple of different ways would give the following output:
my_data <- my_function(val="x", return_message=TRUE)
# → your value x is not equal to a!
#
# ── processing now complete ──
#
my_data
# $val
# [1] "x"
#
# $message
# [1] "your value x is not equal to a!" "processing now complete"
#
my_data <- my_function(val="x", return_message=FALSE)
my_data
# $val
# [1] "x"
#
# $message
# [1] "your value x is not equal to a!" "processing now complete"

{cli} provides a utility function cli_fmt() which gives a much easier interface for this behaviour.
# Function to test `cli_fmt()`
noisy_identity <- function(x) {
cli::cli_h1("Noisily returning {.val {x}}")
cli::cli_bullets(c(
"*" = "Here",
"*" = "are",
"*" = "some",
"*" = "bulllets!"
))
x
}
# This just returns `"hi"` and prints a bunch of text
noisy_identity("hi")
#>
#> ── Noisily returning "hi" ──────────────────────────────────────────────────────
#> • Here
#> • are
#> • some
#> • bulllets!
#> [1] "hi"
# Wrapping in `cli_fmt()` means only the message is returned
cli::cli_fmt(noisy_identity("hi"))
#> [1] ""
#> [2] "── Noisily returning \"hi\" ──────────────────────────────────────────────────────"
#> [3] "• Here"
#> [4] "• are"
#> [5] "• some"
#> [6] "• bulllets!"
# We can use this to define a function that modifies other functions so that
# they return both output value *and* messages produced like so:
capture_cli_messages <- function(fun) {
function(..., .quiet = TRUE) {
output <- list(result = NULL, messages = NULL)
output$messages <- cli::cli_fmt({
output$result <- fun(...)
})
if (!.quiet) cat(output$messages, sep = "\n")
output
}
}
# `capture_cli_messages()` modifies the behaviour of `noisy_identity()`
noisy_identity2 <- capture_cli_messages(noisy_identity)
noisy_identity2("hi")
#> $result
#> [1] "hi"
#>
#> $messages
#> [1] ""
#> [2] "── Noisily returning \"hi\" ──────────────────────────────────────────────────────"
#> [3] "• Here"
#> [4] "• are"
#> [5] "• some"
#> [6] "• bulllets!"
# The .quiet argument can be used if you still want to print the messages out
noisy_identity2("hi", .quiet = FALSE)
#>
#> ── Noisily returning "hi" ──────────────────────────────────────────────────────
#> • Here
#> • are
#> • some
#> • bulllets!
#> $result
#> [1] "hi"
#>
#> $messages
#> [1] ""
#> [2] "── Noisily returning \"hi\" ──────────────────────────────────────────────────────"
#> [3] "• Here"
#> [4] "• are"
#> [5] "• some"
#> [6] "• bulllets!"
Created on 2022-09-21 with reprex v2.0.2

Related

Get names at deepest level of a nested list in R

I'm been working with nested lists and recursive functions in R following this instructions. Now there is just one piece I miss to design an own function, which is getting a vector with the respective names sorted from the highest to the deepest level.
The input list is:
lst <- list(
title = "References and Plant Communities in 'SWEA-Dataveg'",
author = "Miguel Alvarez",
date = "Dezember 28, 2019",
"header-includes" = c(
"- \\usepackage[utf8]{inputenc}",
"- \\usepackage[T1]{fontenc}", "- \\usepackage{bibentry}",
"- \\nobibliography{sweareferences.bib}"),
output = list(pdf_document=list(citation_package="natbib")),
"biblio-style" = "unsrtnat",
bibliography = "sweareferences.bib",
papersize = "a4")
The structure of the output list will then looks like this (printed in the console). Herewith note the vector at lst$output$pdf_document$citation_package:
$title
[1] "title"
$author
[1] "author"
$date
[1] "date"
$`header-includes`
[1] "header-includes"
$output
$output$pdf_document
$output$pdf_document$citation_package
[1] "output"
[2] "pdf_document"
[3] "citation_package"
$`biblio-style`
[1] "biblio-style"
$bibliography
[1] "bibliography"
$papersize
[1] "papersize"
Of course, the function has to be recursive to be applied in any different case.
Here is one possible approach, using only base R. The following function f replaces each terminal node (or "leaf") of a recursive list x with the sequence of names leading up to it. It treats unnamed lists like named lists with all names equal to "", which is a useful generalization.
f <- function(x, s = NULL) {
if (!is.list(x)) {
return(s)
}
nms <- names(x)
if (is.null(nms)) {
nms <- character(length(x))
}
Map(f, x = x, s = Map(c, list(s), nms))
}
f(lst)
$title
[1] "title"
$author
[1] "author"
$date
[1] "date"
$`header-includes`
[1] "header-includes"
$output
$output$pdf_document
$output$pdf_document$citation_package
[1] "output" "pdf_document" "citation_package"
$`biblio-style`
[1] "biblio-style"
$bibliography
[1] "bibliography"
$papersize
[1] "papersize"
Using an external package, this can be done quite efficiently with rrapply() in the rrapply-package:
rrapply::rrapply(lst, f = function(x, .xparents) .xparents)
#> $title
#> [1] "title"
#>
#> $author
#> [1] "author"
#>
#> $date
#> [1] "date"
#>
#> $`header-includes`
#> [1] "header-includes"
#>
#> $output
#> $output$pdf_document
#> $output$pdf_document$citation_package
#> [1] "output" "pdf_document" "citation_package"
#>
#>
#>
#> $`biblio-style`
#> [1] "biblio-style"
#>
#> $bibliography
#> [1] "bibliography"
#>
#> $papersize
#> [1] "papersize"

How to prevent / remove blank line in console when printing list with custom print method

I am trying to reproduce the tibble-way of printing, for an object of class foo (which is basically a list).
When printing each list element separately, there is no issue. But when I try to use a programmatic approach for each list element, it adds a blank line in the console, which I don't want. How do I prevent this from happening?
foo_obj <- list(a = "hello", b = "world")
class(foo_obj) <- c("fooclass")
myfooter <- function(x, width) {
footer <- paste0(cli::symbol$ellipsis, " ", x)
pillar::style_subtle(paste("#", footer))
}
print.fooclass <- function(x, ...) {
print(x$a)
cat(myfooter("s\n\n", 40))
print(x$b)
cat(myfooter("s", 40))
}
## This is the desired output
foo_obj
#> [1] "hello"
#> # … s
#>
#> [1] "world"
#> # … s
print.fooclass_ls <- function(x, ...) {
lapply(1:length(x), function(i){
print(x[i])
cat(myfooter("s\n", 40))
}
)
}
class(foo_obj) <- c("fooclass_ls")
## The empty lines after the print are NOT desired
foo_obj
#> $a
#> [1] "hello"
#>
#> # … s
#> $b
#> [1] "world"
#>
#> # … s
Created on 2021-03-10 by the reprex package (v1.0.0)
Thanks to user20650's great idea! - I will follow their suggestion and self-answer.
The list object prints with a line break - but if we print the sub-element [[i]], there is no line break. In order to get the names printed, you still need to add the names as well!
foo_obj <- list(a = "hello", b = "world")
class(foo_obj) <- c("fooclass")
myfooter <- function(x) {
footer <- paste0(cli::symbol$ellipsis, " ", x)
pillar::style_subtle(paste("#", footer))
}
print.fooclass <- function(x, ...) {
lapply(1:length(x), function(i){
cat(paste0("$", names(x)[i], "\n")) # for the names, which I want
print(x[[i]])
cat(myfooter("s\n\n"))
}
)
}
foo_obj
#> $a
#> [1] "hello"
#> # … s
#>
#> $b
#> [1] "world"
#> # … s
Created on 2021-03-11 by the reprex package (v1.0.0)

Apply mutate to data frame in R: iterates one extra step and causes error

I wonder why my get_http_status function iterates once more than necessary causing an exception
I have a data frame like:
> str(df5)
'data.frame': 10 obs. of 3 variables:
$ text : chr "\n" "\n" "\n" "\n" ...
$ enlace: chr "//www.blogger.com| __truncated__ ...
$ Freq : int 1 1 1 1 1 1 1 1 1 r code here
I'm trying to get the http status code for each "enlace"
Using this function:
get_http_status <- function(url){
if (!is.null(url)){
Sys.sleep(3)
print(url)
ret <- HEAD(url)
return(ret$status_code)
}
return("")
}
df44 <- mutate(df5, status = get_http_status(enlace))
but keeps trowing the error:
** Error in parse_url(url) : length(url) == 1 is not TRUE**
i can warp the function with try/catch and it works, but i don't know why the error is happening in first place.
get_http_status_2 <- function(url){
tryCatch(
expr = {
Sys.sleep(3)
print(url)
ret <- HEAD(url)
return(ret$status_code)
},
error = function(e){
return("")
}
)
}
The content of the df5$enlace is:
> df5$enlace
[1] "//www.blogger.com/rearrange?blogID=4514563088285989046&widgetType=Attribution&widgetId=Attribution1&action=editWidget&sectionId=footer-3"
[2] "//www.blogger.com/rearrange?blogID=4514563088285989046&widgetType=BlogArchive&widgetId=BlogArchive1&action=editWidget&sectionId=sidebar-right-1"
[3] "//www.blogger.com/rearrange?blogID=4514563088285989046&widgetType=BlogSearch&widgetId=BlogSearch1&action=editWidget&sectionId=sidebar-right-1"
[4] "//www.blogger.com/rearrange?blogID=4514563088285989046&widgetType=Followers&widgetId=Followers1&action=editWidget&sectionId=sidebar-right-1"
[5] "//www.blogger.com/rearrange?blogID=4514563088285989046&widgetType=PageList&widgetId=PageList1&action=editWidget&sectionId=crosscol"
[6] "//www.blogger.com/rearrange?blogID=4514563088285989046&widgetType=Text&widgetId=Text1&action=editWidget&sectionId=sidebar-right-1"
[7] "//www.blogger.com/rearrange?blogID=4514563088285989046&widgetType=Text&widgetId=Text2&action=editWidget&sectionId=sidebar-right-1"
[8] "http://5d4a.wordpress.com/2010/08/02/smashing-the-stack-in-2010/"
[9] "http://advancedwindowsdebugging.com/ch06.pdf"
[10] "http://beej.us/guide/
I think it iterate one time more because the result of the function is:
> df44 <- mutate(df5, status = get_http_status(enlace))
[1] "//www.blogger.com/rearrange?blogID=4514563088285989046&widgetType=Attribution&widgetId=Attribution1&action=editWidget&sectionId=footer-3"
[2] "//www.blogger.com/rearrange?blogID=4514563088285989046&widgetType=BlogArchive&widgetId=BlogArchive1&action=editWidget&sectionId=sidebar-right-1"
[3] "//www.blogger.com/rearrange?blogID=4514563088285989046&widgetType=BlogSearch&widgetId=BlogSearch1&action=editWidget&sectionId=sidebar-right-1"
[4] "//www.blogger.com/rearrange?blogID=4514563088285989046&widgetType=Followers&widgetId=Followers1&action=editWidget&sectionId=sidebar-right-1"
[5] "//www.blogger.com/rearrange?blogID=4514563088285989046&widgetType=PageList&widgetId=PageList1&action=editWidget&sectionId=crosscol"
[6] "//www.blogger.com/rearrange?blogID=4514563088285989046&widgetType=Text&widgetId=Text1&action=editWidget&sectionId=sidebar-right-1"
[7] "//www.blogger.com/rearrange?blogID=4514563088285989046&widgetType=Text&widgetId=Text2&action=editWidget&sectionId=sidebar-right-1"
[8] "http://5d4a.wordpress.com/2010/08/02/smashing-the-stack-in-2010/"
[9] "http://advancedwindowsdebugging.com/ch06.pdf"
[10] "http://beej.us/guide/bgc/"
Error in parse_url(url) : length(url) == 1 is not TRUE
Since your function contains a function that is not vectored, use the apply family of higher order function to iterate over your vector.
Below, get_http_status will be called on each element of df$enlace.
For each call a length one character vector is expected as the return, character(1):
vapply(df5$enlace, get_http_status, character(1))

purr::pmap does not keep parameter names

I'm trying to execute a function that uses the names of passed parameters with purrr::pmap. Unlike purrr::map (see below), pmap doesn't preserve these names. The below MWE captures the issue:
print_names <- function(x) {
print(names(x))
}
namedVec <- c(nameA = "valueA")
purrr::map(list(namedVec), print_names)
# [1] "nameA"
# [[1]]
# [1] "nameA"
purrr::pmap(list(namedVec), print_names)
# NULL
# $nameA
# NULL
Note that, in pmap, the .l argument needs to be a list of listed arguments, but in your function call it's just a list:
print_names <- function(x) {
print(names(x))
}
namedVec <- c(nameA = "valueA")
purrr::map(list(namedVec), ~print_names(.))
#> [1] "nameA"
#> [[1]]
#> [1] "nameA"
purrr::pmap(list(list(namedVec)), print_names)
#> [1] "nameA"
#> [[1]]
#> [1] "nameA"
Created on 2018-10-07 by the reprex package (v0.2.1)

sprintf padding with non English symbols

I encountered a strange sprintf() behaviour with the non English symbols. I tried padding a strings but I got an unexpected results:
lapply(c("ZZZ", "ZZZZZZ", "ЯЯЯ", "ЯЯЯЯЯЯ"),
function(x) sprintf("%-20s: %s", x, "VALUE"))
#> [[1]]
#> [1] "ZZZ : VALUE"
#>
#> [[2]]
#> [1] "ZZZZZZ : VALUE"
#>
#> [[3]]
#> [1] "ЯЯЯ : VALUE"
#>
#> [[4]]
#> [1] "ЯЯЯЯЯЯ : VALUE"
#>
Anybody can explain why this is happening and how to fix it?
Session info may be useful:
R version 3.2.2 (2015-08-14)
Platform: x86_64-pc-linux-gnu (64-bit)
Running under: Arch Linux
locale:
[1] LC_CTYPE=ru_RU.UTF-8 LC_NUMERIC=C LC_TIME=ru_RU.UTF-8 LC_COLLATE=C
[5] LC_MONETARY=ru_RU.UTF-8 LC_MESSAGES=ru_RU.UTF-8 LC_PAPER=ru_RU.UTF-8 LC_NAME=C
[9] LC_ADDRESS=C LC_TELEPHONE=C LC_MEASUREMENT=ru_RU.UTF-8 LC_IDENTIFICATION=C
attached base packages:
[1] stats graphics grDevices utils datasets methods base
loaded via a namespace (and not attached):
[1] shiny_0.12.2 R6_2.1.1 rsconnect_0.4.1.4 htmltools_0.2.6 tools_3.2.2 Rcpp_0.12.2 digest_0.6.8
[8] xtable_1.8-0 httpuv_1.3.3 mime_0.4
I can tell you why it's happening, but not how to fix it. From the docs for sprintf:
Field widths and precisions of %s conversions are interpreted as bytes, not characters, as described in the C standard.
In UTF-8 the character Я is two bytes (0xD0 0xAF), so "ЯЯЯ" is 6 bytes whereas "ZZZ" is 3 bytes, and sprintf renders them accordingly.
Edit
One workaround is to use sprintf's asterisk feature, which lets you declare the width of a field (in bytes), along with the nchar function, which lets you calculate both the display width and the number of bytes in a string.
So, for example, nchar("ЯЯЯ", "width") and nchar("ЯЯЯ", "bytes") return 3 and 6, respectively. If we want to pad its width to 20 display characters, then we have to give sprintf a width of 23 bytes: 20 plus the number of bytes minus the display width.
sprintf("%-*s", 23, "ЯЯЯ")
#> [1] "ЯЯЯ "
Or:
str <- "ЯЯЯ"
pad.len <- 20 + nchar(str, "bytes") + nchar(str, "width")
sprintf("%-*s", pad.len, str)
#> [1] "ЯЯЯ "
This works for "ZZZ", too, because the bytes and display width are equal, so it comes out to 20:
pad <- function(str) {
pad.len <- 20 + nchar(str, "bytes") - nchar(str, "width")
return(sprintf("%-*s: %s", pad.len, str, "VALUE"))
}
print(lapply(c("ZZZ", "ZZZZZZ", "ЯЯЯ", "ЯЯЯЯЯЯ"), pad))
#> [[1]]
#> [1] "ZZZ : VALUE"
#>
#> [[2]]
#> [1] "ZZZZZZ : VALUE"
#>
#> [[3]]
#> [1] "ЯЯЯ : VALUE"
#>
#> [[4]]
#> [1] "ЯЯЯЯЯЯ : VALUE"
P.S. This is the first R code I've ever written so if you see any ways to improve it please feel free to comment.
I found solution with stri_pad_right() function from stringi package:
lapply(c("ZZZ", "ZZZZZZ", "ЯЯЯ", "ЯЯЯЯЯЯ"),
function(x) paste0(stringi::stri_pad_right(x, 20), ": VALUE"))
#> [[1]]
#> [1] "ZZZ : VALUE"
#>
#> [[2]]
#> [1] "ZZZZZZ : VALUE"
#>
#> [[3]]
#> [1] "ЯЯЯ : VALUE"
#>
#> [[4]]
#> [1] "ЯЯЯЯЯЯ : VALUE"
#>
Update
Another solution based on the #Jordan answer uses only base R functions:
str_pad <- function(str, width = floor(0.9 * getOption("width")),
side = c("left", "both", "right")) {
side <- match.arg(side)
asc <- iconv(str, "latin1", "ASCII")
ind <- is.na(asc) | asc != str
if (any(ind))
width <- width + nchar(str, "bytes") - nchar(str, "width")
switch(side, left = sprintf("%-*s", width, str),
right = sprintf("%*s", width, str),
both = sprintf("%-*s", width, sprintf("%*s", floor(width/2), str)))
}
lapply(c("ZZZ", "ZZZZZZ", "ЯЯЯ", "ЯЯЯЯЯЯ"),
function(x) paste0(str_pad(x, 20), ": VALUE"))
#> [[1]]
#> [1] "ZZZ : VALUE"
#>
#> [[2]]
#> [1] "ZZZZZZ : VALUE"
#>
#> [[3]]
#> [1] "ЯЯЯ : VALUE"
#>
#> [[4]]
#> [1] "ЯЯЯЯЯЯ : VALUE"
#>

Resources