Skip to content

Commit 7f8aa41

Browse files
authored
check for NA in YAML from R to guide user as this is not supported for Quarto usage. (#264)
* quarto_render changes the class of vectors containing NA values Fixes #168 We now catch any NA in params from R side to ask user to manually change them as they won't be supported and will be lost on Quarto side, as NA is not YAML compatible. * keep R file convention with test file * reorganize function and check in as_yaml directly * Add news item * bump version
1 parent d06e627 commit 7f8aa41

File tree

5 files changed

+248
-2
lines changed

5 files changed

+248
-2
lines changed

DESCRIPTION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Package: quarto
22
Title: R Interface to 'Quarto' Markdown Publishing System
3-
Version: 1.4.4.9022
3+
Version: 1.4.4.9023
44
Authors@R: c(
55
person("JJ", "Allaire", , "[email protected]", role = "aut",
66
comment = c(ORCID = "0000-0003-0174-9868")),

NEWS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# quarto (development version)
22

3+
- 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).
4+
35
- 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).
46

57
- `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.

R/utils.R

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,13 @@ yaml_handlers <- list(
1616

1717
#' @importFrom yaml as.yaml
1818
as_yaml <- function(x) {
19+
check_params_for_na(x)
1920
yaml::as.yaml(x, handlers = yaml_handlers)
2021
}
2122

2223
#' @importFrom yaml write_yaml
2324
write_yaml <- function(x, file) {
25+
check_params_for_na(x)
2426
yaml::write_yaml(x, file, handlers = yaml_handlers)
2527
}
2628

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

35+
check_params_for_na <- function(x) {
36+
# Recursively check for NA values
37+
check_na_recursive <- function(data, path = "") {
38+
if (is.list(data)) {
39+
for (i in seq_along(data)) {
40+
name <- names(data)[i] %||% as.character(i)
41+
new_path <- if (path == "") name else paste0(path, "$", name)
42+
check_na_recursive(data[[i]], new_path)
43+
}
44+
} else if (any(is.na(data) & !is.nan(data))) {
45+
# Found NA values (excluding NaN which is mathematically valid)
46+
na_positions <- which(is.na(data) & !is.nan(data))
47+
n_na <- length(na_positions)
48+
49+
cli::cli_abort(c(
50+
"{.code NA} values detected in parameter {.field {path}}",
51+
"x" = "Found NA at position{if (n_na > 1) 's' else ''}: {.val {na_positions}}",
52+
"i" = "Quarto CLI uses YAML 1.2 spec which cannot process R's {.code NA} values",
53+
"i" = "R's {.code NA} gets converted to YAML strings (like {.code .na.real}) that Quarto doesn't recognize as missing values",
54+
" " = "Consider these alternatives:",
55+
"*" = "Remove NA values from your data before passing to Quarto",
56+
"*" = "Use {.code NULL} instead of {.code NA} for missing optional parameters",
57+
"*" = "Handle missing values within your document code using conditional logic"
58+
))
59+
}
60+
}
61+
62+
check_na_recursive(x)
63+
}
64+
3365

3466
# inline knitr:::merge_list()
3567
merge_list <- function(x, y) {
@@ -38,7 +70,7 @@ merge_list <- function(x, y) {
3870
}
3971

4072
`%||%` <- function(x, y) {
41-
if (is_null(x)) y else x
73+
if (rlang::is_null(x)) y else x
4274
}
4375

4476
in_positron <- function() {

tests/testthat/_snaps/utils.md

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# check_params_for_na detects NA in simple vectors
2+
3+
Code
4+
check_params_for_na(bad_params)
5+
Condition
6+
Error in `check_na_recursive()`:
7+
! `NA` values detected in parameter values
8+
x Found NA at position: 2
9+
i Quarto CLI uses YAML 1.2 spec which cannot process R's `NA` values
10+
i R's `NA` gets converted to YAML strings (like `.na.real`) that Quarto doesn't recognize as missing values
11+
Consider these alternatives:
12+
* Remove NA values from your data before passing to Quarto
13+
* Use `NULL` instead of `NA` for missing optional parameters
14+
* Handle missing values within your document code using conditional logic
15+
16+
# check_params_for_na detects NA in nested structures
17+
18+
Code
19+
check_params_for_na(nested_params)
20+
Condition
21+
Error in `check_na_recursive()`:
22+
! `NA` values detected in parameter data$subset
23+
x Found NA at position: 2
24+
i Quarto CLI uses YAML 1.2 spec which cannot process R's `NA` values
25+
i R's `NA` gets converted to YAML strings (like `.na.real`) that Quarto doesn't recognize as missing values
26+
Consider these alternatives:
27+
* Remove NA values from your data before passing to Quarto
28+
* Use `NULL` instead of `NA` for missing optional parameters
29+
* Handle missing values within your document code using conditional logic
30+
31+
# check_params_for_na shows correct NA positions
32+
33+
Code
34+
check_params_for_na(multi_na_params)
35+
Condition
36+
Error in `check_na_recursive()`:
37+
! `NA` values detected in parameter x
38+
x Found NA at positions: 2 and 4
39+
i Quarto CLI uses YAML 1.2 spec which cannot process R's `NA` values
40+
i R's `NA` gets converted to YAML strings (like `.na.real`) that Quarto doesn't recognize as missing values
41+
Consider these alternatives:
42+
* Remove NA values from your data before passing to Quarto
43+
* Use `NULL` instead of `NA` for missing optional parameters
44+
* Handle missing values within your document code using conditional logic
45+
46+
# as_yaml detects NA in simple vectors
47+
48+
Code
49+
as_yaml(list(values = c(1, NA, 3)))
50+
Condition
51+
Error in `check_na_recursive()`:
52+
! `NA` values detected in parameter values
53+
x Found NA at position: 2
54+
i Quarto CLI uses YAML 1.2 spec which cannot process R's `NA` values
55+
i R's `NA` gets converted to YAML strings (like `.na.real`) that Quarto doesn't recognize as missing values
56+
Consider these alternatives:
57+
* Remove NA values from your data before passing to Quarto
58+
* Use `NULL` instead of `NA` for missing optional parameters
59+
* Handle missing values within your document code using conditional logic
60+
61+
# write_yaml detects NA in nested structures
62+
63+
Code
64+
write_yaml(list(data = list(subset = c(1, NA, 3))), tempfile())
65+
Condition
66+
Error in `check_na_recursive()`:
67+
! `NA` values detected in parameter data$subset
68+
x Found NA at position: 2
69+
i Quarto CLI uses YAML 1.2 spec which cannot process R's `NA` values
70+
i R's `NA` gets converted to YAML strings (like `.na.real`) that Quarto doesn't recognize as missing values
71+
Consider these alternatives:
72+
* Remove NA values from your data before passing to Quarto
73+
* Use `NULL` instead of `NA` for missing optional parameters
74+
* Handle missing values within your document code using conditional logic
75+
76+
# as_yaml shows correct NA positions
77+
78+
Code
79+
as_yaml(list(x = c(1, NA, 3, NA)))
80+
Condition
81+
Error in `check_na_recursive()`:
82+
! `NA` values detected in parameter x
83+
x Found NA at positions: 2 and 4
84+
i Quarto CLI uses YAML 1.2 spec which cannot process R's `NA` values
85+
i R's `NA` gets converted to YAML strings (like `.na.real`) that Quarto doesn't recognize as missing values
86+
Consider these alternatives:
87+
* Remove NA values from your data before passing to Quarto
88+
* Use `NULL` instead of `NA` for missing optional parameters
89+
* Handle missing values within your document code using conditional logic
90+
91+
# quarto_render uses write_yaml validation
92+
93+
Code
94+
quarto_render("test.qmd", execute_params = list(bad_param = c(1, NA)))
95+
Condition
96+
Error in `check_na_recursive()`:
97+
! `NA` values detected in parameter bad_param
98+
x Found NA at position: 2
99+
i Quarto CLI uses YAML 1.2 spec which cannot process R's `NA` values
100+
i R's `NA` gets converted to YAML strings (like `.na.real`) that Quarto doesn't recognize as missing values
101+
Consider these alternatives:
102+
* Remove NA values from your data before passing to Quarto
103+
* Use `NULL` instead of `NA` for missing optional parameters
104+
* Handle missing values within your document code using conditional logic
105+

tests/testthat/test-utils.R

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,110 @@ test_that("has_internet works correctly", {
33
expect_true(has_internet("https://www.example.com/"))
44
expect_false(has_internet("https://www.invalid-host-that-does-not-exist.com"))
55
})
6+
7+
test_that("check_params_for_na allows clean parameters", {
8+
# Should pass without error
9+
good_params <- list(
10+
a = 1:5,
11+
b = c("hello", "world"),
12+
nested = list(x = 10, y = 20)
13+
)
14+
15+
expect_silent(check_params_for_na(good_params))
16+
})
17+
18+
test_that("check_params_for_na detects NA in simple vectors", {
19+
bad_params <- list(values = c(1, NA, 3))
20+
21+
expect_snapshot(
22+
error = TRUE,
23+
check_params_for_na(bad_params),
24+
)
25+
})
26+
27+
test_that("check_params_for_na detects NA in nested structures", {
28+
nested_params <- list(
29+
data = list(
30+
subset = c(1, NA, 3)
31+
)
32+
)
33+
34+
expect_snapshot(
35+
error = TRUE,
36+
check_params_for_na(nested_params),
37+
)
38+
})
39+
40+
test_that("check_params_for_na shows correct NA positions", {
41+
multi_na_params <- list(x = c(1, NA, 3, NA, 5))
42+
43+
expect_snapshot(
44+
error = TRUE,
45+
check_params_for_na(multi_na_params),
46+
)
47+
})
48+
49+
test_that("check_params_for_na handles different NA types", {
50+
# Test different NA types
51+
expect_error(
52+
check_params_for_na(list(x = NA_real_)),
53+
"NA.*values detected"
54+
)
55+
56+
expect_error(
57+
check_params_for_na(list(x = NA_integer_)),
58+
"NA.*values detected"
59+
)
60+
61+
expect_error(
62+
check_params_for_na(list(x = NA_character_)),
63+
"NA.*values detected"
64+
)
65+
66+
expect_error(
67+
check_params_for_na(list(x = c(TRUE, NA))),
68+
"NA.*values detected"
69+
)
70+
})
71+
72+
test_that("as_yaml detects NA in simple vectors", {
73+
expect_snapshot(
74+
as_yaml(list(values = c(1, NA, 3))),
75+
error = TRUE
76+
)
77+
})
78+
79+
test_that("write_yaml detects NA in nested structures", {
80+
expect_snapshot(
81+
write_yaml(list(data = list(subset = c(1, NA, 3))), tempfile()),
82+
error = TRUE
83+
)
84+
})
85+
86+
test_that("as_yaml shows correct NA positions", {
87+
expect_snapshot(
88+
as_yaml(list(x = c(1, NA, 3, NA))),
89+
error = TRUE
90+
)
91+
})
92+
93+
test_that("as_yaml allows NaN values", {
94+
expect_no_error(
95+
as_yaml(list(values = c(1, NaN, 3)))
96+
)
97+
})
98+
99+
test_that("write_yaml allows clean data", {
100+
temp_file <- tempfile()
101+
expect_no_error(
102+
quarto:::write_yaml(list(param1 = c(1, 2, 3), param2 = "test"), temp_file)
103+
)
104+
unlink(temp_file)
105+
})
106+
107+
test_that("quarto_render uses write_yaml validation", {
108+
expect_snapshot(
109+
quarto_render("test.qmd", execute_params = list(bad_param = c(1, NA))),
110+
error = TRUE
111+
)
112+
})

0 commit comments

Comments
 (0)