support displaying tabular data in cli condition messages

Open
#808 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
5/5
Estimated time
Over a week
Newbie friendliness
35/100
Issue type
Feature
Clarity
Mostly clear
Activity status
Stale
Tech stack
r
Domain
cli

Research direction

Start by reproducing the examples and reading the documented behavior of cli_warn(), cli_abort(), cli_verbatim(), and cli_table(). Done means tabular output retains its formatting while being part of one warning or error that can be suppressed or handled as a single condition message.

Written by the indexing model from the issue text.

Description

Background:

see Stack Overflow

I often like to include a view of relevant tabular data in a warning or error message. When I'm rendering quarto reports I often set chunk options to suppress certain levels of output, e.g. #| message: false to suppress messages but print warnings.

In the code below I'm trying to create output that behaves as a single warning, containing a summary of the warning and a snapshot of the offending data in a formatted table. The clitable package provides nice formatting for tables, but I can't seem to get it incorporated into a warning message with cli_warn() without screwing up the formatting.

library(dplyr)
library(cli)
library(clitable)


check_complete <- function(data, cols) {
  missing <- data |>
    select({{cols}}) |>
    filter(if_any(everything(), ~ is.na(.)))
  
  nrow_missing <- nrow(missing)
  
  if(nrow_missing > 0){
    table_text <- cli_table(missing)
    # include the table in the warning message
    cli_warn(c("There are {nrow_missing} rows with missing values in the specified columns:",
               table_text))
  }
}

check_complete(starwars, c(name:hair_color))
#> Warning: There are 33 rows with missing values in the specified columns:
#> ┌─────────────────────┬──────┬────┬────────────┐
#> │ name │height│mass│ hair_color │
#> ├─────────────────────┼──────┼────┼────────────┤
#> │ C-3PO │ 167 │ 75│ NA │
#> │ R2-D2 │ 96 │ 32│ NA │
#> │ R5-D4 │ 97 │ 32│ NA │
#> │ Wilhuff Tarkin │ 180 │ NA │auburn, grey│
#> ...snip...
#> │ Finn │ NA │ NA │ black │
#> │ Rey │ NA │ NA │ brown │
#> │ Poe Dameron │ NA │ NA │ brown │
#> │ BB8 │ NA │ NA │ none │
#> │ Captain Phasma │ NA │ NA │ none │
#> └─────────────────────┴──────┴────┴────────────┘

The whitespace is all screwed up here.

check_complete2 <- function(data, cols) {
  missing <- data |>
    select({{cols}}) |>
    filter(if_any(everything(), ~ is.na(.)))
  
  nrow_missing <- nrow(missing)

  if(nrow_missing > 0){
    table_text <- cli_table(missing)
    # use cli_verbatim() to preserve the whitespace
    # wrap in cli() to try to treat it as one message
    cli({
      cli_warn("There are {nrow_missing} rows with missing values in the specified columns:")
      cli_verbatim(table_text)
    })
  }
}

# correct formatting
check_complete2(starwars, c(name:hair_color))
#> Warning: There are 33 rows with missing values in the specified columns:
#> ┌─────────────────────┬──────┬────┬────────────┐
#> │ name                │height│mass│ hair_color │
#> ├─────────────────────┼──────┼────┼────────────┤
#> │ C-3PO               │ 167  │  75│ NA         │
#> │ R2-D2               │  96  │  32│ NA         │
#> │ R5-D4               │  97  │  32│ NA         │
#> │ Wilhuff Tarkin      │ 180  │ NA │auburn, grey│
#> │ Greedo              │ 173  │  74│ NA         │
#> ...snip...
#> │ Poe Dameron         │ NA   │ NA │ brown      │
#> │ BB8                 │ NA   │ NA │ none       │
#> │ Captain Phasma      │ NA   │ NA │ none       │
#> └─────────────────────┴──────┴────┴────────────┘

Here we've got the formatting correct, but it's not being classed as a warning, so it'll get suppressed if we suppress messages:

suppressMessages(check_complete2(starwars, c(name:hair_color)))
#> Warning: There are 33 rows with missing values in the specified columns:

How can I keep the formatting of the clitable() while outputting the table as part of a single warning message (or error message, in which case the single message part is even more important since execution will stop on the first error)?

Feature Request:

That cli status messages (inform, warn, abort etc) provide some way of rendering tabular data in the UI, so that developers can display more complex data structures to users. clitable provides one possible rendering approach, but it doesn't play nice with current cli functions.

Some suggested answers for reference

cli_warn uses cli_bullets to format the parameter message which likely messes up your cli_table. A hacky approach could be to replace spaces " " with non-break spaces. Outputing text inside a warning is limited by getOption(warning.length)

warning.length:
sets the truncation limit in bytes for error and warning messages. A non-negative integer, with allowed values 100...8170, default 1000.

I guess you can tackle that limitation by splitting the table and throwing multiple warnings.
But this replacing-approach works and the warning it produces can be suppressed.

library(dplyr)
library(cli)
library(clitable)

check_complete <- function(data, cols) {
  missing <- data |>
    dplyr::select({{cols}}) |>
    dplyr::filter(if_any(dplyr::everything(), ~ is.na(.)))
  
  nrow_missing <- nrow(missing)
  
  if(nrow_missing > 0){
    table_text <- cli_table(missing)
    old <- options(warning.length = 8170)
    on.exit(options(old), add = TRUE)
    # include the table in the warning message
    cli_warn(c("There are {nrow_missing} rows with missing values in the specified columns:",
               gsub(" ", "\u00a0", table_text)))
  }
}
data(starwars, package = "dplyr")

suppressMessages(check_complete(dplyr::bind_rows(replicate(10, starwars, simplify = FALSE)), c(name:hair_color)))

# Warning:
#  There are 330 rows with missing values in the specified columns:
# ┌─────────────────────┬──────┬────┬────────────┐
# │ name                │height│mass│ hair_color │
# ├─────────────────────┼──────┼────┼────────────┤
# │ C-3PO               │ 167  │  75│ NA         │
# │ R2-D2               │  96  │  32│ NA         │
# │ R5-D4               │  97  │  32│ NA         │
# │ Wilhuff Tarkin      │ 180  │ NA │auburn, grey│
# │ Greedo              │ 173  │  74│ NA         │
# │Jabba Desilijic Tiure│ 175  │1358│ NA         │
# │ Mon Mothma          │ 150  │ NA │ auburn     │
# │ Arvel Crynyd        │ NA   │ NA │ brown      │

# ... I cut the output here. The table continues until maximum of 8170 byte chars is hit

res


If you capture this warning log with sink or similar, there might be oddities searching the string for "C-3PO ", if there is actually "\u00a0" instead of " ". But if it's just about a visual table within a warning message, this might work for you. For large tables this will be slow. You can consider cutting off the table before warning

check_complete_cut <- function(data, cols) {
  missing <- data |>
    dplyr::select({{cols}}) |>
    dplyr::filter(if_any(dplyr::everything(), ~ is.na(.)))
  
  nrow_missing <- nrow(missing)
  
  if(nrow_missing > 0){
    table_text <- cli_table(missing[1:min(100, nrow(missing)),])
    old <- options(warning.length = 8170)
    on.exit(options(old), add = TRUE)
    # include the table in the warning message
    cli_warn(c("There are {nrow_missing} rows with missing values in the specified columns (showing max. the first 100 rows with missing values):",
               gsub(" ", "\u00a0", table_text)))
  }
}
data(starwars, package = "dplyr")

check_complete_cut(dplyr::bind_rows(replicate(10, starwars, simplify = FALSE)), c(name:hair_color))

2 Build warning directly with rlang::warn()

Since cli_warn is passed to rlang::warn you might just want to use the latter directly.
Here I recreate the white table text using ANSI colors crayon-style.

check_complete_warn <- function(data, cols) {
  missing <- data |>
    dplyr::select({{cols}}) |>
    dplyr::filter(if_any(dplyr::everything(), ~ is.na(.)))
  
  nrow_missing <- nrow(missing)
  
  if(nrow_missing > 0){
    table_text <- cli_table(missing)
    old <- options(warning.length = 8170)
    on.exit(options(old), add = TRUE)
    rlang::warn(
      message = c(
        sprintf("There are %d rows with missing values in the specified columns", nrow_missing),
        paste0("\n", "\033[37m", paste0(table_text, collapse = "\n"), "\033[39m")
      )
    )
  }
}
data(starwars, package = "dplyr")


suppressMessages(check_complete_warn(dplyr::bind_rows(replicate(10, starwars, simplify = FALSE)), c(name:hair_color)))

res

Dominant language
R
Stars
726
Forks
94
Avg merge
3h 35m
Merged PRs (30d)
1

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from r-lib/cli

All issues in r-lib/cli

Similar issues

More R issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.