Skip to content

Commit 60c0dfa

Browse files
authored
Merge pull request #1166 from stan-dev/print_stan_file
New function `print_stan_file()` for color formatted Stan code in quarto and R markdown
2 parents e4ee0a1 + ed63153 commit 60c0dfa

7 files changed

Lines changed: 311 additions & 102 deletions

File tree

NAMESPACE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export(draws_to_csv)
3737
export(eng_cmdstan)
3838
export(install_cmdstan)
3939
export(print_example_program)
40+
export(print_stan_file)
4041
export(read_cmdstan_csv)
4142
export(rebuild_cmdstan)
4243
export(register_knitr_engine)

R/example.R

Lines changed: 0 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -84,104 +84,3 @@ print_example_program <-
8484
code <- readLines(system.file(paste0(example, ".stan"), package = "cmdstanr"))
8585
cat(code, sep = "\n")
8686
}
87-
88-
89-
90-
# including write_stan_file in example.R since it will be mostly used
91-
# in examples
92-
93-
#' Write Stan code to a file
94-
#'
95-
#' Convenience function for writing Stan code to a (possibly
96-
#' [temporary][base::tempfile]) file with a `.stan` extension. By default, the
97-
#' file name is chosen deterministically based on a [hash][rlang::hash()]
98-
#' of the Stan code, and the file is not overwritten if it already has correct
99-
#' contents. This means that calling this function multiple times with the same
100-
#' Stan code will reuse the compiled model. This also however means that the
101-
#' function is potentially not thread-safe. Using `hash_salt = Sys.getpid()`
102-
#' should ensure thread-safety in the rare cases when it is needed.
103-
#'
104-
#' @export
105-
#' @param code (character vector) The Stan code to write to the file. This can
106-
#' be a character vector of length one (a string) containing the entire Stan
107-
#' program or a character vector with each element containing one line of the
108-
#' Stan program.
109-
#' @param dir (string) An optional path to the directory where the file will be
110-
#' written. If omitted, a global option `cmdstanr_write_stan_file_dir` is
111-
#' used. If the global options is not set, [temporary directory][base::tempdir]
112-
#' is used.
113-
#' @param basename (string) If `dir` is specified, optionally the basename to
114-
#' use for the file created. If not specified a file name is generated
115-
#' from [hashing][rlang::hash()] the code.
116-
#' @param force_overwrite (logical) If set to `TRUE` the file will always be
117-
#' overwritten and thus the resulting model will always be recompiled.
118-
#' @param hash_salt (string) Text to add to the model code prior to hashing to
119-
#' determine the file name if `basename` is not set.
120-
#' @return The path to the file.
121-
#'
122-
#' @examples
123-
#' # stan program as a single string
124-
#' stan_program <- "
125-
#' data {
126-
#' int<lower=0> N;
127-
#' array[N] int<lower=0,upper=1> y;
128-
#' }
129-
#' parameters {
130-
#' real<lower=0,upper=1> theta;
131-
#' }
132-
#' model {
133-
#' y ~ bernoulli(theta);
134-
#' }
135-
#' "
136-
#'
137-
#' f <- write_stan_file(stan_program)
138-
#' print(f)
139-
#'
140-
#' lines <- readLines(f)
141-
#' print(lines)
142-
#' cat(lines, sep = "\n")
143-
#'
144-
#' # stan program as character vector of lines
145-
#' f2 <- write_stan_file(lines)
146-
#' identical(readLines(f), readLines(f2))
147-
#'
148-
write_stan_file <- function(code,
149-
dir = getOption("cmdstanr_write_stan_file_dir", tempdir()),
150-
basename = NULL,
151-
force_overwrite = FALSE,
152-
hash_salt = "") {
153-
dir <- absolute_path(dir)
154-
if (!dir.exists(dir)) {
155-
dir.create(dir, recursive = TRUE)
156-
}
157-
collapsed_code <- paste0(code, collapse = "\n")
158-
159-
if (!is.null(basename)) {
160-
if (!endsWith(basename, ".stan")) {
161-
basename <- paste0(basename, ".stan")
162-
}
163-
file <- file.path(dir, basename)
164-
} else {
165-
require_suggested_package("rlang")
166-
hash <- rlang::hash(paste0(hash_salt, collapsed_code))
167-
file <- file.path(dir, paste0("model_", hash, ".stan"))
168-
}
169-
overwrite <- TRUE
170-
# Do not overwrite file if it has the correct contents (to avoid recompilation)
171-
if (!force_overwrite && file.exists(file)) {
172-
tryCatch({
173-
file_contents <- paste0(readLines(file), collapse = "\n")
174-
if (gsub("\r|\n", "\n", file_contents) == gsub("\r|\n", "\n", collapsed_code)) {
175-
overwrite <- FALSE
176-
}
177-
},
178-
error = function(e) {
179-
warning("Error when checking old file contents", e)
180-
})
181-
}
182-
183-
if (overwrite) {
184-
cat(code, file = file, sep = "\n")
185-
}
186-
file
187-
}

R/file.R

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
#' Write Stan code to a file
2+
#'
3+
#' Convenience function for writing Stan code to a (possibly
4+
#' [temporary][base::tempfile]) file with a `.stan` extension. By default, the
5+
#' file name is chosen deterministically based on a [hash][rlang::hash()]
6+
#' of the Stan code, and the file is not overwritten if it already has correct
7+
#' contents. This means that calling this function multiple times with the same
8+
#' Stan code will reuse the compiled model. This also however means that the
9+
#' function is potentially not thread-safe. Using `hash_salt = Sys.getpid()`
10+
#' should ensure thread-safety in the rare cases when it is needed.
11+
#'
12+
#' @export
13+
#' @param code (character vector) The Stan code to write to the file. This can
14+
#' be a character vector of length one (a string) containing the entire Stan
15+
#' program or a character vector with each element containing one line of the
16+
#' Stan program.
17+
#' @param dir (string) An optional path to the directory where the file will be
18+
#' written. If omitted, a global option `cmdstanr_write_stan_file_dir` is
19+
#' used. If the global options is not set, [temporary directory][base::tempdir]
20+
#' is used.
21+
#' @param basename (string) If `dir` is specified, optionally the basename to
22+
#' use for the file created. If not specified a file name is generated
23+
#' from [hashing][rlang::hash()] the code.
24+
#' @param force_overwrite (logical) If set to `TRUE` the file will always be
25+
#' overwritten and thus the resulting model will always be recompiled.
26+
#' @param hash_salt (string) Text to add to the model code prior to hashing to
27+
#' determine the file name if `basename` is not set.
28+
#' @return The path to the file.
29+
#'
30+
#' @examples
31+
#' # stan program as a single string
32+
#' stan_program <- "
33+
#' data {
34+
#' int<lower=0> N;
35+
#' array[N] int<lower=0,upper=1> y;
36+
#' }
37+
#' parameters {
38+
#' real<lower=0,upper=1> theta;
39+
#' }
40+
#' model {
41+
#' y ~ bernoulli(theta);
42+
#' }
43+
#' "
44+
#'
45+
#' f <- write_stan_file(stan_program)
46+
#' print(f)
47+
#'
48+
#' lines <- readLines(f)
49+
#' print(lines)
50+
#' cat(lines, sep = "\n")
51+
#'
52+
#' # stan program as character vector of lines
53+
#' f2 <- write_stan_file(lines)
54+
#' identical(readLines(f), readLines(f2))
55+
#'
56+
write_stan_file <- function(code,
57+
dir = getOption("cmdstanr_write_stan_file_dir", tempdir()),
58+
basename = NULL,
59+
force_overwrite = FALSE,
60+
hash_salt = "") {
61+
dir <- absolute_path(dir)
62+
if (!dir.exists(dir)) {
63+
dir.create(dir, recursive = TRUE)
64+
}
65+
collapsed_code <- paste0(code, collapse = "\n")
66+
67+
if (!is.null(basename)) {
68+
if (!endsWith(basename, ".stan")) {
69+
basename <- paste0(basename, ".stan")
70+
}
71+
file <- file.path(dir, basename)
72+
} else {
73+
require_suggested_package("rlang")
74+
hash <- rlang::hash(paste0(hash_salt, collapsed_code))
75+
file <- file.path(dir, paste0("model_", hash, ".stan"))
76+
}
77+
overwrite <- TRUE
78+
# Do not overwrite file if it has the correct contents (to avoid recompilation)
79+
if (!force_overwrite && file.exists(file)) {
80+
tryCatch({
81+
file_contents <- paste0(readLines(file), collapse = "\n")
82+
if (gsub("\r|\n", "\n", file_contents) == gsub("\r|\n", "\n", collapsed_code)) {
83+
overwrite <- FALSE
84+
}
85+
},
86+
error = function(e) {
87+
warning("Error when checking old file contents", e)
88+
})
89+
}
90+
91+
if (overwrite) {
92+
cat(code, file = file, sep = "\n")
93+
}
94+
file
95+
}
96+
97+
98+
#' Print a Stan file with syntax highlighting in Quarto and R Markdown
99+
#'
100+
#' Prints the contents of a Stan file, optionally with syntax highlighting
101+
#' when used in a Quarto or R Markdown document. When called inside a
102+
#' [knitr][knitr::knitr-package] code chunk with the chunk option
103+
#' `output: asis` (or `results: asis` in R Markdown), the output is a
104+
#' fenced Stan code block that Quarto renders with syntax highlighting.
105+
#' When called interactively or without `output: asis`, the code is
106+
#' printed as plain text via [writeLines()].
107+
#'
108+
#' @export
109+
#' @param file (string) Path to a `.stan` file.
110+
#' @param fold (logical) Whether to wrap the output in an HTML `<details>` block
111+
#' so that the code is collapsed (folded) by default. Only has an effect when
112+
#' rendering with `output: asis` and when outputting HTML. Defaults to
113+
#' `FALSE`.
114+
#' @param summary (string) The summary text shown in the fold toggle
115+
#' when `fold = TRUE`. Defaults to `"Stan model code"`.
116+
#' @return The file path (invisibly).
117+
#'
118+
#' @section Quarto usage:
119+
#' Use in a Quarto code chunk with `output: asis` to get syntax
120+
#' highlighting:
121+
#'
122+
#' ````
123+
#' ```{r}
124+
#' #| output: asis
125+
#' print_stan_file("path/to/model.stan")
126+
#' ```
127+
#' ````
128+
#'
129+
#' To make the code block collapsible:
130+
#'
131+
#' ````
132+
#' ```{r}
133+
#' #| output: asis
134+
#' print_stan_file("path/to/model.stan", fold = TRUE)
135+
#' ```
136+
#' ````
137+
#'
138+
#' @examples
139+
#' stan_file <- write_stan_file("
140+
#' parameters {
141+
#' real y;
142+
#' }
143+
#' model {
144+
#' y ~ std_normal();
145+
#' }
146+
#' ")
147+
#'
148+
#' # Prints plain code at the console
149+
#' print_stan_file(stan_file)
150+
#'
151+
print_stan_file <- function(file, fold = FALSE, summary = "Stan model code") {
152+
code <- readLines(file)
153+
if (is_knitr_asis_output()) {
154+
if (fold) {
155+
cat("<details><summary>", summary, "</summary>\n\n", sep = "")
156+
}
157+
cat("```stan\n")
158+
cat(code, sep = "\n")
159+
cat("\n```\n")
160+
if (fold) {
161+
cat("\n</details>\n")
162+
}
163+
} else {
164+
writeLines(code)
165+
}
166+
invisible(file)
167+
}
168+
169+
is_knitr_asis_output <- function() {
170+
isTRUE(getOption("knitr.in.progress")) &&
171+
requireNamespace("knitr", quietly = TRUE) &&
172+
identical(knitr::opts_current$get("results"), "asis")
173+
}

_pkgdown.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ reference:
9595
- read_cmdstan_csv
9696
- write_stan_json
9797
- write_stan_file
98+
- print_stan_file
9899
- draws_to_csv
99100
- as_mcmc.list
100101
- as_draws.CmdStanMCMC

man/print_stan_file.Rd

Lines changed: 65 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/write_stan_file.Rd

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)