Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Package: quarto
Title: R Interface to 'Quarto' Markdown Publishing System
Version: 1.4.4.9022
Version: 1.4.4.9023
Authors@R: c(
person("JJ", "Allaire", , "[email protected]", role = "aut",
comment = c(ORCID = "0000-0003-0174-9868")),
Expand Down
2 changes: 2 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# quarto (development version)

- Added NA value detection in YAML processing to prevent silent failures when passing R's `NA` values to Quarto CLI. Functions `as_yaml()` and `write_yaml()` now validate for NA values and provide clear error messages with actionable suggestions. This addresses issues where R's `NA` values get converted to YAML strings (like `.na.real`) that Quarto doesn't recognize as missing values, because they are not supported in YAML 1.2 spec. This is to help users handle missing data appropriately before passing to Quarto (#168).

- Added `add_spin_preamble()` function to add YAML preambles to R scripts for use with Quarto Script rendering support. The function automatically detects existing preambles and provides flexible customization options through `title` and `preamble` parameters (#164).

- `quarto_create_project()` gains a `title` argument to set the project title independently from the directory name. This allows creating projects with custom titles, including when using `name = "."` to create a project in the current directory (thanks, @davidkane9, #148). This matches with `--title` addition for `quarto create project` in Quarto CLI v1.5.15.
Expand Down
34 changes: 33 additions & 1 deletion R/utils.R
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@ yaml_handlers <- list(

#' @importFrom yaml as.yaml
as_yaml <- function(x) {
check_params_for_na(x)
yaml::as.yaml(x, handlers = yaml_handlers)
}

#' @importFrom yaml write_yaml
write_yaml <- function(x, file) {
check_params_for_na(x)
yaml::write_yaml(x, file, handlers = yaml_handlers)
}

Expand All @@ -30,6 +32,36 @@ as_yaml_block <- function(x) {
paste0("---\n", yaml_content, "---\n")
}

check_params_for_na <- function(x) {
# Recursively check for NA values
check_na_recursive <- function(data, path = "") {
if (is.list(data)) {
for (i in seq_along(data)) {
name <- names(data)[i] %||% as.character(i)
new_path <- if (path == "") name else paste0(path, "$", name)
check_na_recursive(data[[i]], new_path)
}
} else if (any(is.na(data) & !is.nan(data))) {
# Found NA values (excluding NaN which is mathematically valid)
na_positions <- which(is.na(data) & !is.nan(data))
n_na <- length(na_positions)

cli::cli_abort(c(
"{.code NA} values detected in parameter {.field {path}}",
"x" = "Found NA at position{if (n_na > 1) 's' else ''}: {.val {na_positions}}",
"i" = "Quarto CLI uses YAML 1.2 spec which cannot process R's {.code NA} values",
"i" = "R's {.code NA} gets converted to YAML strings (like {.code .na.real}) that Quarto doesn't recognize as missing values",
" " = "Consider these alternatives:",
"*" = "Remove NA values from your data before passing to Quarto",
"*" = "Use {.code NULL} instead of {.code NA} for missing optional parameters",
"*" = "Handle missing values within your document code using conditional logic"
))
}
}

check_na_recursive(x)
}


# inline knitr:::merge_list()
merge_list <- function(x, y) {
Expand All @@ -38,7 +70,7 @@ merge_list <- function(x, y) {
}

`%||%` <- function(x, y) {
if (is_null(x)) y else x
if (rlang::is_null(x)) y else x
}

in_positron <- function() {
Expand Down
105 changes: 105 additions & 0 deletions tests/testthat/_snaps/utils.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# check_params_for_na detects NA in simple vectors

Code
check_params_for_na(bad_params)
Condition
Error in `check_na_recursive()`:
! `NA` values detected in parameter values
x Found NA at position: 2
i Quarto CLI uses YAML 1.2 spec which cannot process R's `NA` values
i R's `NA` gets converted to YAML strings (like `.na.real`) that Quarto doesn't recognize as missing values
Consider these alternatives:
* Remove NA values from your data before passing to Quarto
* Use `NULL` instead of `NA` for missing optional parameters
* Handle missing values within your document code using conditional logic

# check_params_for_na detects NA in nested structures

Code
check_params_for_na(nested_params)
Condition
Error in `check_na_recursive()`:
! `NA` values detected in parameter data$subset
x Found NA at position: 2
i Quarto CLI uses YAML 1.2 spec which cannot process R's `NA` values
i R's `NA` gets converted to YAML strings (like `.na.real`) that Quarto doesn't recognize as missing values
Consider these alternatives:
* Remove NA values from your data before passing to Quarto
* Use `NULL` instead of `NA` for missing optional parameters
* Handle missing values within your document code using conditional logic

# check_params_for_na shows correct NA positions

Code
check_params_for_na(multi_na_params)
Condition
Error in `check_na_recursive()`:
! `NA` values detected in parameter x
x Found NA at positions: 2 and 4
i Quarto CLI uses YAML 1.2 spec which cannot process R's `NA` values
i R's `NA` gets converted to YAML strings (like `.na.real`) that Quarto doesn't recognize as missing values
Consider these alternatives:
* Remove NA values from your data before passing to Quarto
* Use `NULL` instead of `NA` for missing optional parameters
* Handle missing values within your document code using conditional logic

# as_yaml detects NA in simple vectors

Code
as_yaml(list(values = c(1, NA, 3)))
Condition
Error in `check_na_recursive()`:
! `NA` values detected in parameter values
x Found NA at position: 2
i Quarto CLI uses YAML 1.2 spec which cannot process R's `NA` values
i R's `NA` gets converted to YAML strings (like `.na.real`) that Quarto doesn't recognize as missing values
Consider these alternatives:
* Remove NA values from your data before passing to Quarto
* Use `NULL` instead of `NA` for missing optional parameters
* Handle missing values within your document code using conditional logic

# write_yaml detects NA in nested structures

Code
write_yaml(list(data = list(subset = c(1, NA, 3))), tempfile())
Condition
Error in `check_na_recursive()`:
! `NA` values detected in parameter data$subset
x Found NA at position: 2
i Quarto CLI uses YAML 1.2 spec which cannot process R's `NA` values
i R's `NA` gets converted to YAML strings (like `.na.real`) that Quarto doesn't recognize as missing values
Consider these alternatives:
* Remove NA values from your data before passing to Quarto
* Use `NULL` instead of `NA` for missing optional parameters
* Handle missing values within your document code using conditional logic

# as_yaml shows correct NA positions

Code
as_yaml(list(x = c(1, NA, 3, NA)))
Condition
Error in `check_na_recursive()`:
! `NA` values detected in parameter x
x Found NA at positions: 2 and 4
i Quarto CLI uses YAML 1.2 spec which cannot process R's `NA` values
i R's `NA` gets converted to YAML strings (like `.na.real`) that Quarto doesn't recognize as missing values
Consider these alternatives:
* Remove NA values from your data before passing to Quarto
* Use `NULL` instead of `NA` for missing optional parameters
* Handle missing values within your document code using conditional logic

# quarto_render uses write_yaml validation

Code
quarto_render("test.qmd", execute_params = list(bad_param = c(1, NA)))
Condition
Error in `check_na_recursive()`:
! `NA` values detected in parameter bad_param
x Found NA at position: 2
i Quarto CLI uses YAML 1.2 spec which cannot process R's `NA` values
i R's `NA` gets converted to YAML strings (like `.na.real`) that Quarto doesn't recognize as missing values
Consider these alternatives:
* Remove NA values from your data before passing to Quarto
* Use `NULL` instead of `NA` for missing optional parameters
* Handle missing values within your document code using conditional logic

107 changes: 107 additions & 0 deletions tests/testthat/test-utils.R
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,110 @@ test_that("has_internet works correctly", {
expect_true(has_internet("https://www.example.com/"))
expect_false(has_internet("https://www.invalid-host-that-does-not-exist.com"))
})

test_that("check_params_for_na allows clean parameters", {
# Should pass without error
good_params <- list(
a = 1:5,
b = c("hello", "world"),
nested = list(x = 10, y = 20)
)

expect_silent(check_params_for_na(good_params))
})

test_that("check_params_for_na detects NA in simple vectors", {
bad_params <- list(values = c(1, NA, 3))

expect_snapshot(
error = TRUE,
check_params_for_na(bad_params),
)
})

test_that("check_params_for_na detects NA in nested structures", {
nested_params <- list(
data = list(
subset = c(1, NA, 3)
)
)

expect_snapshot(
error = TRUE,
check_params_for_na(nested_params),
)
})

test_that("check_params_for_na shows correct NA positions", {
multi_na_params <- list(x = c(1, NA, 3, NA, 5))

expect_snapshot(
error = TRUE,
check_params_for_na(multi_na_params),
)
})

test_that("check_params_for_na handles different NA types", {
# Test different NA types
expect_error(
check_params_for_na(list(x = NA_real_)),
"NA.*values detected"
)

expect_error(
check_params_for_na(list(x = NA_integer_)),
"NA.*values detected"
)

expect_error(
check_params_for_na(list(x = NA_character_)),
"NA.*values detected"
)

expect_error(
check_params_for_na(list(x = c(TRUE, NA))),
"NA.*values detected"
)
})

test_that("as_yaml detects NA in simple vectors", {
expect_snapshot(
as_yaml(list(values = c(1, NA, 3))),
error = TRUE
)
})

test_that("write_yaml detects NA in nested structures", {
expect_snapshot(
write_yaml(list(data = list(subset = c(1, NA, 3))), tempfile()),
error = TRUE
)
})

test_that("as_yaml shows correct NA positions", {
expect_snapshot(
as_yaml(list(x = c(1, NA, 3, NA))),
error = TRUE
)
})

test_that("as_yaml allows NaN values", {
expect_no_error(
as_yaml(list(values = c(1, NaN, 3)))
)
})

test_that("write_yaml allows clean data", {
temp_file <- tempfile()
expect_no_error(
quarto:::write_yaml(list(param1 = c(1, 2, 3), param2 = "test"), temp_file)
)
unlink(temp_file)
})

test_that("quarto_render uses write_yaml validation", {
expect_snapshot(
quarto_render("test.qmd", execute_params = list(bad_param = c(1, NA))),
error = TRUE
)
})